Sorting in xslt does not work - sorting

I am trying to sort the xml based on the field value person_id_external.
The code which I am using is:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="A/B">
<xsl:copy>
<xsl:apply-templates>
<xsl:sort select="C/person_id_external" order="ascending" />
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The payload is:
<A>
<B>
<C>
<logon_user_name>10027</logon_user_name>
<person_id>1100111</person_id>
<person_id_external>10027</person_id_external>
</C>
</B>
<B>
<C>
<logon_user_name>428122</logon_user_name>
<person_id>11141</person_id>
<person_id_external>111358</person_id_external>
</C>
</B>
<B>
<C>
<logon_user_name>428122</logon_user_name>
<person_id>100441</person_id>
<person_id_external>10636</person_id_external>
</C>
</B>
</A>
The result provides a copy of the input but does not sort.
Expected result is :
<A>
<B>
<C>
<logon_user_name>10027</logon_user_name>
<person_id>1100111</person_id>
<person_id_external>10027</person_id_external>
</C>
</B>
<B>
<C>
<logon_user_name>428122</logon_user_name>
<person_id>11141</person_id>
<person_id_external>10636</person_id_external>
</C>
</B>
<B>
<C>
<logon_user_name>428122</logon_user_name>
<person_id>100441</person_id>
<person_id_external>111358</person_id_external>
</C>
</B>
</A>
Cheers,
Vikcy

-- edited in response to your edit --
In your example, each B node has only one C node. Therefore, you must sort the B nodes in order to get the expected result - and you must do so from the context of their parent A:
<xsl:template match="A">
<xsl:copy>
<xsl:apply-templates>
<xsl:sort select="C/person_id_external" order="ascending"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
Note that the default sort data-type is text (i.e. alphabetical).

The below code works:
<xsl:template match="A">
<xsl:copy>
<xsl:apply-templates>
<xsl:sort select="C/person_id_external" data-type="number" order="ascending"/>
</xsl:apply-templates>
</xsl:copy>
The incoming payload had the type as number.
Cheers,
Vikas Singh

Related

adding attribute to an existing node

i need to add an attribute initial-page-number to a tag fo:sequence
tha tag is
<fo:page-sequence master-reference="alternating" initial-page-number="1"><fo:page-sequence>
..
...
</fo:page-sequence>
become
<fo:page-sequence master-reference="alternating" initial-page-number="1">
..
</fo:page-sequence>
but with the xslt i obtain two fo:page:
<fo:page-sequence master-reference="alternating" initial-page-number="1"><fo:page-sequence>
</fo:page-sequence></fo:page-sequence>
How can i replace old fo:page-sequence with new one?
This is my xsl stylesheet:
<xsl:stylesheet>
<xsl:template match="ss:split/fo:page-sequence">
<xsl:choose>
<xsl:when test="#master-reference['alternating']">
<xsl:element name="fo:page-sequence">
<xsl:for-each select="#*">
<xsl:attribute name="{name()}"><xsl:value-of select="."/></xsl:attribute>
</xsl:for-each>
<xsl:attribute name="initial-page-number">
<xsl:value-of select="1"/>
</xsl:attribute>
<xsl:copy>
<xsl:apply-templates select="child::*"/>
</xsl:copy>
</xsl:element>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template match='comment()'>
<xsl:comment><xsl:value-of select="."/></xsl:comment>
</xsl:template>
<xsl:template match="#*|*">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Your stylesheet changes every fo:page-sequence because the predicate ['alternating'] is always true.
You can check for the master-reference value in the match pattern, plus you can just copy the existing attributes, and you can copy the contents of the fo:page-sequence since it won't contain another fo:page-sequence:
<xsl:template
match="ss:split/fo:page-sequence[#master-reference = 'alternating']">
<xsl:copy>
<xsl:copy-of select="#*" />
<xsl:attribute name="initial-page-number">1</xsl:attribute>
<xsl:copy-of select="node()" />
</xsl:copy>
</xsl:template>
Your stylesheet creates an fo:page-sequence using <xsl:element name="fo:page-sequence">, and another one with <xsl:copy> (as the matching element is an fo:page-sequence).
Just remove the xsl:copy (but leave <xsl:apply-templates select="child::*"/>, as you want to process the children of the current node!) and you should get what you need.

differentiate scenarios using XPATH

I've the below cases.
**Case1:**
<para>1A/66</para>
<para>1A/34S/4</para>
<para>1/66</para>
**Case 2:**
<para>A/66</para>
<para>A1/1</para>
Here, the explaination is if the para starts with a letter (here it is A, it can be any alphabet), it should print case 2 else it should print case 1.
please let me know how can i do this.
Here is a DEmo
With XSLT 2.0 you have regular expression support so using e.g.
<xsl:template match="para[matches(., '^[a-zA-Z]')]">case 2</xsl:template>
<xsl:template match="para[matches(., '^[^a-zA-Z]')]">case 1</xsl:template>
Of course you can use a different regular expression to match on non-ASCII letters as well if "can be any alphabet" is meant to indicate other letters.
Here you are :
<xsl:template match="/">
<hmtl>
<head>
<title>New Version!</title>
</head>
<body>
<xsl:call-template name="print-param" />
<xsl:apply-templates select="child::*"/>
</body>
</hmtl>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match = "body">
<!-- Match cases -->
</xsl:template>
<xsl:template name = "print-param">
<!-- Match cases -->
**Case1:**
<xsl:for-each select=".//para[matches(., '^[A-Za-z]\.*')]">
<para><xsl:value-of select="." /></para>
</xsl:for-each>
**Case 2:**
<xsl:for-each select=".//para[not(matches(., '^[A-Za-z]\.*'))]">
<para><xsl:value-of select="." /></para>
</xsl:for-each>
</xsl:template>
</xsl:transform>
Check this demo

How to match the case and apply templates

I've the below XML line of code.
<title><page>651</page>CHAPTER 13 This is <content-style font-style="italic">This goes in content-style</content-style> The title</title>
Here i'm trying to do the below.
Get the number after CHAPTER and concat it with Chapter, i'm able to do it with the below code.
<xsl:value-of select="concat('Chapter ', substring-before(substring-after(child::title,' '),' ')"/>
ignore the page in the title, and i use the below template match and able to do it.
<xsl:template match="title/page"/>
If there is just plain data, i'm using the below to get it.
<xsl:value of select ="substring-after(substring-after(./title,' '),' ')">
But the problem came in the above type, here i need to apply templates on substring-after(substring-after(.,' '),' ') and unfortunately this is not working.
I have the below XSLT
<xsl:template match ="title">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="content-style">
<xsl:variable name="fontStyle">
<xsl:value-of select="concat('font-style-',#font-style)"/>
</xsl:variable>
<span class="{$fontStyle}">
<xsl:value-of select="."/>
<xsl:apply-templates select="para"/>
</span>
</xsl:template>
expected O/P
<div class="chapter-title">
<span class="chapter-num">Chapter 13</span><br /><br /> This is <span class="font-style-italic">This goes in content-style</span> the title</span></div>
Can you please let me know how can i do this.
Thanks
Your expected output has an unmatched span end tag.
Ignoring that, I would do something like:
<xsl:template match ="title">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="content-style">
<span class="font-style-{#font-style}">
<xsl:value-of select="."/>
</span>
</xsl:template>
<xsl:template match="title/text()">
<xsl:analyze-string select="." regex="Chapter [0-9]+">
<xsl:matching-substring>
<span class="chapter-num">
<xsl:value-of select="."/>
</span><br/>>br/>
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="."/>
</xsl:-nonmatching-substring>
</xsl:analyze-string>
</xsl:template>

How to order self-referencing xml

I have a list of order lines with each one product on them. The products in may form a self-referencing hierarchy. I need to order the lines in such a way that all products that have no parent or whose parent is missing from the order are at the top, followed by their children. No child may be above its parent in the end result.
So how can i order the following xml:
<order>
<line><product code="3" parent="1"/></line>
<line><product code="2" parent="1"/></line>
<line><product code="6" parent="X"/></line>
<line><product code="1" /></line>
<line><product code="4" parent="2"/></line>
</order>
Into this:
<order>
<line><product code="6" parent="X"/></line>
<line><product code="1" /></line>
<line><product code="2" parent="1"/></line>
<line><product code="3" parent="1"/></line>
<line><product code="4" parent="2"/></line>
</order>
Note that the order within a specific level is not important, as long as the child node follows at some point after it's parent.
I have a solution which works for hierarchies that do not exceed a predefined depth:
<order>
<xsl:variable name="level-0"
select="/order/line[ not(product/#parent=../line/product/#code) ]"/>
<xsl:for-each select="$level-0">
<xsl:copy-of select="."/>
</xsl:for-each>
<xsl:variable name="level-1"
select="/order/line[ product/#parent=$level-0/product/#code ]"/>
<xsl:for-each select="$level-1">
<xsl:copy-of select="."/>
</xsl:for-each>
<xsl:variable name="level-2"
select="/order/line[ product/#parent=$level-1/product/#code ]"/>
<xsl:for-each select="$level-2">
<xsl:copy-of select="."/>
</xsl:for-each>
</order>
The above sample xslt will work for hierarchies with a maximum depth of 3 levels and is easily extended to more, but how can i generalize this and have the xslt sort arbitrary levels of depth correctly?
To start with, you could define a couple of keys to help you look up the line elements by either their code or parent attribute
<xsl:key name="products-by-parent" match="line" use="product/#parent" />
<xsl:key name="products-by-code" match="line" use="product/#code" />
You would start off by selecting the line elements with no parent, using a key to do this check:
<xsl:apply-templates select="line[not(key('products-by-code', product/#parent))]"/>
Then, within the template that matches the line element, you would just copy the element, and then select its "children" like so, using the other key
<xsl:apply-templates select="key('products-by-parent', product/#code)"/>
This would be a recursive call, so it would recursively look for its children until no more are found.
Try this XSLT
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="products-by-parent" match="line" use="product/#parent"/>
<xsl:key name="products-by-code" match="line" use="product/#code"/>
<xsl:template match="order">
<xsl:copy>
<xsl:apply-templates select="line[not(key('products-by-code', product/#parent))]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="line">
<xsl:call-template name="identity"/>
<xsl:apply-templates select="key('products-by-parent', product/#code)"/>
</xsl:template>
<xsl:template match="#*|node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Do note the use of the XSLT identity transform to copy the existing nodes in the XML.
Very interesting problem. I would do this in two passes: first, nest the elements according to their hierarchy. Then output the elements, sorted by the count of their ancestors.
XSLT 1.0 (+ EXSLT node-set() function):
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="product-by-code" match="product" use="#code" />
<!-- first pass -->
<xsl:variable name="nested">
<xsl:apply-templates select="/order/line/product[not(key('product-by-code', #parent))]" mode="nest"/>
</xsl:variable>
<xsl:template match="product" mode="nest">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates select="../../line/product[#parent=current()/#code]" mode="nest"/>
</xsl:copy>
</xsl:template>
<!-- output -->
<xsl:template match="/order">
<xsl:copy>
<xsl:for-each select="exsl:node-set($nested)//product">
<xsl:sort select="count(ancestor::*)" data-type="number" order="ascending"/>
<line><product><xsl:copy-of select="#*"/></product></line>
</xsl:for-each>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
When applied to your input, the result is:
<?xml version="1.0" encoding="UTF-8"?>
<order>
<line>
<product code="6" parent="X"/>
</line>
<line>
<product code="1"/>
</line>
<line>
<product code="3" parent="1"/>
</line>
<line>
<product code="2" parent="1"/>
</line>
<line>
<product code="4" parent="2"/>
</line>
</order>
This still leaves the issue of the existing/missing parent X - I will try to address that later.

XSLT: sort and limit nested nodes

XML:
<node>
<node date="01-01-2002">Node</node>
<node date="01-01-2005">Node</node>
<node date="01-01-2001">Node</node>
<node date="01-01-2003">Node</node>
<node date="01-01-2006">Node</node>
<node>
<node date="01-01-2000">Node</node>
<node date="01-01-2007">Node</node>
</node>
<node date="01-01-2004">Node</node>
</node>
Problem:
I need to sort by date AND take a limited number of sorted nodes. Need to be able to traverse any number of levels.
Required result:
<p>01-01-2000</p>
<p>01-01-2001</p>
<p>01-01-2002</p>
<p>01-01-2003</p>
<p>01-01-2004</p>
Assumptions:
For sorting by date I use c# extension method that returns time stamp:
<xsl:sort select="cs:formatDate(#date)" order="ascending" data-type="number" />
Limit to 5 oldest nodes.
Order: ascending
XSLT 1.0
EDIT:
As requested this is where i got so far:
I can do sorting and limiting for not nested nodes:
<xsl:template match="node">
<xsl:apply-templates select="node">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
<xsl:with-param name="limit" select="5"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node[#date]">
<xsl:param name="limit" />
<xsl:if test="position() < $limit+1">
<h5><xsl:value-of select="#date"/></h5>
</xsl:if>
</xsl:template>
Or when I try to apply for nested as below, I get nested nodes sorted in isolation, and I cannot limit them in same way anymore:
<xsl:template match="*">
<xsl:apply-templates select="node[#date]">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
</xsl:apply-templates>
<xsl:apply-templates select="node[not(#date)]">
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node[#date]">
<h5><xsl:value-of select="#date"/></h5>
</xsl:template>
<xsl:template match="node[not(#date)]">
<xsl:apply-templates select="node[#date]">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
</xsl:apply-templates>
<xsl:apply-templates select="node[not(#date)]">
</xsl:apply-templates>
</xsl:template>
EDIT:
I thought it is obvious, but probably not: I need sort to be applied before the limit. E.g: "get oldest five" and NOT:"get first five nodes from xml and then sort them"
<xsl:template match="/">
<xsl:apply-templates select="//node[#date]">
<xsl:sort select="concat(substring-after(substring-after(#date,'-'),'-'),substring-before(substring-after(#date,'-'),'-'),substring-before(#date,'-'))" order="ascending" data-type="number" />
<xsl:with-param name="start" select="1"/>
<xsl:with-param name="end" select="5"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node">
<xsl:param name="start" />
<xsl:param name="end" />
<xsl:if test="position() >= $start and position() <= $end">
<p>
<xsl:value-of select="#date"/>
</p>
</xsl:if>
</xsl:template>

Resources