XSLT manipulate string, move last word - xslt

I would like to ask if it is possible to swap the last word with the one in front of it, if ends with 27.5 or 29:
<item>
<code>1</code>
<title><![CDATA[Test 30 S 27.5]]></title>
</item>
<item>
<code>2</code>
<title><![CDATA[Test 20 Orange XL 29]]></title>
</item>
<item>
<code>3</code>
<title><![CDATA[Test 30 XS 29]]></title>
</item>
<item>
<code>4</code>
<title><![CDATA[Test 60 27.5 XS]]></title>
</item>
Example output:
<item>
<code>1</code>
<title><![CDATA[Test 30 27.5 S]]></title>
</item>
<item>
<code>2</code>
<title><![CDATA[Test 20 Orange 29 XL]]></title>
</item>
<item>
<code>3</code>
<title><![CDATA[Test 30 29 XS]]></title>
</item>
<item>
<code>4</code>
<title><![CDATA[Test 60 27.5 XS]]></title>
</item>

Use tokenize or analyze-string and then reorder the sequence you get from tokenize or process the result of analyze-string:
<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:output cdata-section-elements="title"/>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="title[ends-with(., '27.5') or ends-with(., '29')]">
<xsl:copy>
<xsl:value-of select="let $words := tokenize(., '\s+')
return (subsequence($words, 1, count($words) - 2), $words[last()], $words[last() - 1])"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/3NSSEuS/1

Related

Usage of the Variable inside the select-value clause to traverse through the XML path

I am trying to fetch the XML value based on a condition, if the variable value matches the value of the XML path mentioned then to obtain the value of its own sub elements.
The Input XML looks like below
<ns1:productSpecificationFullDTO xmlns:ns1="http://www.micros.com/creations/core/domain/dto/v1p0/full" xmlns:ns2="http://www.micros.com/creations/core/domain/dto/v1p0/simple">
<ns1:product>
<ns1:name>Test Component 1</ns1:name>
<ns1:parent>false</ns1:parent>
</ns1:product>
<ns1:product>
<ns1:name>Test Component 2</ns1:name>
<ns1:parent>false</ns1:parent>
</ns1:product>
<ns1:specification>
<ns1:name>Test Component 1</ns1:name>
<ns1:parent>false</ns1:parent>
<ns1:Labeling>
<ns1:mainProductTitle>Test1</ns1:ns1:mainProductTitle>
</ns1:Labeling>
</ns1:specification>
<ns1:specification>
<ns1:name>Test Component 2</ns1:name>
<ns1:parent>false</ns1:parent>
<ns1:Labeling>
<ns1:mainProductTitle>Test2</ns1:ns1:mainProductTitle>
</ns1:Labeling>
</ns1:specification>
My XSLT Definition is below
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ns1="http://www.micros.com/creations/core/domain/dto/v1p0/full" xmlns:ns2="http://www.micros.com/creations/core/domain/dto/v1p0/simple" exclude-result-prefixes="ns1 ns1">
<xsl:template match="/">
<ItemDetails>
<Items>
<!-- Food section start here -->
<xsl:for-each select="/ns1:productSpecificationFullDTO/ns1:product/ns1:parent[text() != 'true']/../ns1:name[text() != 'Parent']/..">
<xsl:variable name="subItem" select="ns1:name/text()"/>
<Item>
<name>
<xsl:value-of select="$subItem"/>
</name>
<LongDescription>
<xsl:value-of select="normalize-space(ns1:productSpecificationFullDTO/ns1:specification/ns1:parent[text() != 'true']/../ns1:name[text() = '''$subItem''']/../ns1:Labeling/ns1:mainProductTitle/text())"/>
</LongDescription>
</Item>
</xsl:for-each>
</Items>
</ItemDetails>
</xsl:template>
The output is as below
<Items>
<Item>
<name>Test Component 1</name>
<LongDescription/>
</Item>
<Item>
<name>Test Component 2</name>
<LongDescription/>
</Item>
Desired Output is
<Items>
<Item>
<name>Test Component 1</name>
<LongDescription>Test1<LongDescription/>
</Item>
<Item>
<name>Test Component 2</name>
<LongDescription>Test2<LongDescription/>
</Item>
As Seen above i'm unable to fetch the value of that variable's sub element.
Please advise, Thanks
I think this solves what you are trying to accomplish, simplifying your XPath expressions and using a key to get to the linked descriptions.
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ns1="http://www.micros.com/creations/core/domain/dto/v1p0/full"
xmlns:ns2="http://www.micros.com/creations/core/domain/dto/v1p0/simple"
exclude-result-prefixes="ns1 ns2">
<xsl:output method="xml" indent="yes"/>
<xsl:key name="keySpec" match="ns1:specification" use="ns1:name"/>
<xsl:template match="/">
<ItemDetails>
<Items>
<!-- Food section start here -->
<xsl:for-each select="/ns1:productSpecificationFullDTO/ns1:product[not(ns1:parent='true') and not(ns1:name='Parent')]">
<Item>
<name>
<xsl:value-of select="ns1:name"/>
</name>
<LongDescription>
<xsl:value-of select="key('keySpec',ns1:name)/ns1:Labeling/ns1:mainProductTitle"/>
</LongDescription>
</Item>
</xsl:for-each>
</Items>
</ItemDetails>
</xsl:template>
</xsl:stylesheet>
See it working here : https://xsltfiddle.liberty-development.net/6qjt5Sw/1

XSLT 2.0/3.0- Selecting two records and inserting missing entries

I am working on a tricky requirement. I have some xml data which contains the holiday/weekend information. I need to insert missing dates in between (assuming missing days are working days).
I am thinking to get the first date and last date and then run a loop for number of days in that month and then insert the missing days but that requires finding the month information, and there may be all the months in the data.
Is there any quicker way to manipulate the dates in XSLT.
<HOLIDAYS>
<item>
<DATE>2020-01-01</DATE>
<FREEDAY>X</FREEDAY>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
<item>
<DATE>2020-01-11</DATE>
<FREEDAY>X</FREEDAY>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
<item>
<DATE>2020-01-12</DATE>
<FREEDAY>X</FREEDAY>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
<item>
<DATE>2020-01-18</DATE>
<FREEDAY>X</FREEDAY>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
</HOLIDAYS>
output required
<HOLIDAYS>
<item>
<DATE>2020-01-01</DATE>
<FREEDAY/>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
<item>
<item>
<DATE>2020-01-02</DATE>
<FREEDAY/>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
<DATE>2020-01-03</DATE>
<FREEDAY>X</FREEDAY>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
<item>
<DATE>2020-01-04</DATE>
<FREEDAY>X</FREEDAY>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
.....
<item>
<DATE>2020-01-31</DATE>
<FREEDAY>X</FREEDAY>
<HOLIDAY/>
<HOLIDAY_ID/>
<TXT_SHORT/>
<TXT_LONG/>
</item>
</HOLIDAYS>
As I said in a comment, you can substract xs:dates to get a duration, convert it to "a day" using division by a duration of a single day, then you should have the number of times you want to insert a new date:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" indent="yes"/>
<xsl:template match="item[following-sibling::item]">
<xsl:next-match/>
<xsl:apply-templates select="(1 to xs:integer((following-sibling::item[1]/DATE/xs:date(.) - xs:date(DATE)) div xs:dayTimeDuration('P1D')) - 1) ! current()" mode="empty"/>
</xsl:template>
<xsl:template match="item" mode="empty">
<xsl:copy>
<DATE>{xs:date(DATE) + xs:dayTimeDuration('P1D') * position()}</DATE>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
That only inserts the items with new DATEs, you can xsl:copy-of select="* except DATE" the other elements if needed or insert any new content.

Issue while using Preceding axis in XPath and XSLT

I am learning XSLT 2.0 and XPath. While creating the examples I came across to preceding axis available in XPath and created the below example.
Please find the below order.xml file used as input.
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Order id="12345">
<Item>
<ItemId>007</ItemId>
<ItemName>iPhone 5</ItemName>
<Price>500</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<ItemId>456</ItemId>
<ItemName>Ipad</ItemName>
<Price>600</Price>
<Quantity>2</Quantity>
</Item>
<Item>
<ItemId>7864567</ItemId>
<ItemName>Personal Development Book</ItemName>
<Price>10</Price>
<Quantity>10</Quantity>
</Item>
<Item>
<ItemId>123</ItemId>
<ItemName>Java Book</ItemName>
<Price>20</Price>
<Quantity>12</Quantity>
</Item>
</Order>
Please find the below XSLT testaxis.xsl file used for transformation of the above XML.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:value-of select="count(/Order/Item[ItemName='Ipad']/ItemName/preceding::*)" />
</xsl:template>
The output after transformation is
6
Here context node is below one if I am not wrong.
<ItemName>Ipad</ItemName>
If we count all the nodes which are before the context node then counts come to 5 .
Now coming to the question, why it is showing the count of the nodes as 6 in the output?
Please do let me know if I misunderstood anything
Thanks in advance.
You are correct about which node is the context node, and that node does have 6 preceding elements:
The first <Item> element.
The four elements inside that.
The <ItemId> element immediately before the context node.
That makes six. You can verify this by doing the following:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" />
<xsl:template match="/">
<xsl:for-each select="/Order/Item[ItemName='Ipad']/ItemName/preceding::*">
<xsl:value-of select="concat(name(), '
')" />
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The preceding elements are the ones marked below with a "*"
<*Item>
<*ItemId>007</ItemId>
<*ItemName>iPhone 5</ItemName>
<*Price>500</Price>
<*Quantity>1</Quantity>
</Item>
<Item>
<*ItemId>456</ItemId>
You will see that there are six of them.

xslt for-each-group question

I have a xml need to group by category
I try to use
<xsl:for-each-group select="ItemList/Item" group-by="CategoryID">
How can I get the CategoryName, CategoryID or any information related to Category
during the transformation?
Original XML
<ItemList>
<Item>
<CategoryID>1</CategoryID>
<CategoryName>Book</CategoryName>
<ItemName>ASP.NET</ItemName>
<ItemDetails>ASP.NET12345</ItemDetails>
</Item>
<Item>
<CategoryID>1</CategoryID>
<CategoryName>Book</CategoryName>
<ItemName>PHP</ItemName>
<ItemDetails>PHP12345</ItemDetails>
</Item>
<Item>
<CategoryID>2</CategoryID>
<CategoryName>Tool</CategoryName>
<ItemName>ToolAbcde</ItemName>
<ItemDetails>sth details</ItemDetails>
</Item></ItemList>
New XML
<NewXML>
<Category>
<CategoryID>1</CategoryID>
<CategoryName>Book</CategoryName>
<ItemList>
<Item>
<ItemName>ASP.NET</ItemName>
<ItemDetails>ASP.NET12345</ItemDetails>
</Item>
<Item>
<ItemName>PHP</ItemName>
<ItemDetails>PHP12345</ItemDetails>
</Item>
<ItemName>PHP</ItemName>
</ItemList>
</Category>
<Category>
<CategoryID>2</CategoryID>
<CategoryName>Tool</CategoryName>
<ItemList>
<Item>
<ItemName>ToolAbcde</ItemName>
<ItemDetails>sth details</ItemDetails>
</Item>
</ItemList>
</Category></NewXML>
The xslt spec has an extensive doucmentation on grouping, complete with examples at http://www.w3.org/TR/xslt20/#xsl-for-each-group. You are probably looking for the current-group() and current-grouping-key() functions. Also
Within the sequence constructor, the context item is the initial item of the relevant group, the context position is the position of this item among the sequence of initial items (one item for each group) arranged in processing order of the groups, the context size is the number of groups, the current group is the group being processed, and the current grouping key is the grouping key for that group.
Meaning that these two expressions would be equivalent inside xsl:for-each-group: CategoryId and current-group()[1]/CategoryId. Since CategoryId is the grouping key this is also the same as current-grouping-key(). Here is a complete example for your use case:
<?xml version="1.0" encoding="utf-8" ?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" indent="yes" encoding="utf-8" />
<xsl:template match="/">
<NewXml>
<xsl:for-each-group select="ItemList/Item" group-by="CategoryID">
<Category>
<CategoryID><xsl:value-of select="CategoryID" /></CategoryID>
<CategoryName><xsl:value-of select="CategoryName" /></CategoryName>
<ItemList>
<xsl:for-each select="current-group()">
<Item>
<ItemName><xsl:value-of select="ItemName" /></ItemName>
<ItemDetails><xsl:value-of select="ItemDetails" /></ItemDetails>
</Item>
</xsl:for-each>
</ItemList>
</Category>
</xsl:for-each-group>
</NewXml>
</xsl:template>
</xsl:stylesheet>

How do i render a limited number of elements in XSLT?

I need to render a specified number of elements from an XML source, where the elements "DueDate" is not exeeded. Here is an example of the xml:
<Items>
<Item>
<Title>Title 1</Title>
<DueDate>01-02-2008</DueDate>
</Item>
<Item>
<Title>Title 2</Title>
<DueDate>01-02-2009</DueDate>
</Item>
<Item>
<Title>Title 3</Title>
<DueDate>01-02-2010</DueDate>
</Item>
<Item>
<Title>Title 4</Title>
<DueDate>01-02-2011</DueDate>
</Item>
<Item>
<Title>Title 5</Title>
<DueDate>01-02-2012</DueDate>
</Item>
<Item>
<Title>Title 6</Title>
<DueDate>01-02-2013</DueDate>
</Item>
</Items>
The number of elements to display and the current date are passed to the XSLT as paramaters.
Is it possible to count the number of rendered elements in a for-each loop in Xslt? Or is there a better approach?
An example could be that the limit was set to 3 elements. In this example I would expect to see the following results: "Title 3", "Title 4" and "Title 5".
You can do something like this. Make it into a template you can call with paramters:
<xsl:template match="/">
<xsl:variable name="count" select="3"/>
<xsl:for-each select="Items/Item">
<xsl:if test="position() < $count">
<xsl:value-of select="Title"/> - <xsl:value-of select="DueDate"/>
</xsl:if>
</xsl:for-each>
Try nesting this inside a standard xsl for-each where n in your case is 3:
<xsl:if test="position() < n">
But if you also want to check the date then you will need nest another if and create dates in the format yyyyMMdd which can be numerically compared like this:
<xsl:variable name="secondDate" select="concat(substring(submissionDeadline, 1,4),substring(submissionDeadline, 6,2),substring(submissionDeadline, 9,2))"/>
<xsl:if test="$firstDate > $secondDate">
The main problem is that your input XML does not conform to the useful 'yyyy-mm-dd' format. This makes sorting/filtering these items a pain in the ass behind.
If I understand you correctly, you need to
filter on due date first
apply a maximum to the output after that
The XPath for selecting all <Item>s up to a certain maximum due date would go like this:
Item[
substring-before(DueDate, '-')
<=
substring-before($MaxDueDate, '-')
and
substring-before(substring-after(DueDate, '-'), '-')
<=
substring-before(substring-after($MaxDueDate, '-'), '-')
and
substring-after(substring-after(DueDate, '-'), '-')
<=
substring-after(substring-after($MaxDueDate, '-'), '-')
][
position() <= $MaxCount
]
Now compare that to the trivial way, if you had 'yyyy-mm-dd' dates:
Item[
DueDate <= $MaxDueDate
][
position() <= $MaxCount
]
So, to copy only these elements, you would go:
<xsl:template match="Items">
<xsl:copy>
<xsl:copy-of select="
{ the expression I gave above }
" />
</xsl:copy>
</xsl:template>
For parameter values of '01-02-2012' and 4, respectively, I get:
<Items>
<Item>
<Title>Title 1</Title>
<DueDate>01-02-2008</DueDate>
</Item>
<Item>
<Title>Title 2</Title>
<DueDate>01-02-2009</DueDate>
</Item>
<Item>
<Title>Title 3</Title>
<DueDate>01-02-2010</DueDate>
</Item>
<Item>
<Title>Title 4</Title>
<DueDate>01-02-2011</DueDate>
</Item>
</Items>