I want to sort all the <text> elements by the value of the attribute top.
However an element should only be sorted if its previous sibling has a value of top that exceeds its own by 2 or more units.
For example, the following elements
<text top="100">text 1</text>
<text top="99">text 2</text>
<text top="100">text 3</text>
<text top="99">text 4</text>
<text top="35">text 5</text>
<text top="40">text 6</text>
should be transformed to:
<text top="35">text 5</text>
<text top="40">text 6</text>
<text top="100">text 1</text>
<text top="99">text 2</text>
<text top="100">text 3</text>
<text top="99">text 4</text>
So that the group:
<text top="100">text 1</text>
<text top="99">text 2</text>
<text top="100">text 3</text>
<text top="99">text 4</text>
remains as is after sorting.
I only use XSLT from time to time and only know the usual sorting approach:
<xsl:for-each select="text">
<xsl:sort select="#top" />
<xsl:copy>
<xsl:copy-of select="./node()|./#*" />
</xsl:copy>
</xsl:for-each>
But the result I want to achieve would require some kind of bubble sort.
Not sure whether it's doable with pure XSLT.
I have an XSLT 2.0 processor.
I wonder whether in XSLT 2/3 it can just be done with an adequate group-ending-with pattern:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="3.0">
<xsl:param name="limit" as="xs:integer" select="1"/>
<xsl:output indent="yes"/>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="root">
<xsl:for-each-group select="text" group-ending-with="text[abs(xs:decimal(following-sibling::text[1]/#top) - xs:decimal(#top)) > $limit]">
<xsl:sort select="min(current-group()/#top/xs:decimal(.))"/>
<xsl:sequence select="current-group()"/>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
Based on the much simplified XQuery code
for tumbling window $group in root/text
start when true()
end $e next $ne when abs(xs:decimal($ne/#top) - xs:decimal($e/#top)) > 1
order by min($group/#top/xs:decimal(.))
return
$group
As I undertand the requeriments are grouping and then sorting. Do note that it is assumed that groups which their elements have less than 2 units of increment are sorted among the others groups taking only the minimum into account (meaning that groups don't overlap).
This stylesheet:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0">
<xsl:template match="*[text]">
<xsl:for-each-group
select="text"
group-adjacent="boolean(
(preceding-sibling::text[1]
|following-sibling::text[1])
[abs(#top - current()/#top) < 2])">
<xsl:sort select="min(#top)"/>
<xsl:choose>
<xsl:when test="current-grouping-key()">
<xsl:copy-of select="current-group()"/>
</xsl:when>
<xsl:otherwise>
<xsl:perform-sort select="current-group()">
<xsl:sort select="#top" data-type="number"/>
</xsl:perform-sort>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
Output:
<text top="35">text 5</text>
<text top="40">text 6</text>
<text top="100">text 1</text>
<text top="99">text 2</text>
<text top="100">text 3</text>
<text top="99">text 4</text>
Test it in here
EDIT: not assuming only increasing sequence with abs() function.
I'm trying to create a JSON from a XML which has messages and each message has its date/time.
Below is the XML
<message>
<messageText heading="Temporary Maintenance Message 1">test message1</messageText>
<displayScheduleContainer>
<startDate>22/05/2019</startDate>
<startTimeHrs>12</startTimeHrs>
<startTimeMins>45</startTimeMins>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
<message>
<messageText heading="Temporary Maintenance Message 1">test message2</messageText>
<displayScheduleContainer>
<startDate>22/06/2019</startDate>
<startTimeHrs>12</startTimeHrs>
<startTimeMins>45</startTimeMins>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
The logic inside XSLT reads the date and time to activate the message
<xsl:for-each select="xalan:nodeset($messageData)/activeMessage/message">
<xsl:variable name="variableN">
<xsl:call-template name="jsonMsg" />
</xsl:variable>
<xsl:choose>
<xsl:when test="$variableN = 'true'">
<xsl:copy-of select="messageText/text()" />
<xsl:if test="position() < last()">,</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:for-each>
<xsl:template name="jsonMsg">
<xsl:choose>
<xsl:when test="displayScheduleContainer/noEndDate = 'true'">
<xsl:variable name="messageInDateTime">
<xsl:call-template name="noEndDateTemplate">
<xsl:with-param name="startDateTime"
select="concat(displayScheduleContainer/startDate, ' ', displayScheduleContainer/startTimeHrs, ':', displayScheduleContainer/startTimeMins)" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$messageInDateTime" />
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="noEndDateTemplate">
<xsl:param name="startDateTime" />
<xsl:variable name="sdf"
select="java:text.SimpleDateFormat.new('dd/MM/yyyy hh:mm')" />
<xsl:variable name="currentDateTime" select="java:util.Date.new()" />
<xsl:choose>
<xsl:when
test="java:compareTo(java:parse($sdf, $startDateTime), $currentDateTime) < 0">
<xsl:text>true</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>false</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
The problem i'm facing here is if the last value is false, I end up getting the comma at the end. As i'm checking for the last position and adding the comma. Due to this the whole JSON is broken. In this case it adds the comma because i'm displaying the text only if it is true.
"message": ["test message1", ]
I'm using XSLT 1.0
Instead of xsl:choose, append a predicate to your select expression. Here's a simplified example:
XML
<messages>
<message>
<messageText>test message1</messageText>
<displayScheduleContainer>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
<message>
<messageText>test message2</messageText>
<displayScheduleContainer>
<noEndDate>true</noEndDate>
</displayScheduleContainer>
</message>
<message>
<messageText>test message3</messageText>
<displayScheduleContainer>
<noEndDate>false</noEndDate>
</displayScheduleContainer>
</message>
</messages>
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/messages">
<xsl:for-each select="message[displayScheduleContainer/noEndDate = 'true']">
<xsl:value-of select="messageText" />
<xsl:if test="position() < last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Result
"test message1,test message2"
Added:
If the test is too complex to fit in a predicate, do the transformation in two passes. Here, again, a simplified example:
XSLT 1.0
<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="text" encoding="UTF-8"/>
<xsl:template match="/messages">
<!-- first pass -->
<xsl:variable name="eligible-messages">
<xsl:for-each select="message">
<xsl:if test="displayScheduleContainer/noEndDate = 'true'">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<!-- output -->
<xsl:for-each select="exsl:node-set($eligible-messages)/message">
<xsl:value-of select="messageText" />
<xsl:if test="position() < last()">,</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Replace the test in:
<xsl:if test="displayScheduleContainer/noEndDate = 'true'">
with the test/s you want to perform.
I need to convert the following content
<QTLS_ITEM>
<ID>123</ID>
<ID1>1345</ID1>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>upgrade</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<ID>123</ID>
<ID1>1345</ID1>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>Plugin</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<ID>123</ID>
<ID1>1345</ID1>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>License</PROD_NAME>
</QTLS_ITEM>
This is a looping element type.<QTLS_ITEM> is a repeating element. For each element I have to concatenate those two fields and get the value as below.
I want to transform it into a single Element like
<Item_description>
102697704257 upgrade
102697704257 Plugin
102697704257 License
<Item_Description>
Which means I need to concatenate both the SERAIL_NUMBER and PROD_NAME.
Can anyone help on this?
<xsl:template name="string-join">
<xsl:param name="nodes"/>
<xsl:param name="delimiter"/>
<xsl:for-each select="$nodes">
<xsl:value-of select="."/>
<xsl:if test="$nodes[position()!=last()-1]">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
I am using this to get the Serial numbers separated by comma. But I want to concatenate and get the result in the above format.
The answer for my case is
<xsl:template name="join">
<xsl:param name="list"/>
<xsl:param name="separator"/>
<xsl:for-each select="db:SERAIL_NUMBER | db:PROD_NAME">($list value)
<xsl:value-of select="."/>
<xsl:if test="position() != last()">
<xsl:value-of select="$separator"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
The input that you show us is not well-formed XML, because it does not have a single root element. Given a well-formed XML input such as:
XML
<root>
<QTLS_ITEM>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>upgrade</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>Plugin</PROD_NAME>
</QTLS_ITEM>
<QTLS_ITEM>
<SERAIL_NUMBER>102697704257</SERAIL_NUMBER>
<PROD_NAME>License</PROD_NAME>
</QTLS_ITEM>
</root>
you can do simply:
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">
<Item_description>
<xsl:for-each select="QTLS_ITEM">
<xsl:value-of select="SERAIL_NUMBER, PROD_NAME" />
<xsl:text>
</xsl:text>
</xsl:for-each>
</Item_description>
</xsl:template>
</xsl:stylesheet>
to get:
Result
<?xml version="1.0" encoding="UTF-8"?>
<Item_description>102697704257 upgrade
102697704257 Plugin
102697704257 License
</Item_description>
This has worked out in my case
<xsl:template name="join">
<xsl:param name="list"/>
<xsl:param name="separator"/>
<xsl:for-each select="db:SERAIL_NUMBER|db:PROD_NAME">($list value)
<xsl:value-of select="."/>
<xsl:if test="position() != last()">
<xsl:value-of select="$separator"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
I have a XML file when I use SSIS XML as Source it is generating multiple outputs. I am using XML task to design XSLT for getting single output.
Following is the XML. I need only single output in SSIS. How to write XSLT for the same?
<?xml version="1.0" encoding="utf-8"?>
<Test>
<iden type="shiftid">100</iden >
<iden type="trailid">25</iden >
<TestHeader>
<SequenceNum>111</SequenceNum>
<TestType>CONTAINER</TestType>
<hub role="origin">
<name />
<hubId>030</hubId>
<hubType>New</hubType>
</hub>
<hub role="Dest">
<name />
<hubId>0757</hubId>
<hubType>hublet</hubType>
</hub>
<TestItem>
<lineNumber>1</lineNumber>
<container>1005</container>
<conditionCode>OK</conditionCode>
<hub role="originator">
<name />
<hubId>TRANS-SHIPMENT</hubId>
<hubType>depot</hubType>
</hub>
</TestItem>
<TestItem>
<lineNumber>2</lineNumber>
<container>1005</container>
<conditionCode>OK</conditionCode>
<hub role="originator">
<name />
<hubId>SHIPMENT</hubId>
<hubType>depot</hubType>
</hub>
</TestItem>
<TestItem>
<lineNumber>1</lineNumber>
<container>1006</container>
<conditionCode>OK</conditionCode>
<hub role="originator">
<name />
<hubId>SHIPMENT</hubId>
<hubType>depot</hubType>
</hub>
</TestItem>
</TestHeader>
<TestContainer type="RAPP" id="1005">
<uom hb="m3" type="board">10.0729</uom>
</TestContainer>
<TestContainer type="RAMP" id="1006">
<uom hb="m3" type="board">10.0729</uom>
</TestContainer>
</Test>
I have struggled and comeup with XSLT as follows
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns: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 name="Iden" match="Test">
<xsl:for-each select="../iden">
<xsl:choose>
<xsl:when test="#type='shiftid'">
<ShipmentID>
<xsl:value-of select="."/>
</ShipmentID>
</xsl:when>
<xsl:when test="#type='trailid'">
<TrailerID>
<xsl:value-of select="."/>
</TrailerID>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:template>
<xsl:template name="Item" match="Test">
<xsl:param name="MatchContainer"/>
<xsl:for-each select="//TestItem">
<xsl:if test="$MatchContainer=container">
<Testitem>
<lineNumber>
<xsl:value-of select="lineNumber"/>
</lineNumber>
<containerTestitemID>
<xsl:value-of select="container"/>
</containerTestitemID>
<ConditionCode>
<xsl:value-of select="conditionCode"/>
</ConditionCode>
<Originator>
<xsl:value-of select="hub[#role='originator']/hubId"/>
</Originator>
<OriginatorType>
<xsl:value-of select="hub[#role='originator']/hubType"/>
</OriginatorType>
</Testitem>
</xsl:if>
</xsl:for-each>
</xsl:template>
<xsl:template name="container" match="Test">
<xsl:for-each select="../TestContainer">
<Testcontainerlist>
<ContainerType>
<xsl:value-of select="#type"/>
</ContainerType>
<ContainerID>
<xsl:value-of select="#id"/>
</ContainerID>
<Volume>
<xsl:value-of select="uom"/>
</Volume>
<VolumeUOM>
<xsl:value-of select="volume/#hb"/>
</VolumeUOM>
<VolumeType>
<xsl:value-of select="volume/#type"/>
</VolumeType>
<xsl:call-template name="Item">
<xsl:with-param name="MatchContainer" select="#id"/>
</xsl:call-template>
</Testcontainerlist>
</xsl:for-each>
</xsl:template>
<xsl:template name="Header" match="Test">
<xsl:for-each select="TestHeader">
<Test>
<xsl:call-template name="Iden">
</xsl:call-template>
<dropSequenceNumber>
<xsl:value-of select="SequenceNum"/>
</dropSequenceNumber>
<TestType>
<xsl:value-of select="TestType"/>
</TestType>
<OriginID>
<xsl:value-of select="hub[#role='origin']/hubId"/>
</OriginID>
<DestinationID>
<xsl:value-of select="hub[#role='Dest']/hubId"/>
</DestinationID>
<OriginType>
<xsl:value-of select="hub[#role='origin']/hubType"/>
</OriginType>
<DestinationType>
<xsl:value-of select="hub[#role='Dest']/hubType"/>
</DestinationType>
<xsl:call-template name="container">
</xsl:call-template>
</Test>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
My desired output should be as follows
<?xml version="1.0" encoding="utf-8"?>
<Test>
<List>
<ShipmentID>100</ShipmentID>
<TrailerID>25</TrailerID>
<dropSequenceNumber>111</dropSequenceNumber>
<TestType>CONTAINER</TestType>
<OriginID>030</OriginID>
<DestinationID>0757</DestinationID>
<OriginType>New</OriginType>
<DestinationType>hublet</DestinationType>
<ContainerType>RAPP</ContainerType>
<ContainerID>1005</ContainerID>
<Volume>10.0729</Volume>
<VolumeUOM>m3</VolumeUOM>
<VolumeType>board</VolumeType>
<lineNumber>1</lineNumber>
<containerTestitemID>1005</containerTestitemID>
<ConditionCode>OK</ConditionCode>
<Originator>TRANS-SHIPMENT</Originator>
<OriginatorType>depot</OriginatorType>
</List>
<List>
<ShipmentID>100</ShipmentID>
<TrailerID>25</TrailerID>
<dropSequenceNumber>111</dropSequenceNumber>
<TestType>CONTAINER</TestType>
<OriginID>030</OriginID>
<DestinationID>0757</DestinationID>
<OriginType>New</OriginType>
<DestinationType>hublet</DestinationType>
<ContainerType>RAPP</ContainerType>
<ContainerID>1005</ContainerID>
<Volume>10.0729</Volume>
<VolumeUOM>m3</VolumeUOM>
<VolumeType>board</VolumeType>
<lineNumber>2</lineNumber>
<containerTestitemID>1005</containerTestitemID>
<ConditionCode>OK</ConditionCode>
<Originator>SHIPMENT</Originator>
<OriginatorType>depot</OriginatorType>
</List>
<List>
<ShipmentID>100</ShipmentID>
<TrailerID>25</TrailerID>
<dropSequenceNumber>111</dropSequenceNumber>
<TestType>CONTAINER</TestType>
<OriginID>030</OriginID>
<DestinationID>0757</DestinationID>
<OriginType>New</OriginType>
<DestinationType>hublet</DestinationType>
<ContainerType>RAMP</ContainerType>
<ContainerID>1006</ContainerID>
<Volume>10.0729</Volume>
<VolumeUOM>m3</VolumeUOM>
<VolumeType>board</VolumeType>
<lineNumber>1</lineNumber>
<containerTestitemID>1006</containerTestitemID>
<ConditionCode>OK</ConditionCode>
<Originator>SHIPMENT</Originator>
<OriginatorType>depot</OriginatorType>
</List>
</Test>
A customer supplied XML contains the delivery address as a comma separated string, I would like to split this string into named nodes using XSLT 1.0.
<?xml version="1.0" encoding="UTF-8"?>
<root>
<text>
Company, Streetaddress 20, 1234 AA, City
</text>
</root>
Output
<?xml version="1.0" encoding="UTF-8"?>
<root>
<text>
<COMPANY>Company</COMPANY>
<ADDRESS>Streetaddress 20</ADDRESS>
<ZIPCODE>1234 AA</ZIPCODE>
<CITY>City</CITY>
</text>
</root>
I tried several recursive templates for XSLT 1.0 which do a fine job splitting but the resulting nodes are identically named.
If possible, how can this be achieved using XSLT 1.0?
Does it have to be a recursive template? How about a straight-forward chain of substring-before and substring-after like this:
<xsl:template match="text">
<xsl:copy>
<COMPANY>
<xsl:value-of select="normalize-space(substring-before(., ','))"/>
</COMPANY>
<xsl:variable name="s1" select="substring-after(., ',')"/>
<ADDRESS>
<xsl:value-of select="normalize-space(substring-before($s1, ','))"/>
</ADDRESS>
<xsl:variable name="s2" select="substring-after($s1, ',')"/>
<ZIPCODE>
<xsl:value-of select="normalize-space(substring-before($s2, ','))"/>
</ZIPCODE>
<CITY>
<xsl:value-of select="normalize-space(substring-after($s2, ','))"/>
</CITY>
</xsl:copy>
</xsl:template>
For the fun of it, here is a generic version using a recursive template.
<xsl:template match="text">
<xsl:copy>
<xsl:call-template name="parse-comma-separated">
<xsl:with-param name="elements" select="'COMPANY,ADDRESS,ZIPCODE,CITY'"/>
<xsl:with-param name="text" select="."/>
</xsl:call-template>
</xsl:copy>
</xsl:template>
<xsl:template name="parse-comma-separated">
<xsl:param name="elements"/>
<xsl:param name="text"/>
<xsl:choose>
<xsl:when test="contains($elements, ',')">
<xsl:element name="{normalize-space(substring-before($elements, ','))}">
<xsl:value-of select="normalize-space(substring-before($text, ','))"/>
</xsl:element>
<xsl:call-template name="parse-comma-separated">
<xsl:with-param name="elements" select="substring-after($elements, ',')"/>
<xsl:with-param name="text" select="substring-after($text, ',')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:element name="{normalize-space($elements)}">
<xsl:value-of select="normalize-space($text)"/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
As the supplied XML only had the address as single node (Streettaddress 20) I added a second variable to split the address string into streetaddress and housenumber.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output encoding="UTF-8" indent="yes" method="xml"/>
<xsl:template match="text">
<root>
<COMPANY>
<xsl:value-of select="normalize-space(substring-before(., ','))"/>
</COMPANY>
<xsl:variable name="s1" select="substring-after(., ',')"/>
<xsl:variable name="address_temp" select="normalize-space(substring-before($s1, ','))"/>
<ADDRESS>
<xsl:value-of select="normalize-space(substring-before($address_temp, ' '))"/>
</ADDRESS>
<HOUSENUMBER>
<xsl:value-of select="normalize-space(substring-after($address_temp, ' '))"/>
</HOUSENUMBER>
<xsl:variable name="s2" select="substring-after($s1, ',')"/>
<ZIPCODE>
<xsl:value-of select="normalize-space(substring-before($s2, ','))"/>
</ZIPCODE>
<CITY>
<xsl:value-of select="normalize-space(substring-after($s2, ','))"/>
</CITY>
</root>
</xsl:template>
</xsl:stylesheet>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<COMPANY>Company</COMPANY>
<ADDRESS>Streetaddress</ADDRESS>
<HOUSENUMBER>20</HOUSENUMBER>
<ZIPCODE>1234 AA</ZIPCODE>
<CITY>City</CITY>
</root>