views:

603

answers:

4

F# newbie

I have 2 xml files in 2 folders c:\root\a\file.xml and c:\root\b\file.xml They have identical structure

<parent>
   <property name="firstName">Jane</property>
   <property name="lastName">...</property>
   <property name="dateOfBirth">...</property>>
</parent>

I need to chose the file which property node with name "firstName" has the value Jane. In F# (possibly by using System.Xml.Linq) I have tried several solutions but none working so far. Anyone willing to help?

+5  A: 

You don't need to use LINQ for this. Here's one way to do it:

open System.Xml.XPath

let getName (filename:string) =
  let xpath = XPathDocument(filename).CreateNavigator()
  let node = xpath.SelectSingleNode(@"parent/property[@name='firstName']")
  node.Value

let files = [@"c:\root\a\file.xml"; @"c:\root\b\file.xml"]
let fileForJane = files |> List.find (fun file -> getName file = "Jane")
kvb
+9  A: 

It would be useful if you could show some code that you tried - someone can then explain what is the problem, so you can learn more than when someone just posts code that works. Anyway, you'll need to reference some assemblies with the System.Xml.Linq and open the namespace first. In F# interactive, you can write it like this (in F# project, just use Add Reference dialog):

#r "System.Core.dll"
#r "System.Xml.Linq.dll"
open System.Xml.Linq

When using XLinq in F#, you need a simple utility function for converting strings to XName object (which represents an element/attribute name). There is an implicit conversion in C#, but this sadly doesn't work in F#.

let xn s = XName.Get(s)

Then you can load your XML document using the XDocument class and use Element method to get a single "parent" element. Then you can call Elements to get all nested "property" elements:

let xd = XDocument.Load("file.xml")
let props = xd.Element(xn "parent").Elements(xn "property")

Now you can search the elements to find the one element with the specified attribute value. For example using Seq.tryFind (which also allows you to handle the case when the element is not found):

let nameOpt = props |> Seq.tryFind (fun xe -> 
  xe.Attribute(xn "name").Value = "firstName")

Now, the value nameOpt is of type option<XElement> so you can pattern match on it to see if the element was found (e.g. Some(el)) or if it wasn't found (None).

EDIT: Another way to write this is to use sequence expressions and then just take the first element (this doesn't handle the case when element is not found):

let nameEl = 
  seq { for el in xd.Element(xn "parent").Elements(xn "property") do
          if xe.Attribute(xn "name").Value = "firstName" then yield xe }
  |> Seq.head
Tomas Petricek
+4  A: 

Also, you can mix kvb's solution with (?) operator:

let (?) (fname: string) (nodeName: string) : string =
  let xpath = XPathDocument(fname).CreateNavigator()
  let node = xpath.SelectSingleNode(@"parent/property[@name='"^nodeName^"']")
  node.Value;;

val ( ? ) : string -> string -> string

> "i:/a.xml"?firstName;;
val it : string = "Jane"
ssp
A: 

I Like this approach better

#r "System.Core.dll"
#r "System.Xml.Linq.dll"
open System.Xml.Linq

let xn s = XName.Get(s)

let xd = XDocument.Load("file.xml")
let fnp = xd.Descendants(xn "property")
        .Where(fun (p : XElement) -> p.Attribute(xn "name").Value = "firstName")
        .Single()
Antwan W. A-Dubb