find element in xml, and take other element value - c#

My xml look like:
<CURRENCIES>
<LAST_UPDATE>2014-01-17</LAST_UPDATE>
<CURRENCY>
<NAME>Dollar</NAME>
<UNIT>1</UNIT>
<CURRENCYCODE>USD</CURRENCYCODE>
<COUNTRY>USA</COUNTRY>
<RATE>3.489</RATE>
<CHANGE>-0.086</CHANGE>
</CURRENCY>
</CURRENCIES>
I want to find specific currncy from the element "NAME" and "COUNTRY", and take the value "RATE".
I wrote:
public void ConvertCurrency(int value, string currency)
{
WebClient webClient = new WebClient();
XDocument xml = new XDocument();
webClient.DownloadFile("http://www.boi.org.il/currency.xml", #"currency.xml");
XDocument currency_xml = XDocument.Load("currency.xml");
var findCurrency = from currency1 in currency_xml.Descendants("CURRENCIES")
where (Convert.ToString(currency1.Element("CURRENCY").Element("NAME").Value) == currency) && (Convert.ToString(currency1.Element("CURRENCY").Element("COUNTRY").Value) == "USA")
select currency1.Element("RATE").Value;
int rate = Convert.ToInt32(findCurrency);
int result = value * rate;
Console.WriteLine("Result:{0}",result);
}
How can I do it right?

Your query is over CURRENCIES elements, and you're only looking at the first CURRENCY child. Then you're looking for a RATE child within CURRENCIES rather than CURRENCY. Additionally, you're getting a sequence of integers - and that isn't a single int. I think you want:
// Load directly from the web - it's simpler...
XDocument doc = XDocument.Load("http://www.boi.org.il/currency.xml");
var element = doc.Root
.Elements("CURRENCY")
.Where(x => (string) x.Element("COUNTRY") == "USA") &&
(string) x.Element("NAME") == currency)
.FirstOrDefault();
if (element != null)
{
// You don't want an int here - you shouldn't lose information!
decimal rate = (decimal) element.Element("RATE");
decimal result = value * rate;
Console.WriteLine("Result: {0}", result);
}
else
{
Console.WriteLine("Couldn't find currency rate for USA");
}
Notes:
I haven't used a query expression here as it wouldn't help to simplify anything
This will fail if there's a CURRENCY element for USA without a RATE element; do you need to fix that?
I prefer to use the user-defined conversions in XElement instead of using Convert.ToXyz; they're specifically geared up for XML values (so won't use the culture when converting decimal values, for example)

Jon Skeet's answer is complete, anyway here is your fixed query with LINQ syntax
var findCurrency = (from c in currency_xml.Descendants("CURRENCY")
where (string)c.Element("NAME") == currency
&& (string)c.Element("COUNTRY") == "USA"
select (string)c.Element("RATE")).FirstOrDefault();

Related

Sort List by date values

I have the following list -
List<string> finalMessageContent
where
finalMessageContent[0] = "<div class="mHr" id="mFID">
<div id="postedDate">11/12/2015 11:12:16</div>
</div>" // etc etc
I am trying to sort the list by a particular value located in the entires - postedDate tag.
Firstly I have create an new object and then serialized it to make the html elements able to be parsed -
string[][] newfinalMessageContent = finalMessageContent.Select(x => new string[] { x }).ToArray();
string json = JsonConvert.SerializeObject(newfinalMessageContent);
JArray markerData = JArray.Parse(json);
And then used Linq to try and sort using OrderByDescending -
var items = markerData.OrderByDescending(x => x["postedDate"].ToString()).ToList();
However this is failing when trying to parse the entry with -
Accessed JArray values with invalid key value: "postedDate". Array position index expected.
Perhaps linq is not the way to go here however it seemed like the most optimised, where am I going wrong?
First, i would not use string methods, regex or a JSON-parser to parse HTML. I would use HtmlAgilityPack. Then you could provide such a method:
private static DateTime? ExtractPostedDate(string inputHtml, string controlID = "postedDate")
{
var doc = new HtmlAgilityPack.HtmlDocument();
doc.LoadHtml(inputHtml);
HtmlNode div = doc.GetElementbyId(controlID);
DateTime? result = null;
DateTime value;
if (div != null && DateTime.TryParse(div.InnerText.Trim(), DateTimeFormatInfo.InvariantInfo, DateTimeStyles.None, out value))
result = value;
return result;
}
and following LINQ query:
finalMessageContent = finalMessageContent
.Select(s => new { String = s, Date = ExtractPostedDate(s) })
.Where(x => x.Date.HasValue)
.OrderByDescending(x => x.Date.Value)
.Select(x => x.String)
.ToList();
Don't know if I get your question right.
But did you know that you can parse HTML with XPath?
foreach (var row in doc.DocumentNode.SelectNodes("//div[#id="postedDate"]"))
{
Console.WriteLine(row.InnerText);
}
this is just an example from the top of my head you might have to double-check the XPath query depending on your document. You can also consider converting it to array or parsing the date and do other transformations with it.
Like I said this is just from the top of my head. Or if the html is not so compley consider to extract the dates with an RegEx but this would be a topic for another question.
HTH
Json Serializer serializes JSON typed strings. Example here to json
To parse HTML I suggest using HtmlAgility https://htmlagilitypack.codeplex.com/
Like this:
HtmlAgilityPack.HtmlDocument htmlparsed = new HtmlAgilityPack.HtmlDocument();
htmlParsed.LoadHtml(finalMessageContent[0]);
List<HtmlNode> OrderedDivs = htmlParsed.DocumentNode.Descendants("div").
Where(a => a.Attributes.Any(af => af.Value == "postedDate")).
OrderByDescending(d => DateTime.Parse(d.InnerText)); //unsafe parsing

Using Linq to XML, how can I select the elements that have exactly x number of parent elements?

I am trying to use Linq to XML in my c# program to extract element names from an auto-generated XSD file. This means I cannot do queries based on element names/ contents, and instead need to find elements based on their position in the xsd.
I'm trying to do this with a function that returns an element's 'depth'. Somehow, if I use it in my query, I get not only the elements I need but also all underlying elements/attributes. I am guessing I need an "exclude" function in some way, but I don't know how to do this. This is the code I have at the moment:
static public void XsdReader()
{
var xsd = XDocument.Load(#"c:\XsdToLoad.xml");
var elementsAtDepthEight = from e in xsd.Descendants()
where GetElementDepth(e) == 8
select e;
foreach (var p in elementsAtDepthEight)
{
Console.WriteLine(p.ToString());
}
}
static int GetElementDepth(XElement element)
{
int result = 1;
//always return 1 as the root node has depth of 1.
//in addition, return the maximum depth returned by this method called on all the children.
if (element.Parent != null)
{
result += GetElementDepth(element.Parent);
}
return result;
}
The simplest way to measure "depth" is to count ancestors:
var elementsAtDepth8 = xsd.Descendants()
.Where(x => x.Ancestors().Count() == 8);
No need to write your own recursive code at all.
Another slightly odd alternative would be to write:
var elementsAtDepth = xsd.Elements()
.Elements()
.Elements()
.Elements()
.Elements()
.Elements()
.Elements()
.Elements();
You could obviously write a pair of recursive methods to do this:
public static IEnumerable<XElement> ElementsAtDepth(this XNode node, int depth)
{
return node.Elements().ElementsAtDepth(depth - 1);
}
public static IEnumerable<XElement> ElementsAtDepth(
this IEnumerable<XElement> elements, int depth)
{
// TODO: Validate that depth >= 0
return depth == 0 ? elements : elements.Elements().ElementsAtDepth(depth - 1);
}

Linq-to-xml to get child nodes

I am having trouble identifying how to use linq-to-xml to extract total price and individual prices from the xml below (e.g I want to get the fare price and also sum of all prices). Any help would be much appreciated especially with using the method syntax of linq-to-xml
I use the following code to get the data loaded into an xDocument and work with the xmlResponse object to parse the response.
var xmlResponse = from element in xdoc.Descendants()
select element;
and get data like
xmlResponse.SingleOrDefault(x => x.Name.LocalName == "Registration")
Below is a subset of thwe xml response :-
<StateList>
<State>
<SourceJobID>J999999999999</SourceJobID>
<TargetJobState>Complete</TargetJobState>
<TargetJobID>11111111</TargetJobID>
<TargetSystem>TESTSYSTEM</TargetSystem>
<VehicleDetails>
<Registration>TESTREGISRATION</Registration>
<Plate>11111111111</Plate>
<CO2Rating>160</CO2Rating>
<Badge>1111111</Badge>
<Description>TEST DESCRIPTION</Description>
</VehicleDetails>
<CompleteDetails>
<CompletedOn>2015-09-15T13:39:11+01:00</CompletedOn>
<JobDistance>0</JobDistance>
<WaitingTime />
<CO2Usage>0</CO2Usage>
<ChargeList>
<Charge>
<Name>Airport Pickup</Name>
<Currency>GBP</Currency>
<Price>0.00</Price>
</Charge>
<Charge>
<Name>Fare</Name>
<Currency>GBP</Currency>
<Price>0.00</Price>
</Charge>
<Charge>
<Name>Extra Stops</Name>
<Currency>GBP</Currency>
<Price>0.00</Price>
</Charge>
</ChargeList>
</CompleteDetails>
</State>
Assuming you only have a single state like in your example, you could do something like the following:
decimal fare = decimal.Parse(xml.Descendants("Charge").Single(x => x.Element("Name").Value == "Fare").Element("Price").Value);
decimal total = xml.Descendants("Charge").Sum(x => decimal.Parse(x.Element("Price").Value));
Although if you have a series of elements in your list you will have to modify that.
EDIT: If, as you say in the comments, you would like to sum only certain charges:
// Valid names of charges to sum.
string[] names = { "Airport Pickup", "Fare" };
// Iterate over every state.
foreach (var state in xml.Descendants("State"))
{
// Get all charge elements in the current state whose names are contained in 'names' - then convert their 'Price' element to decimal and sum them.
decimal stateTotal = state.Descendants("Charge").Where(x => names.Contains(x.Element("Name").Value)).Sum(x => decimal.Parse(x.Element("Price").Value));
}
if(doc.Descendants("Charge").Any())
{
var FarePrice = doc.Descendants("Charge")
.Where(x => x.Descendants("Name").First().Value.Equals("Fare")).First().Element("Price").Value;
var Sum = doc.Descendants("Charge")
.Select(x => Convert.ToDouble(x.Descendants("Price").First().Value))
.Sum();
Console.WriteLine("Fare price:{0} Sum:{1}",FarePrice,Sum);
}
It returns 35 as sum for 10 and 25 inputs.
Fiddle here : https://dotnetfiddle.net/cuHXBn

C# Reading certain element / attribute from xml file

Im trying to read a certain attributes from following xml file (as console program)
http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml
As you see inside 'forecast' element there are multiple 'time' elements. What I want is to pick certain 'time' element and then pick given thing inside of it (lets say 'symbol') and print all/any attributes it has.
I want to be able to control which 'time' element I pick and which attributes I want to print.
This far all I have managed to do is to print every 'time' element and their attributes and also I managed to print every attribute inside of given 'time' element. But I just can't figure how to control it.
With the following code, I can print everything inside the first 'time' element. Item(0) is the index of element and the for loop makes sure that I don't get empty lines. As you can see from xml file, some 'time' elements has different amount of attributes inside of them so I guess I need to call them By name insted of index.
static void xmlReader()
{
int i;
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.Load(parseLink());
foreach (XmlNode xmlNode in xmlDoc.DocumentElement.GetElementsByTagName("time").Item(0))
for (i = 0; i < xmlNode.Attributes.Count; i++)
{
Console.WriteLine(xmlNode.Attributes[i].Value);
}
Console.ReadKey();
}
Use Linq2Xml, it's much easier and convenient.
public static void Main()
{
var forecast = XDocument.Load(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml")
.Root
.Element("forecast");
foreach (var time in forecast.Elements("time")
.Where(e => e.Element("clouds")
.Attribute("value")
.Value == "overcast clouds"))
{
Console.WriteLine(time.Element("symbol").Attribute("name").Value);
}
}
Using XDocument is a bit easier here...
private static void XmlOutput()
{
var filePathAndName = #"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml";
var xmlDoc = XDocument.Load(filePathAndName);
// Get list of Nodes matching the "time" name or any other filters you wish.
var nodes = xmlDoc.Descendants().Where(nd => nd.Name.LocalName == "time");
// Filter the node list to only those where the date is as specified (and any other filters you wish).
// This could be done in the initial linq query - just making it clearer here.
nodes = nodes.Where(nd => nd.Attributes().Any(cnd => cnd.Name.LocalName == "from" && cnd.Value.Contains("2015-03-07")));
foreach (XElement element in nodes)
{
// For each descendant node where named "symbol"...
foreach(var node in element.Descendants().Where(nd => nd.Name.LocalName == "symbol"))
{
// Write out these particular attributes ("number" and "name")
string output = "";
output += node.Attributes().FirstOrDefault(nd => nd.Name.LocalName == "number").Value;
output += ", " + node.Attributes().FirstOrDefault(nd => nd.Name.LocalName == "name").Value;
Console.WriteLine(output);
}
}
Console.ReadKey();
}
Similar #aush's response, but with some formatting
var doc = XDocument.Load(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml");
var forecastEl = doc.Root.Element(XName.Get("forecast"));
var timeNodes = from e in forecastEl.Elements("time")
where e.Element("symbol")
.Attribute(XName.Get("name"))
.Value == "light snow"
select e;
var colFormat = "{0,-20} {1,-20} {2,-30}";
Console.WriteLine(colFormat, "TimeFrom", "TimeTo", "SymbolName");
foreach(var time in timeNodes)
Console.WriteLine(colFormat
, time.Attribute("from").Value
, time.Attribute("to").Value
, time.Element("symbol").Attribute("name").Value);
Results:
TimeFrom TimeTo SymbolName
2015-03-07T12:00:00 2015-03-07T15:00:00 light snow
2015-03-07T15:00:00 2015-03-07T18:00:00 light snow
You can use XmlReader (tutorial) as flowing,
Here I get from attribute from time element and name attribute sybol element.. And they are for same Time element. Also added extracting example for value of cloud
using (XmlReader reader = XmlReader.Create(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml"))
{
// Walk through all Elements
while (reader.Read())
{
// If we meet time element ; go inside and walk
if (reader.Name == "time")
{
Console.WriteLine("A new TIME ");
// Extract from attribute
String from = reader["from"];
if (from != null)
{
Console.WriteLine("\t From : " + from);
}
// Now walk through all elements inside same Time element
// Here I use do-while ; what we check is End element of time : </time> .. when we walk till we meet </time> we are inside children of same Time
// That mean we start from <time> and walk till we meet </time>
do
{
reader.Read();
if (reader.Name == "symbol")
{
// You can use this approach for any Attribute in symbol Element
String name = reader["name"];
if (name != null)
{
Console.WriteLine("\t Symbol name :" + name);
}
}
if (reader.Name == "clouds")
{
String clouds = reader["value"];
if (clouds != null)
{
Console.WriteLine("\t\t Clouds value : " + clouds);
}
}
} while (reader.NodeType != XmlNodeType.EndElement && reader.Name != "time");
}
}
}
Simply try this on your Console program..
Using my xml library here, you can search for an actual DateTime value like this:
XElement root = XElement.Load(#"http://api.openweathermap.org/data/2.5/forecast?q=lahti,fin&mode=xml");
var search = DateTime.Parse("2015-03-07T21:00:00");
XElement time = root.XPathElement("//time[#from >= {0} and #to > {1}]", search, search);
var value = time.ToString();

How to write XPath expression to select node name from its value

I'm trying to write an XPath expression to select the name of a node from its value in "qualities" and then select in "qualityNames" the value inside node whose name has previously captured.
E.g. In "qualities" - got value "4", take name "rarity3" then in "qualityNames" I got node named "rarity3" and take value "amazingrarity"
<result>
<status>1</status>
<qualities>
<Normal>0</Normal>
<rarity1>1</rarity1>
<rarity2>2</rarity2>
<vintage>3</vintage>
<rarity3>4</rarity3>
<rarity4>5</rarity4>
</qualities>
<qualityNames>
<Normal>Normal</Normal>
<rarity1>Genuine</rarity1>
<rarity2>rarity2</rarity2>
<vintage>Vintage</vintage>
<rarity3>amazingrarity</rarity3>
<rarity4>Unusual</rarity4>
</qualityNames>
</result>
I'm doing this in C# (It's a MVC App) and I'd prefer to use XPath because I'm indexing the XML and I haven't found a fastest way to query in-memory technique (this XML file has ~3MB and I'm using IndexingXPathNavigator).
Use the local-name() and text() functions + predicates. For value "4" it will be
//qualityNames/*[local-name()=local-name(//qualities/*[text() = '4'])]
Tested with http://www.xpathtester.com
Sounds like you want to create a dictionary of key/value pairs (assuming the node names are only needed to find matches and aren't important to your code).
If so, you can use the following:
var doc = XElement.Parse(#"<result>
<status>1</status>
<qualities>
<Normal>0</Normal>
<rarity1>1</rarity1>
<rarity2>2</rarity2>
<vintage>3</vintage>
<rarity3>4</rarity3>
<rarity4>5</rarity4>
</qualities>
<qualityNames>
<Normal>Normal</Normal>
<rarity1>Genuine</rarity1>
<rarity2>rarity2</rarity2>
<vintage>Vintage</vintage>
<rarity3>amazingrarity</rarity3>
<rarity4>Unusual</rarity4>
</qualityNames>
</result>");
var query = from quality in doc.XPathSelectElements("qualities/*")
join qualityName in doc.XPathSelectElements("qualityNames/*")
on quality.Name equals qualityName.Name
select new { Key = quality.Value, Value = qualityName.Value };
var qualities = query.ToDictionary(a => a.Key, a => a.Value);
var quality3 = qualities["3"];
// quality3 == "Vintage"
var quality4 = qualities["4"];
// quality4 == "amazingrarity"
EDIT: example of how to cache this dictionary
// add reference to System.Web dll
public Dictionary<string, string> GetQualities()
{
// assuming this code is in a controller
var qualities = this.HttpContext.Cache["qualities"] as Dictionary<string, string>;
if (qualities == null)
{
// LoadQualitiesFromXml() is the code above
qualities = LoadQualitiesFromXml();
this.HttpContext.Cache["qualities"] = qualities;
}
return qualities;
}
I think this is what you asked
var rarity3ValueInQualities = xml.SelectSingleNode("/result/qualities/rarity3").InnerText;
var rarity3ValueInqualityNames = xml.SelectSingleNode("/result/qualityNames/rarity3").InnerText;

Categories