Proper use of for-each-group in XSLT - xslt

I have the following xml sample:
<items>
<item>
<item_id>1</item_id>
<item_name>item 1</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>2</item_id>
<item_name>item 2</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>3</item_id>
<item_name>item 3</item_name>
<group_id>2</group_id>
<group_name>group 2</group_name>
</item>
</items>
which I need transformed into the following csv format:
1,item 1
2,item 2
3,item 3
1,group 1
2,group 2
In the xml, item_id will always be followed by item_name. The item name will not always be concat('item_',#). It could be an description such as 'toothpaste'. Items will never be repeated in the file but occasionally the group_id->group_name pairing will not always be 1-1. In this case, it is preferred to take the first pairing.
I am accomplishing this using a 'for-each-group' statement but it seems a bit hacky. Are there any downsides to this approach? What are some better ways to accomplish this?
<xsl:template match="/">
<xsl:call-template name="list_format">
<xsl:with-param name="list" select="'item'"/>
</xsl:call-template>
<xsl:call-template name="list_format">
<xsl:with-param name="list" select="'group'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="list_format">
<xsl:param name="list"/>
<xsl:variable name="linefeed" select="'
'"></xsl:variable>
<xsl:variable name="list_id" select="concat($list,'_id')"></xsl:variable>
<xsl:variable name="list_name" select="concat($list,'_name')"></xsl:variable>
<xsl:for-each-group select="/items/item" group-by="*[name()=$list_id]">
<xsl:sort select="*[name()=$list_id]"/>
<xsl:value-of select="*[name()=$list_id]"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="*[name()=$list_name]"/>
<xsl:value-of select="$linefeed"/>
</xsl:for-each-group>
</xsl:template>

hmmm .. not really a downside but it doesn't work in XSLT 1.0 as it doesn't support for-each-group :-/
I don't think you would need XSLT 1.0 solution .. but still am posting my work :) Check this out!
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/items">
<xsl:call-template name="items_grouping">
<xsl:with-param name="list" select="'item'"/>
</xsl:call-template>
<xsl:call-template name="items_grouping">
<xsl:with-param name="list" select="'group'"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="items_grouping">
<xsl:param name="list"/>
<xsl:variable name="linefeed" select="'
'"></xsl:variable>
<xsl:variable name="list_id" select="concat($list,'_id')"></xsl:variable>
<xsl:variable name="list_name" select="concat($list,'_name')"></xsl:variable>
<xsl:for-each select="item">
<xsl:for-each select="*[name()=$list_id and not(.= ../preceding-sibling::item/*[name()=$list_id]/.)]">
<xsl:value-of select="concat(.,',')"/>
<xsl:value-of select="../*[name()=$list_name]"/>
<xsl:value-of select="$linefeed"/>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
Used same methodology of yours .. only change is 'preceding-sibling'
and also for this code, order of child-nodes of <item> doesn't matter

Here is a simple and short XSLT 2.0 (as requested) solution:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:apply-templates/>
<xsl:for-each-group select="/*/*/group_id" group-by=".">
<xsl:value-of separator="," select="../*[starts-with(name(), 'group_')]"/>
<xsl:text>
</xsl:text>
</xsl:for-each-group>
</xsl:template>
<xsl:template match="item">
<xsl:value-of separator="," select="*[starts-with(name(), 'item_')]"/>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the provided XML document:
<items>
<item>
<item_id>1</item_id>
<item_name>item 1</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>2</item_id>
<item_name>item 2</item_name>
<group_id>1</group_id>
<group_name>group 1</group_name>
</item>
<item>
<item_id>3</item_id>
<item_name>item 3</item_name>
<group_id>2</group_id>
<group_name>group 2</group_name>
</item>
</items>
the wanted, correct result is produced:
1,item 1
2,item 2
3,item 3
1,group 1
2,group 2

Related

xslt group by function or append to array

I have collection of items:
<items>
<result>
<item>
<product_name>some description</product_name>
</item>
<item>
<product_name>some similar description</product_name>
</item>
<item>
<product_name>other product size 1</product_name>
</item>
<item>
<product_name>other product size 2</product_name>
</item>
</result>
</items>
I also have some external function strdist:string-distance that compares previous product to current one and if there is some match then it returns true. Based on this returned value I'd like to:
add the current element of for-each to the group of previous element when returned value is true
close previous group, create a new group and add current element of for-each loop to it when returned value is false
I'm little bit struggling with the process of how to create the groups and add there elements in for-loop.
Here is my template
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:strdist="http://example.com/string-distance"
exclude-result-prefixes="strdist">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:template match="/">
<xsl:variable name="items" as="element()*">
<xsl:perform-sort select="items/result/item">
<xsl:sort select="./product_name"/>
</xsl:perform-sort>
</xsl:variable>
<xsl:variable name="groupedItems" as="element()*">
<groups>
<xsl:for-each select="$items">
<xsl:variable name="position" select="position()"/>
<xsl:variable name="currentProductName" select="./product_name/text()"/>
<xsl:choose>
<xsl:when test="$position -1 = 0">
<xsl:element name="group"/>
<xsl:message select="concat($position, ' # ', $currentProductName)"/>
</xsl:when>
<xsl:when test="$position -1 > 0">
<xsl:variable name="previousProductName"
select="$items[position() = $position -1]/product_name/text()"/>
<xsl:message
select="concat($position, ' # ', $previousProductName, ' # ', $currentProductName)"/>
<xsl:message select="strdist:string-distance($previousProductName, $currentProductName)"/>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</groups>
</xsl:variable>
<xsl:message select="$groupedItems" />
</xsl:template>
</xsl:stylesheet>
Finally I'd like to have something like this:
<items>
<result>
<group>
<item>
<product_name>some description</product_name>
</item>
<item>
<product_name>some similar description</product_name>
</item>
</group>
<group>
<item>
<product_name>other product size 1</product_name>
</item>
<item>
<product_name>other product size 2</product_name>
</item>
</group>
</result>
</items>
I'm using xslt 2.0 with saxon-he 10.3.
Here is the example using for-each-group group-starting-with and a sample function:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="#all"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes"/>
<xsl:function name="mf:string-distance" as="xs:boolean">
<xsl:param name="name1" as="xs:string"/>
<xsl:param name="name2" as="xs:string"/>
<xsl:sequence select="tokenize($name1)[1] = tokenize($name2)[1]"/>
</xsl:function>
<xsl:template match="result">
<xsl:copy>
<xsl:for-each-group select="item" group-starting-with="item[not(mf:string-distance(product_name, preceding-sibling::item[1]/product_name))]">
<group>
<xsl:apply-templates select="current-group()"/>
</group>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Seems to give the wanted result at https://xsltfiddle.liberty-development.net/bEJbVrR.

Why doesn't most specialized template match in XSLT

This is my transformation:
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/root">
<output>
<xsl:apply-templates select="outer[#type='foo']/inner"/>
</output>
</xsl:template>
<xsl:template match="outer[#type='foo']/inner[#type='bar1' or #type='bar2' or #type='bar3' or #type='bar4']">
<item>
<xsl:value-of select="text()"/>
</item>
</xsl:template>
<xsl:template match="outer[#type='foo']/inner">
<xsl:message terminate="yes">
<xsl:value-of select="concat('Unexpected type: ', #type)"/>
</xsl:message>
</xsl:template>
</xsl:transform>
This is my input:
<root>
<outer type="foo">
<inner type="bar2">bar2</inner>
</outer>
</root>
When I execute the transformation on the input, Xalan quits with a fatal error, which is caused by the <xsl:message terminate="yes"> in the third <xsl:template>. Why? Shouldn't the second, more specialized <xsl:template> match instead?
Another solution would be to simply have the 2 templates trade positions:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/root">
<output>
<xsl:apply-templates select="outer[#type='foo']/inner"/>
</output>
</xsl:template>
<xsl:template match="outer[#type='foo']/inner">
<xsl:message terminate="yes">
<xsl:value-of select="concat('Unexpected type: ', #type)"/>
</xsl:message>
</xsl:template>
<xsl:template match="outer[#type='foo']/inner[#type='bar1' or #type='bar2' or #type='bar3' or #type='bar4']">
<item>
<xsl:value-of select="text()"/>
</item>
</xsl:template>
</xsl:transform>
Although this is probably not clean, so you should rather leave the order as it was and explicitly set a prio higher than .5 to the more specialized template instead:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/root">
<output>
<xsl:apply-templates select="outer[#type='foo']/inner"/>
</output>
</xsl:template>
<xsl:template match="outer[#type='foo']/inner[#type='bar1' or #type='bar2' or #type='bar3' or #type='bar4']" priority="1">
<item>
<xsl:value-of select="text()"/>
</item>
</xsl:template>
<xsl:template match="outer[#type='foo']/inner">
<xsl:message terminate="yes">
<xsl:value-of select="concat('Unexpected type: ', #type)"/>
</xsl:message>
</xsl:template>
</xsl:transform>
Both of your matching expressions have a priority of .5 according to http://lenzconsulting.com/how-xslt-works/#priority
So your templates are conflicting. How about handling everything in 1 single template?
<xsl:template match="outer[#type='foo']/inner">
<xsl:choose>
<xsl:when test="#type='bar1' or #type='bar2' or #type='bar3' or #type='bar4'">
<item>
<xsl:value-of select="text()"/>
</item>
</xsl:when>
<xsl:otherwise>
<xsl:message terminate="yes">
<xsl:value-of select="concat('Unexpected type: ', #type)"/>
</xsl:message>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

how to sum subdocument properties

There is list
<items>
<item parentid='1'>
<amount>3</amount>
</item>
<item parentid='2'>
<amount>1</amount>
</item>
</items>
and document:
<udata id='1'>
<price>10</price>
</udata>
<udata id='1'>
<price>20</price>
</udata>
How to sum price's all document's?
To sum count I use'd:
<xsl:value-of select="sum(items/item/amount)"/>
I'd use:
<xsl:apply-templates select="udata/items/item" mode='price2' />
<xsl:template mode='price2' match='item'>
<xsl:apply-templates select="document(concat('upage://', page/#parentId))" mode='price'>
<xsl:with-param select='amount' name='count'/>
</xsl:apply-templates>
</xsl:template>
<xsl:template mode='price' match='/'>
<xsl:param name='count'/>
<xsl:value-of select="$count * /udata/page/properties/group[#name='price_prop']/property[#name='price']/value"/>
</xsl:template>
In result i had:
3020
I need 50. How to do this?
Here is a sample assuming XSLT 2.0 (e.g. as possible with Saxon 9 or AltovaXML):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:param name="data-url" select="'test2012050103.xml'"/>
<xsl:variable name="data-doc" select="document($data-url)"/>
<xsl:key name="k1" match="udata" use="#id"/>
<xsl:template match="items">
<xsl:value-of select="sum(for $item in item return $item/amount * key('k1', $item/#parentid, $data-doc)/price)"/>
</xsl:template>
</xsl:stylesheet>
Example documents are
<items>
<item parentid='1'>
<amount>3</amount>
</item>
<item parentid='2'>
<amount>1</amount>
</item>
</items>
and
<root>
<udata id='1'>
<price>10</price>
</udata>
<udata id='2'>
<price>20</price>
</udata>
</root>
Output is 50.
[edit]Here is an XSLT 1.0 stylesheet:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:param name="data-url" select="'test2012050103.xml'"/>
<xsl:variable name="data-doc" select="document($data-url)"/>
<xsl:key name="k1" match="udata" use="#id"/>
<xsl:template match="items">
<xsl:call-template name="sum">
<xsl:with-param name="items" select="item"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="sum">
<xsl:param name="items" select="/.."/>
<xsl:param name="total" select="0"/>
<xsl:choose>
<xsl:when test="not($items)">
<xsl:value-of select="$total"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="price">
<xsl:for-each select="$data-doc">
<xsl:value-of select="$items[1]/amount * key('k1', $items[1]/#parentid)/price"/>
</xsl:for-each>
</xsl:variable>
<xsl:call-template name="sum">
<xsl:with-param name="items" select="$items[position() > 1]"/>
<xsl:with-param name="total" select="$total + $price"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Here is a shorter solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:variable name="vPrices" select=
"document('file:///c:/temp/delete/priceData.xml')/*/*"/>
<xsl:template match="/*">
<xsl:apply-templates select="item[1]"/>
</xsl:template>
<xsl:template match="item" name="sumProducts">
<xsl:param name="pAccum" select="0"/>
<xsl:variable name="vNewAccum" select=
"$pAccum +amount * $vPrices[#id = current()/#parentid]/price"/>
<xsl:if test="not(following-sibling::*)">
<xsl:value-of select="$vNewAccum"/>
</xsl:if>
<xsl:apply-templates select="following-sibling::*">
<xsl:with-param name="pAccum" select="$vNewAccum"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
When applied on the following XML document:
<items>
<item parentid='1'>
<amount>3</amount>
</item>
<item parentid='2'>
<amount>1</amount>
</item>
</items>
and having the file c:\temp\delete\priceData.xml contain:
<root>
<udata id='1'>
<price>10</price>
</udata>
<udata id='2'>
<price>20</price>
</udata>
</root>
then the wanted, correct result is produced:
50

XSLT 1.0 to iterate over several XML elements with comma delimited values

I have an XML document structured as follows
<items>
<item>
<name>item1</name>
<attributes>a,b,c,d</attributes>
</item>
<item>
<name>item2</name>
<attributes>c,d,e</attributes>
</item>
</items>
For each unique attribute value (delimited by commas) I need to list all item names associated with that value like so:
a : item1
b : item1
c : item1, item2
d : item1, item2
e : item2
My initial plan was to use a template to parse the attributes into Attribute nodes, surrounding each with appropriate tags, and then separating out the unique values with an XPATH expression like
Attribute[not(.=following::Attribute)]
but since the result of the template isn't a node-set that ever goes through an XML parser, I can't traverse it. I also tried exslt's node-set() function only to realize it does not allow me to traverse the individual Attribute nodes either.
At this point I'm at a loss for a simple way to do this and would really appreciate any help or ideas on how to proceed. Thanks!
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:ext="http://exslt.org/common">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kAtrByVal" match="attr" use="."/>
<xsl:template match="/">
<xsl:variable name="vrtfPass1">
<groups>
<xsl:apply-templates/>
</groups>
</xsl:variable>
<xsl:variable name="vPass1"
select="ext:node-set($vrtfPass1)"/>
<xsl:apply-templates select="$vPass1/*"/>
</xsl:template>
<xsl:template match="item">
<group name="{name}">
<xsl:apply-templates select="attributes"/>
</group>
</xsl:template>
<xsl:template match="attributes" name="tokenize">
<xsl:param name="pText" select="."/>
<xsl:if test="string-length($pText)">
<xsl:variable name="vText" select=
"concat($pText,',')"/>
<attr>
<xsl:value-of select="substring-before($vText,',')"/>
</attr>
<xsl:call-template name="tokenize">
<xsl:with-param name="pText" select=
"substring-after($pText,',')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match=
"attr[generate-id()
=
generate-id(key('kAtrByVal',.)[1])
]
">
<xsl:value-of select="concat('
',.,': ')"/>
<xsl:for-each select="key('kAtrByVal',.)">
<xsl:value-of select="../#name"/>
<xsl:if test="not(position()=last())">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
when applied on the provided XML document:
<items>
<item>
<name>item1</name>
<attributes>a,b,c,d</attributes>
</item>
<item>
<name>item2</name>
<attributes>c,d,e</attributes>
</item>
</items>
produces the wanted, correct result:
a: item1
b: item1
c: item1, item2
d: item1, item2
e: item2
Explanation:
Pass1: tokenization and end result:
<groups>
<group name="item1">
<attr>a</attr>
<attr>b</attr>
<attr>c</attr>
<attr>d</attr>
</group>
<group name="item2">
<attr>c</attr>
<attr>d</attr>
<attr>e</attr>
</group>
</groups>
.2. Pass2 takes the result of Pass1 (converted to a nodeset using the extension function ext:node-set()) as input, performs Muenchian grouping and produces the final, wanted result.
My first thought is to make two passes. First, tokenize the attributes elements using a (slightly) modified version of #Alejandro's answer to this previous question:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<items>
<xsl:apply-templates/>
</items>
</xsl:template>
<xsl:template match="item">
<item name="{name}">
<xsl:apply-templates select="attributes"/>
</item>
</xsl:template>
<xsl:template match="attributes" name="tokenize">
<xsl:param name="text" select="."/>
<xsl:param name="separator" select="','"/>
<xsl:choose>
<xsl:when test="not(contains($text, $separator))">
<val>
<xsl:value-of select="normalize-space($text)"/>
</val>
</xsl:when>
<xsl:otherwise>
<val>
<xsl:value-of select="normalize-space(
substring-before($text, $separator))"/>
</val>
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="substring-after(
$text, $separator)"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Which produces:
<items>
<item name="item1">
<val>a</val>
<val>b</val>
<val>c</val>
<val>d</val>
</item>
<item name="item2">
<val>c</val>
<val>d</val>
<val>e</val>
</item>
</items>
Then apply the following stylesheet to that output:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:key name="byVal" match="val" use="." />
<xsl:template match="val[generate-id() =
generate-id(key('byVal', .)[1])]">
<xsl:value-of select="." />
<xsl:text> : </xsl:text>
<xsl:apply-templates select="key('byVal', .)" mode="group" />
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="val" mode="group">
<xsl:value-of select="../#name" />
<xsl:if test="position() != last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:template>
<xsl:template match="val" />
</xsl:stylesheet>
Producing:
a : item1
b : item1
c : item1, item2
d : item1, item2
e : item2
Doing this in one stylesheet would require more thought (or an extension function).

Find max node value

I have a xml given below:
<root title="الصفحة الرئيسة">
<item title="الصفحة الرئيسة" itemuri="tcm:8-29-4" ShowInNav="True" type="sg" pageuri="tcm:8-10592-64" sLink="/ara/index.aspx">
<item title="من نحن" itemuri="tcm:8-779-4" ShowInNav="True" type="sg" pageuri="tcm:8-9934-64" navorder="00500" sLink="/ara/about/index.aspx">
</item>
<item title="برامجنا" itemuri="tcm:8-817-4" ShowInNav="True" type="sg" pageuri="tcm:8-10112-64" navorder="00100" sLink="/ara/courses/language-study-abroad.aspx">
</item>
<item title="مدارسنا" itemuri="tcm:8-824-4" ShowInNav="True" type="sg" pageuri="tcm:8-10162-64" navorder="00300" sLink="/ara/schools/english-language.aspx">
</item>
</item>
</root>
Now I want to the value of maximum navorder, so that I can use that value further in "if" condition.
Below is the XSLT 1.0 code of two possible solutions.
A third solution is to use EXSLT.
A fourth solution is to use the maximum template of FXSL.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="vMax1" select=
"*/*/*/#navorder[not(. < ../../*/#navorder)][3]"/>
$vMax1: <xsl:value-of select="$vMax1"/>
<xsl:variable name="vMax2">
<xsl:call-template name="max">
<xsl:with-param name="pSeq"
select="*/*/*/#navorder"/>
</xsl:call-template>
</xsl:variable>
$vMax2: <xsl:value-of select="$vMax2"/>
</xsl:template>
<xsl:template name="max">
<xsl:param name="pSeq"/>
<xsl:variable name="vLen" select="count($pSeq)"/>
<xsl:if test="$vLen > 0">
<xsl:choose>
<xsl:when test="$vLen = 1">
<xsl:value-of select="$pSeq[1]"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vHalf"
select="floor($vLen div 2)"/>
<xsl:variable name="vMax1">
<xsl:call-template name="max">
<xsl:with-param name="pSeq"
select="$pSeq[not(position() > $vHalf)]"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="vMax2">
<xsl:call-template name="max">
<xsl:with-param name="pSeq"
select="$pSeq[position() > $vHalf]"/>
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<xsl:when test="$vMax1 >= $vMax2">
<xsl:value-of select="$vMax1"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$vMax2"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When the above transformatio is applied on the original XML document:
<root title="الصفحة الرئيسة">
<item title="الصفحة الرئيسة" itemuri="tcm:8-29-4"
ShowInNav="True" type="sg"
pageuri="tcm:8-10592-64"
sLink="/ara/index.aspx">
<item title="من نحن" itemuri="tcm:8-779-4"
ShowInNav="True" type="sg"
pageuri="tcm:8-9934-64"
navorder="00500"
sLink="/ara/about/index.aspx"/>
<item title="برامجنا" itemuri="tcm:8-817-4"
ShowInNav="True" type="sg"
pageuri="tcm:8-10112-64"
navorder="00100"
sLink="/ara/courses/language-study-abroad.aspx"/>
<item title="مدارسنا" itemuri="tcm:8-824-4"
ShowInNav="True" type="sg"
pageuri="tcm:8-10162-64"
navorder="00300"
sLink="/ara/schools/english-language.aspx"/>
</item>
</root>
the wanted result is produced:
$vMax1: 00500
$vMax2: 00500
the following is a solution using XPath 1.0:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<xsl:variable name="maximum">
<xsl:value-of select="/root/item//item[not(preceding-sibling::item/#navorder > #navorder or following-sibling::item/#navorder > #navorder)]/#navorder"/>
</xsl:variable>
<max>
<xsl:value-of select="$maximum"/>
</max>
</xsl:template>
</xsl:stylesheet>
The expression
/root/item//item[not(preceding-sibling::item/#navorder > #navorder or following-sibling::item/#navorder > #navorder)]/#navorder
filters all item nodes so that there is no preceding or following node that has a higher navorder value on the same level.
I found this forum post as one potential solution.
Of course, if you have access to XPath 2.0, there's a more direct solution: fn:max(arg, arg, …)
Couple of ways: with xpath 2 you can use the max() function, but if you're in xslt 1 you'll have to use sort in a for-each and then put the item at position 1 in your variable.
Have a look at this. Then use it in your xpath
/root/item[#navorder = math:max(/root/item/#navorder)]
Or something similar :)
NOTE This requires the use of EXSLT.
NOTE 2: No it doesn't. Just copy and paste the code.