Phyllis
Phyllis

Reputation: 64

xpath get position of an ancestor node

I have an XML structure like the following:

<inventory>
    <location>
        <category>
            <children>
                <book>
                    <title>Harry Potter</title>
                    <price>$28.50</price>
                </book>
                <cd>
                    <title>Frozen</title>
                    <price>12.8</price>
                </cd>
            </children>
        </category>
        <category>
            <adult>
                <book>
                    <title>Da vinci code</title>
                    <price>32.50</price>
                </book>
                <cd>
                    <title>Da vinci code</title>
                    <price>13.80</price>
                </cd>
            </adult>
        </category>
    </location>
    <location>
        <category>
            <cooking>
                <book>
                    <title>everyday Italian</title>
                    <price>30.50</price>
                </book>
            </cooking>
        </category>
    </location>
</inventory>

What I want to print is:

Location category# category  title       price
1        1         children  Harry       28.50
2        1         cooking   everyday... 30.50
1        2         cd        Da vinci code 13.8

If I'm currently on each of the <title> elements, how do I get the position of <location> and <category>?

What I've tried:

count(ancestor::location/preceding-silbing::location) + 1
count(ancestor::category/preceding-silbing::category) + 1

But none of them work.

Upvotes: 0

Views: 2405

Answers (2)

Daniel Haley
Daniel Haley

Reputation: 52848

Since you're using XSLT 2.0 try using the more powerful xsl:number instead of count().

Location:

<xsl:number select="ancestor::location"/>

Category:

<xsl:number select="ancestor::category"/>

Here's a more detailed example. (Note, this is just for illustration purposes; it's probably not the most efficient example.)

XML Input

<inventory>
    <location>
        <category>
            <children>
                <book>
                    <title>Harry Potter</title>
                    <price>$28.50</price>
                </book>
                <cd>
                    <title>Frozen</title>
                    <price>12.8</price>
                </cd>
            </children>
        </category>
        <category>
            <adult>
                <book>
                    <title>Da vinci code</title>
                    <price>32.50</price>
                </book>
                <cd>
                    <title>Da vinci code</title>
                    <price>13.80</price>
                </cd>
            </adult>
        </category>
    </location>
    <location>
        <category>
            <cooking>
                <book>
                    <title>everyday Italian</title>
                    <price>30.50</price>
                </book>
            </cooking>
        </category>
    </location>
</inventory>

XSLT 2.0

<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    xmlns:local="local">
    <xsl:output method="text"/>
    <xsl:strip-space elements="*"/>

    <xsl:variable name="column-widths">
        <cols>
            <col name="location" width="10"/>
            <col name="categorynbr" width="11"/>
            <col name="category" width="{max((string-length('category'),//category/*/string-length(name())))+2}"/>
            <col name="title" width="{max((string-length('title'),//category/*/book/title/string-length(normalize-space())))+2}"/>
            <col name="price" width="{max((string-length('price'),//category/*/book/price/string-length(normalize-space())))+2}"/>
        </cols>
    </xsl:variable>

    <xsl:function name="local:padValue">
        <xsl:param name="colname"/>
        <xsl:param name="value"/>
        <xsl:variable name="padding">
            <xsl:for-each select="1 to xs:integer($column-widths/*/col[@name=$colname]/@width) - string-length($value)">
                <xsl:text> </xsl:text>
            </xsl:for-each>                    
        </xsl:variable>
        <xsl:value-of select="concat($value,$padding)"/>
    </xsl:function>

    <xsl:template match="/*">
        <xsl:value-of select="concat(local:padValue('location','Location'),
            local:padValue('categorynbr','category#'),
            local:padValue('category','category'),
            local:padValue('title','title'),
            local:padValue('price','price'),'&#xA;')"/>
        <xsl:apply-templates select="*/*/*/book">
            <xsl:sort>
                <xsl:number select="ancestor::category"/>
            </xsl:sort>
        </xsl:apply-templates>
    </xsl:template>

    <xsl:template match="book">
        <xsl:variable name="loc">
            <xsl:number select="ancestor::location"/>            
        </xsl:variable>
        <xsl:variable name="cat">
            <xsl:number select="ancestor::category"/>            
        </xsl:variable>
        <xsl:value-of select="concat(local:padValue('location',$loc),
            local:padValue('categorynbr',$cat),
            local:padValue('category',../local-name()),
            local:padValue('title',normalize-space(title)),
            local:padValue('price',normalize-space(price)),'&#xA;')"/>
    </xsl:template>

</xsl:stylesheet>

Output

Location  category#  category  title             price   
1         1          children  Harry Potter      $28.50  
2         1          cooking   everyday Italian  30.50   
1         2          adult     Da vinci code     32.50   

Upvotes: 1

Burkart
Burkart

Reputation: 464

My assumption is that this is what you're after:

Your context node is either of the title elements in the input document. Now you want to know

  • The (one-based) index of the location element that your context node is contained in, within its parent inventory element

  • The (one-based) index of the category element that your context node is contained in, within its parent category element

For the provided XML input, this can be achieved with the XPath expressions

count(ancestor::location/preceding-sibling::location) + 1

and

count(ancestor::category/preceding-sibling::category) + 1

Make sure that axis and node-test are separated by two colons (corrected by edit). Also make sure preceding-sibling is spelled correctly.

You can add the predicate [1] after the location steps ancestor::location and ancestor::category to explicitly state that you're after the first ancestor in reverse document order (i.e., the closest ancestor). This would be necessary if the XML contained nested location or category elements.

Upvotes: 0

Related Questions