How to calculate positions of XML elements using XSL? - xslt

I have a list of items in XML (mind the duplicates):
<root>
<a>hello</a>
<a>bye</a>
<a>5</a>
<a>hello</a>
<a>8</a>
</root>
I want to translate it to this:
<root>
<a>4</a>
<a>3</a>
<a>1</a>
<a>4</a>
<a>2</a>
</root>
Essentially, I'm replacing values with their positions in a sorted list of all values (comparing text to text). I'm trying to do this using <xsl:key>, but can't figure out how exactly.

Try perhaps:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/root">
<xsl:variable name="sorted">
<xsl:perform-sort select="a">
<xsl:sort select="."/>
</xsl:perform-sort>
</xsl:variable>
<root>
<xsl:for-each select="a">
<a>
<xsl:value-of select="index-of(distinct-values($sorted/a), .)"/>
</a>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
Or maybe a bit more elegantly:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/root">
<xsl:variable name="sorted" as="xs:string*">
<xsl:perform-sort select="distinct-values(a)">
<xsl:sort select="."/>
</xsl:perform-sort>
</xsl:variable>
<root>
<xsl:for-each select="a">
<a>
<xsl:value-of select="index-of($sorted, .)"/>
</a>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
Or even just simply:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/root">
<xsl:variable name="sorted">
<xsl:perform-sort select="a">
<xsl:sort select="."/>
</xsl:perform-sort>
</xsl:variable>
<root>
<xsl:for-each select="a">
<a>
<xsl:value-of select="index-of($sorted/a, .)[1]"/>
</a>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>

Related

Create copy of record based on node that occurs multiple times

I have a requirement to create a copy of an XML file based on a field that occurs multiple times.
Input XML: There are two EmpEmployment nodes in the XML. I need to separate them and copy the rest of the nodes so that I have two PerPerson records with one EmpEmployment each.
<PerPerson>
<PerPerson>
<personalInfoNav>
<PerPersonal/>
</personalInfoNav>
<nationalIdNav>
<PerNationalId/>
</nationalIdNav>
<personIdExternal>AA</personIdExternal>
<personEmpTerminationInfoNav>
<PersonEmpTerminationInfo/>
</personEmpTerminationInfoNav>
<phoneNav>
<PerPhone/>
</phoneNav>
<employmentNav>
<EmpEmployment>
<compInfoNav>
<EmpCompensation/>
</compInfoNav>
<jobInfoNav>
<EmpJob/>
</jobInfoNav>
</EmpEmployment>
<EmpEmployment>
<compInfoNav>
<EmpCompensation/>
</compInfoNav>
<jobInfoNav>
<EmpJob/>
</jobInfoNav>
</EmpEmployment>
</employmentNav>
<homeAddressNavDEFLT>
<PerAddressDEFLT/>
</homeAddressNavDEFLT>
</PerPerson>
</PerPerson>
I am trying to do this using XSLT 1.0
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:hci="http://sap.com/it/" exclude-result-prefixes="hci">
<xsl:strip-space elements="*"/>
<xsl:output encoding="utf-8" indent="yes" method="xml"/>
<xsl:template match="/">
<PerPerson>
<xsl:for-each select="PerPerson/PerPerson">
<xsl:variable name="var_person" select="./*[not(name()='EmpEmployment')]"/>
<xsl:for-each select="employmentNav/EmpEmployment">
<xsl:variable name="var_empInfo" select="."/>
<PerPerson>
<xsl:copy-of select="$var_person"/>
<xsl:copy-of select="$var_empInfo"/>
</PerPerson>
</xsl:for-each>
</xsl:for-each>
</PerPerson>
</xsl:template>
</xsl:stylesheet>
Its not working as expected. I am unable to create the desired output below:
<PerPerson>
<PerPerson>
<personalInfoNav>
<PerPersonal/>
</personalInfoNav>
<nationalIdNav>
<PerNationalId/>
</nationalIdNav>
<personIdExternal>AA</personIdExternal>
<personEmpTerminationInfoNav>
<PersonEmpTerminationInfo/>
</personEmpTerminationInfoNav>
<phoneNav>
<PerPhone/>
</phoneNav>
<employmentNav>
<EmpEmployment>
<compInfoNav>
<EmpCompensation/>
</compInfoNav>
<jobInfoNav>
<EmpJob/>
</jobInfoNav>
</EmpEmployment>
</employmentNav>
<homeAddressNavDEFLT>
<PerAddressDEFLT/>
</homeAddressNavDEFLT>
</PerPerson>
<PerPerson>
<personalInfoNav>
<PerPersonal/>
</personalInfoNav>
<nationalIdNav>
<PerNationalId/>
</nationalIdNav>
<personIdExternal>AA</personIdExternal>
<personEmpTerminationInfoNav>
<PersonEmpTerminationInfo/>
</personEmpTerminationInfoNav>
<phoneNav>
<PerPhone/>
</phoneNav>
<employmentNav>
<EmpEmployment>
<compInfoNav>
<EmpCompensation/>
</compInfoNav>
<jobInfoNav>
<EmpJob/>
</jobInfoNav>
</EmpEmployment>
</employmentNav>
<homeAddressNavDEFLT>
<PerAddressDEFLT/>
</homeAddressNavDEFLT>
</PerPerson>
</PerPerson>
You could do:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/PerPerson">
<PerPerson>
<xsl:for-each select="PerPerson/employmentNav/EmpEmployment">
<PerPerson>
<xsl:copy-of select="../preceding-sibling::*"/>
<employmentNav>
<xsl:copy-of select="."/>
</employmentNav>
<xsl:copy-of select="../following-sibling::*"/>
</PerPerson>
</xsl:for-each>
</PerPerson>
</xsl:template>
</xsl:stylesheet>
In XSLT 2.0 you can use tunnel-parameters.
And then just use the template macht engine like below
<?xml version='1.0' encoding='UTF-8'?>
<xsl:stylesheet
version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all">
<xsl:template match="#*|node()" mode="#all">
<xsl:copy>
<xsl:apply-templates select="#*|node()" mode="#current"/>
</xsl:copy>
</xsl:template>
<xsl:template match="PerPerson/PerPerson">
<xsl:variable name="person" select="."/>
<xsl:for-each select="employmentNav/EmpEmployment">
<xsl:apply-templates select="$person" mode="denormalize">
<xsl:with-param name="position" as="xs:integer" select="position()" tunnel="yes"/>
</xsl:apply-templates>
</xsl:for-each>
</xsl:template>
<xsl:template match="employmentNav" mode="denormalize">
<xsl:param name="position" as="xs:integer" tunnel="yes"/>
<xsl:copy>
<xsl:apply-templates select="EmpEmployment[$position]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

XSLT - add prefix for namespace

Here's my input XML:
<?xml version="1.0" encoding="UTF-8"?>
<Sync
xmlns="http://schema.infor.com/InforOAGIS/2" languageCode="en-US" versionID="2.8.0">
<Data>
<ID>0001</ID>
<Text>ABCD</Text>
</Data>
</Sync>
And here's my expected outcome:
<?xml version="1.0" encoding="UTF-8"?>
<ns0:Sync xmlns:ns0="http://schema.infor.com/InforOAGIS/2"
languageCode="en-US"
versionID="2.8.0">
<DataArea xmlns:dns="http://schema.infor.com/InforOAGIS/2" xmlns="">
<ID>0001</ID>
<Text>ABCD</Text>
</DataArea>
</ns0:Sync>
My current XSLT as below (https://xsltfiddle.liberty-development.net/nbiE19N).
There are 2 problems:
I have the extra xmlns="" in DataArea element. I only want to add the dns namespace.
I cannot add the ns0 prefix for my namespace
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/*:Sync">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="/*:Sync/*:Data">
<DataArea>
<xsl:namespace name="dns" select="'http://schema.infor.com/InforOAGIS/2'"/>
<ID>
<xsl:value-of select="/*:Sync/*:Data/*:ID"/>
</ID>
<Text>
<xsl:value-of select="/*:Sync/*:Data/*:Text"/>
</Text>
</DataArea>
</xsl:template>
</xsl:stylesheet>
Any suggestion is appreciated!
Does this return the expected result:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xpath-default-namespace="http://schema.infor.com/InforOAGIS/2">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/Sync">
<ns0:Sync xmlns:ns0="http://schema.infor.com/InforOAGIS/2">
<xsl:copy-of select="#*"/>
<xsl:apply-templates/>
</ns0:Sync>
</xsl:template>
<xsl:template match="Data">
<DataArea xmlns:dns="http://schema.infor.com/InforOAGIS/2">
<xsl:apply-templates/>
</DataArea>
</xsl:template>
<xsl:template match="*">
<xsl:element name="{local-name()}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
P.S. I am not sure why you need the xmlns:dns="http://schema.infor.com/InforOAGIS/2" declaration; it's not being used anywhere.

Assigning a Unique reference and reference it to another collection

I am new to xml and am trying to add in a link between two collections by inserting a Position() number which I have added into the StyleCollection. I now need to insert this as a StyleLink into the PackingLineCollection where the StyleNumber is equal to the StyleNumber in the StyleCollection. Can anybody point me in the right direction?
Here is the Input:
<Order>
<UniqueReferenceNumber>Order1234</UniqueReferenceNumber>
<StyleCollection>
<Style>
<StyleNumber>1234</StyleNumber>
<StyleType>
<Code>abc</Code>
</StyleType>
<LocationNumber></LocationNumber>
</Style>
<Style>
<StyleNumber>567</StyleNumber>
<StyleType>
<Code>xyz</Code>
</StyleType>
<LocationNumber></LocationNumber>
</Style>
</StyleCollection>
<SubOrderCollection>
<SubOrder>
<UniqueReferenceNumber>SubOrder1</UniqueReferenceNumber>
<PackingLineCollection>
<PackingLine>
<StyleNumber>1234</StyleNumber>
</PackingLine>
</PackingLineCollection>
</SubOrder>
<SubOrder>
<UniqueReferenceNumber>SubOrder2</UniqueReferenceNumber>
<PackingLineCollection>
<PackingLine>
<StyleNumber>xyz</StyleNumber>
</PackingLine>
</PackingLineCollection>
</SubOrder>
</SubOrderCollection>
</Order>
Here is the xslt:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" mlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes"/>
<xsl:template match ="/">
<Order>
<Reference>
<xsl:value-of select="Order/UniqueReferenceNumber"/>
</Reference>
<StyleCollection>
<xsl:for-each select="Order/StyleCollection/Style">
<Style>
<StyleNumber>
<xsl:value-of select="StyleNumber"/>
</StyleNumber>
<LocationNumber><xsl:value-of select="position()"/>
</LocationNumber>
<StyleType>
<Code>
<xsl:value-of select="StyleType/Code"/>
</Code>
</StyleType>
</Style>
</xsl:for-each>
</StyleCollection>
<SubOrderCollection>
<xsl:for-each select="Order/SubOrderCollection/SubOrder">
<SubOrder>
<Reference><xsl:value-of select="UniqueReferenceNumber"/>
</Reference>
<PackingLineCollection>
<xsl:for-each select="PackingLineCollection/PackingLine">
<PackingLine>
<StyleNumber>
<xsl:value-of select="StyleNumber" />
</StyleNumber>
<StyleLink><xsl:value-of
Select="Order/StyleCollection/Style/LocationNumber"/></StyleLink>
</PackingLine>
</xsl:for-each>
</PackingLineCollection>
</SubOrder>
</xsl:for-each>
</SubOrderCollection>
</Order>
</xsl:template>
</xsl:stylesheet>
I have implemented it using a key and xsl:number as follows (in XSLT 3 but the key and xsl:number should work in XSLT 1 or 2 as well, you would need to replace the used <xsl:mode on-no-match="shallow-copy"/> with the identity transformation template):
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="3.0">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:key name="style-ref" match="StyleCollection/Style" use="StyleNumber"/>
<xsl:template match="Style/LocationNumber">
<xsl:copy>
<xsl:number from="StyleCollection" level="any"/>
</xsl:copy>
</xsl:template>
<xsl:template match="PackingLine/StyleNumber">
<xsl:next-match/>
<xsl:apply-templates select="key('style-ref', .)" mode="count"/>
</xsl:template>
<xsl:template match="Style" mode="count">
<StyleLink>
<xsl:number level="any" from="StyleCollection"/>
</StyleLink>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/nc4NzQt/1 however does not find a matching Style for the second PackingLine/StyleNumber, not sure whether your example data is meant to have no reference there or whether I misunderstood your requirements.
XSLT 1 is
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="1.0">
<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>
<xsl:template match="#* | node()" name="identity">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:key name="style-ref" match="StyleCollection/Style" use="StyleNumber"/>
<xsl:template match="Style/LocationNumber">
<xsl:copy>
<xsl:number from="StyleCollection" level="any"/>
</xsl:copy>
</xsl:template>
<xsl:template match="PackingLine/StyleNumber">
<xsl:call-template name="identity"/>
<xsl:apply-templates select="key('style-ref', .)" mode="count"/>
</xsl:template>
<xsl:template match="Style" mode="count">
<StyleLink>
<xsl:number level="any" from="StyleCollection"/>
</StyleLink>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/nc4NzQt/2

XSLT manipulate text scattered across various nodes

Input file as follows:
<?xml version="1.0" encoding="UTF-8"?>
<!-- lower UPPER case -->
<document>
<rubbish> rubbish </rubbish>
<span class='lower'>
lower
<span class='upper'> upper </span>
case
</span>
</document>
Wanted output:
lower UPPER case
I know how to get the text included in the outer span with value-of, but this also
includes the string "upper" unchanged which is not what I want. I do not know how
to manipulate the text in the inner span and insert it in the middle of
the other text.
Failed attempt:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="text" indent="no"/>
<xsl:template match="/">
<xsl:for-each select="//span[#class = 'lower']">
<xsl:if test="span/#class = 'upper'">
<xsl:text>do something</xsl:text> <!--TO DO -->
</xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
You need to take a recursive approach here, for example:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="text()[parent::span]">
<xsl:choose>
<xsl:when test="../#class='upper'">
<xsl:value-of select="translate(., 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="." />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
To understand how this works, read up on built-in template rules: http://www.w3.org/TR/xslt/#built-in-rule
The following approach does away with the <choose> and completely pushes the problem down to the match expression:
<?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" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:template match="text()"/>
<xsl:template match="text()[parent::span[#class = 'upper']]">
<xsl:value-of select="translate(., 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')"/>
</xsl:template>
<xsl:template match="text()[parent::span[#class = 'lower']]">
<xsl:value-of select="translate(., 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')"/>
</xsl:template>
</xsl:stylesheet>

xslt extract and sort leaf nodes by name - unexpected result

I wanted to extract leaf nodes and have them sorted.
My XSL gives unexpected results. How can I solve this?
Input
<root>
<b>
<b33 zzz="2" fff="3"></b33>
<b11></b11>
<b22></b22>
</b>
<a>
<a27></a27>
<a65 fff="0" eee="2" zzz="10"></a65>
<a11></a11>
</a>
</root>
Xsl
<?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" omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<root>
<xsl:call-template name="leafnodes"/>
</root>
</xsl:template>
<xsl:template match="*[not(*)]|#*" name="leafnodes">
<xsl:copy>
<xsl:apply-templates select="node()">
<xsl:sort select="name()"/>
</xsl:apply-templates>
<xsl:apply-templates select="#*">
<xsl:sort select="name()"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Output (I would expected it to be sorted, it is not)
<root>
<b33 fff="3" zzz="2" />
<b11 />
<b22 />
<a27 />
<a65 eee="2" fff="0" zzz="10" />
<a11 />
</root>
I would expect the nodes in the order a11, a27, a65, b11, b22, b33.
If I leave out the '[not(*)]', the xsl takes all nodes and sorts them properly.
How can this be solved?
To output all element which have no child sorted by name and the attributes also sorted by name. Try this;
<?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" omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<root>
<xsl:apply-templates select="//*[not(*)]">
<xsl:sort select="name()"/>
</xsl:apply-templates>
</root>
</xsl:template>
<xsl:template match="*|#*">
<xsl:copy>
<xsl:apply-templates select="#*" >
<xsl:sort select="name()"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Which will generate following output:
<root>
<a11/>
<a27/>
<a65 eee="2" fff="0" zzz="10"/>
<b11/>
<b22/>
<b33 fff="3" zzz="2"/>
</root>