Stan C
Stan C

Reputation: 291

XPath/XQuery - Selecting a node while excluding some elements

I have an XML file which I need to modify. First I need to do a group by and then exclude a couple of nodes.

<cars>
    <car>
        <make>Toyota</make>
        <model>Camry</model>
        <color>White</color>
        <price>123</price>
    </car>
    <car>
        <make>Honda</make>
        <model>Accord</model>
        <color>White</color>
        <price>423</price>
    </car>
</cars>

Here is my code that does the transformation:

<root>
    {
    for $color in distinct-values(doc('cars.xml')//cars/car/color)
    let $car := doc('cars.xml')//cars/car
    return  <appearance color="{$color}">
            { $car[color eq $color ] }
            </appearance>
    }
</root>

I get:

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <appearance color="White">
        <car>
            <make>Toyota</make>
            <model>Camry</model>
            <color>White</color>
            <price>123</price>
        </car>
        <car>
            <make>Honda</make>
            <model>Accord</model>
            <color>White</color>
            <price>423</price>
        </car>
    </appearance>
</root>

This does 95% of what I need except for one problem. I need to exclude nodes "color" and "price" from the final XML while preserving the grouping.

I tried to do the following in my return statement: { $car[color eq $color ]/*[not(self::price)][not(self::color)] }

sort of works but it completely eliminates the "car" element!! Any advice?

Upvotes: 4

Views: 4156

Answers (3)

Joe Wicentowski
Joe Wicentowski

Reputation: 5294

I like black sowl's solution, but I'd suggest the following slightly cleaner version of the code. Cleaner in these ways:

  1. No need to use //cars since cars is the root element
  2. The 1st return statement is unneeded
  3. No need to repeatedly traverse cars/car
  4. Use singular & plural in variable names to indicate single item vs. sequence

    <root>
        {
        let $cars := doc('cars.xml')//car
        let $colors := distinct-values($cars/color)
        for $color in $colors
        return
            <appearance color="{$color}">
                {
                for $car in $cars[color = $color]
                return 
                    <car>
                        {$car/*[not(self::color or self::price)]}
                    </car>  
                }
            </appearance>
        }
    </root>
    

Upvotes: 1

Michael Kay
Michael Kay

Reputation: 163595

Note that in XPath, you can't create new nodes. You can either return nodes that already exist in the input document unchanged, or you can compute values such as counts and totals, but if you want a new or modified node tree, you need XSLT or XQuery.

If your output is going to be similar to your input with small changes, then the XSLT solution is often a lot simpler than the XQuery solution.

Upvotes: 0

black sowl
black sowl

Reputation: 195

Try this:

<root>
{
    let $cars-doc := doc('cars.xml')
    let $color := distinct-values($cars-doc//cars/car/color)
    return
        for $carcolor in $color
        return
            <appearance color="{$carcolor}">
            {
                for $car in $cars-doc//cars/car
                where $car/color = $carcolor
                return 
                    <car>
                    {$car/*[not(self::color or self::price)]}
                    </car>  
            }
            </appearance>
}
</root>  

Hope this helps.

Upvotes: 7

Related Questions