Increase number in text string for each match - xslt

I am looking to shorten my XSLT codebase by seeing if XSLT can increase a text number for each match. The text number exists in both the attribute value "label-period0" and the "xls:value-of" value.
The code works, no errors so this is more a question of how to shorten the code and make use of some sort of iteration on a specific character in a string.
I added 2 similar code structures for "period0" and "period1" to better see what exactly are the needed changes in terms of the digit in the text strings.
Source XML file:
<data>
<periods>
<period0><from>2016-01-01</from><to>2016-12-01</to></period0>
<period1><from>2015-01-01</from><to>2015-12-01</to></period1>
<period2><from>2014-01-01</from><to>2014-12-01</to></period2>
<period3><from>2013-01-01</from><to>2013-12-01</to></period3>
</periods>
<balances>
<balance0><instant>2016-12-31</instant></balance0>
<balance1><instant>2015-12-31</instant></balance1>
<balance2><instant>2014-12-31</instant></balance2>
<balance3><instant>2013-12-31</instant></balance3>
</balances>
</data>
XSL file:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform
version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="xml" indent="yes"/>
<!-- Block all data that has no user defined template -->
<xsl:mode on-no-match="shallow-skip"/>
<xsl:template match="data">
<results>
<periods>
<periods label="period0">
<xsl:value-of
select =
"concat(periods/period0/from, '--', periods/period0/to)"
/>
</periods>
<periods label="period1">
<xsl:value-of
select =
"concat(periods/period1/from, '--', periods/period1/to)"
/>
</periods>
<!-- Etc for period [2 and 3]-->
</periods>
<balances>
<balance label="balance0">
<xsl:value-of select ="balances/balance0/instant"/>
</balance>
<!-- Etc for balance [1,2 and 3] -->
</balances>
</results>
</xsl:template>
</xsl:transform>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<results>
<periods>
<periods label="period0">2016-01-01--2016-12-01</periods>
<periods label="period1">2015-01-01--2015-12-01</periods>
</periods>
<balances>
<balance label="balance0">2016-12-31</balance>
</balances>
</results>
Wanted result:
(with an XSL that steps the digit in the text string, or any other logics in XSL that could cater for manipulating the digit in text string)
<?xml version="1.0" encoding="UTF-8"?>
<results>
<periods>
<periods label="period0">2016-01-01--2016-12-01</periods>
<periods label="period1">2015-01-01--2015-12-01</periods>
<periods label="period2">2014-01-01--2015-12-01</periods>
<periods label="period3">2013-01-01--2015-12-01</periods>
</periods>
<balances>
<balance label="balance0">2016-12-31</balance>
<balance label="balance1">2015-12-31</balance>
<balance label="balance2">2014-12-31</balance>
<balance label="balance3">2013-12-31</balance>
</balances>
</results>

Couldn't you do simply something like:
<xsl:template match="/data">
<results>
<periods>
<xsl:for-each select="periods/*">
<periods label="{name()}">
<xsl:value-of select="from"/>
<xsl:text>--</xsl:text>
<xsl:value-of select="to"/>
</periods>
</xsl:for-each>
</periods>
<balances>
<xsl:for-each select="balances/*">
<balance label="{name()}">
<xsl:value-of select="instant"/>
</balance>
</xsl:for-each>
</balances>
</results>
</xsl:template>
If you want to do your own numbering, you can change:
<periods label="{name()}">
to:
<periods label="period{position() - 1}">

Related

How to not touch a record if text exist in an XML field but process the record in no text exist using XSLT 2

Using XSLT 2 how can I skip and not touch a record if a field contains text, in this case a date? I want to only process all the record that don't have a <SurveyDate> and don't touch record that already have a <SurveyDate>.
I tried using a choose statement with a test of "not(SurveyDate/text())" but this is not working. here is my complete XSL code:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:lookup="lookup" xmlns:exsl="http://exslt.org/common" exclude-result-prefixes="lookup exsl">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes" encoding="utf-8" media-type="xml/plain" />
<xsl:strip-space elements="*" />
<xsl:template match="node() | #*">
<xsl:copy>
<xsl:apply-templates select="node() | #*" />
</xsl:copy>
</xsl:template>
<xsl:template match="Sub">
<!-- This is the final output -->
<xsl:choose>
<xsl:when test="not(SurveyDate/text())">
<xsl:if test= "count(Request/Phase/Status) = count(Request/Phase/Status[matches(. , 'Sup|Ser|Adm|Can')])">
<Request>
<xsl:copy-of select="Request/Code"/>
<SurveyDate>
<xsl:value-of select="format-dateTime(current-dateTime(), '[Y0001]-[M01]-[D01]T[H1]:[m01]:[s01]')"/>
</SurveyDate>
</Request>
</xsl:if>
</xsl:when>
<xsl:otherwise>
<!-- just for testing remove when done -->
<Test>Do nothing</Test>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
And this is my test XML data.
<?xml version='1.0' encoding='UTF-8'?>
<document>
<businessobjects>
<Sub>
<Code>1.02</Code>
<Status>UsrWorkOrderCancelled</Status>
<Request>
<Code>1.00</Code>
<Description>Test 1</Description>
<SurveyDate>2022-11-02T22:55:55</SurveyDate>
<Phase>
<Code>1.01</Code>
<Status>UsrWorkOrderSupervisorApproved</Status>
</Phase>
<Phase>
<Code>1.02</Code>
<Status>UsrWorkOrderCancelled</Status>
</Phase>
</Request>
</Sub>
<Sub>
<Code>2.01</Code>
<Status>UsrWorkOrderSupervisorApproved</Status>
<Request>
<Code>2.00</Code>
<Description>Test 2</Description>
<SurveyDate></SurveyDate>
<Phase>
<Code>2.01</Code>
<Status>UsrWorkOrderSupervisorApproved</Status>
</Phase>
<Phase>
<Code>2.02</Code>
<Status>UsrWorkOrderCancelled</Status>
</Phase>
</Request>
</Sub>
</businessobjects>
</document>
The result XML I need is this:
<document>
<businessobjects>
<Request>
<Code>2.00</Code>
<SurveyDate>2022-11-03T21:45:13</SurveyDate>
</Request>
</businessobjects>
</document>
My advice: forget using xsl:choose or xsl:if, and instead put the conditional logic into the template's match expression:
<xsl:template match="Sub[not(Request/SurveyDate/text())]">
<!-- handle Sub without SurveyDate -->
<!-- ... -->
</xsl:template>
Leave the case where a Sub does have a SurveyDate for the identity template to handle, if you want to copy it unchanged. If you want to remove it (it's not clear from your test code what you want to do with it), you could add another template to do so:
<xsl:template match="Sub"/>
Note that template would have a lower priority than the one above, because its match expression is simpler, so it would apply only to Sub elements which did have a SurveyDate descendant.

How to determine the value format is dd-mmm-yyyy in xslt

I have to determine the input value having date format of dd-mmm-yyyy. If I can find will set some attribute based on the attribute I can do the format in C# report processing class.
<td>
<xsl:if test="To write expression to match the value">
<r>
<xyz:value-of select="'Set Value'" />
</r>
</xsl:if>
</td>
Input value is "30-Jun-2019". If it matches I want to set .
Basically I have set of columns in the report. I have to identify the the values in the report if the value matches with the Date format of dd-mmm-yyy setting some attribute in the xslt and applying the same format in report parser code which is written in c#
As I said in comments, there is no regex support in XSLT 1.0, so this can get quite tedious.
Consider the following example:
XML
<input>
<item>21-Jan-1987</item>
<item>921-Jan-1987</item>
<item>15-Jul-2009</item>
<item>15-Jux-2009</item>
<item>03-Dec-2014</item>
<item>03-Dec-999</item>
</input>
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:strip-space elements="*"/>
<xsl:template match="/input">
<output>
<xsl:for-each select="item">
<item value="{.}">
<xsl:variable name="dd" select="substring-before(., '-')" />
<xsl:variable name="mmm" select="substring-before(substring-after(., '-'), '-')" />
<xsl:variable name="yyyy" select="substring-after(substring-after(., '-'), '-')" />
<xsl:if test="translate($dd, '123456789', '000000000') = '00' and translate($yyyy, '123456789', '000000000') = '0000' and ($mmm='Jan' or $mmm='Feb' or $mmm='Mar' or $mmm='Apr' or $mmm='May' or $mmm='Jun' or $mmm='Jul' or $mmm='Aug' or $mmm='Sep' or $mmm='Oct' or $mmm='Nov' or $mmm='Dec')">
<xsl:text>Is Date</xsl:text>
</xsl:if>
</item>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<item value="21-Jan-1987">Is Date</item>
<item value="921-Jan-1987"/>
<item value="15-Jul-2009">Is Date</item>
<item value="15-Jux-2009"/>
<item value="03-Dec-2014">Is Date</item>
<item value="03-Dec-999"/>
</output>
Note that this checks only that the input conforms to the pattern, not that the date itself is valid. Also keep in mind that XML is case-sensititve.
Added:
If you prefer, you could simplify the test to:
<xsl:if test="translate(translate(translate(., '123456789', '000000000'), 'JFMASOND', '########'), 'anebpryulgctov', '%%%%%%%%%%%%%%') = '00-#%%-0000'">
but then a value like 15-Jpt-2009 will pass as date.
In XSLT 2.0 this is fairly trivial: matches(., '[0-9]{2}-[A-Z][a-z]{2}-[0-9]
{4}')
In 1.0 it's considerably harder, and it depends a little bit how precise you want to be. But you could get close with translate(translate($input, 'ABC...abc...', 'AAAAAAAA....'), '0123456789', '9999999999') = '99-AAA-9999') where the '...' means you have to write out the rest of the alphabet.

XSL text output - trim blank lines and leading spaces

I have an XSLT that looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="no" encoding="utf-8" media-type="text/plain" />
<xsl:template match="/SOME/NODE">
<xsl:if test="./BLAH[foo]">
<xsl:value-of select="concat(#id, ',' , ./BLAH/bar/#id, ',' , ./blorb/text())"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
The output looks something like this (it is going to be a CSV file):
1,2,3
4,456,22
90,5,some text
365,16,soasdkjasdjkasdf
9,43,more text
What I need is for it to be transformed into:
1,2,3
4,456,22
90,5,some text
365,16,soasdkjasdjkasdf
9,43,more text
The main problems are the blank lines (from nodes that do not match the IF condition) and the indentation. Is there any way to remove the blank lines and trim the indentation while preserving the line breaks after lines that are not blank?
I've tried using <xsl:strip-space elements="*"/>, but then the output looks like this:
1,2,3,4,456,22,90,5,some text,365,16,soasdkjasdjkasdf,9,43,more text
Which doesn't work since I need to have 3 values on each line.
As requested, a (heavily simplified) sample of the input:
<SOME>
<NODE>
<BLAH id="1">
<foo>The Foo</foo>
<bar id="2" />
<blorb> some text </blorb>
</BLAH>
</NODE>
<NODE>
<BLAH id="3">
<bar id="4" />
<blorb>some text that shouldn't be in output because there's no foo here</blorb>
</BLAH>
</NODE>
<NODE>
<BLAH id="5">
<foo>another Foo</foo>
<bar id="6" />
<blorb>some other text</blorb>
</BLAH>
</NODE>
</SOME>
I would suggest you approach it this way:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="utf-8" />
<xsl:template match="/SOME">
<xsl:for-each select="NODE/BLAH[foo]">
<xsl:value-of select="#id"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="bar/#id"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="blorb"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

XSL cross reference

I am stuck with a XSLT 1.0 problem. I tried to find info on StackOverflow but I couldn't apply the examples.
Here is the structure of my XML:
<XML>
<PR>
<AS>
<ID_AS>AS-001</ID_AS>
<FIRST>
<ID_CATALOG>Id-001</ID_CATALOG>
<STATUS>NOK</STATUS>
</FIRST>
<SECOND>
<ID_CATALOG>Id-002</ID_CATALOG>
<STATUS>OK</STATUS>
</SECOND>
</AS>
<AS>
<ID_AS>AS-002</ID_AS>
<FIRST>
<ID_CATALOG>Id-003</ID_CATALOG>
<STATUS>OK</STATUS>
</FIRST>
<SECOND>
<ID_CATALOG>Id-004</ID_CATALOG>
<STATUS>OK</STATUS>
</SECOND>
</AS>
</PR>
<METADATA>
<ID_CATALOG>Id-001</ID_CATALOG>
<ANGLES>32.25</ANGLES>
</METADATA>
<METADATA>
<ID_CATALOG>Id-002</ID_CATALOG>
<ANGLES>18.75</ANGLES>
</METADATA>
<METADATA>
<ID_CATALOG>Id-003</ID_CATALOG>
<ANGLES>5.23</ANGLES>
</METADATA>
<METADATA>
<ID_CATALOG>Id-004</ID_CATALOG>
<ANGLES>12.41</ANGLES>
</METADATA>
</XML>
I want to display for each AS, the FIRST/ID_CATALOG, FIRST/STATUS and ANGLES corresponding to the ID_CATALOG, then SECOND/etc.
The output would be similar to:
AS-001
Id-001 NOK 32.25
Id-002 OK 18.75
AS-002
Id-003 OK 5.23
Id-004 OK 12.41
I tried the following XSL but I only get the ANGLES for the first item
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns="http://earth.google.com/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:hma="http://earth.esa.int/hma" xmlns:gml="http://www.opengis.net/gml" xmlns:xlink="http://www.w3.org/1999/xlink">
<xsl:output method="xml" indent="yes" encoding="ISO-8859-1"/>
<!--==================MAIN==================-->
<xsl:template match="/">
<html>
<body>
AS List:
<br/><br/>
<xsl:call-template name="ASandCo"/>
</body>
</html>
</xsl:template>
<!--==================TEMPLATES==================-->
<xsl:template name="ASandCo">
<AS>
<xsl:for-each select="XML/PR/AS">
<xsl:value-of select="ID_AS"/>
<br/>
<xsl:value-of select="FIRST/ID_CATALOG"/> - <xsl:value-of select="FIRST/STATUS"/> -
<xsl:if test="contains(/XML/METADATA/ID_CATALOG, FIRST/ID_CATALOG)">
<xsl:value-of select="/XML/METADATA/ANGLES"/>
</xsl:if>
<br/>
<xsl:value-of select="SECOND/ID_CATALOG"/> - <xsl:value-of select="SECOND/STATUS"/> -
<xsl:if test="contains(/XML/METADATA/ID_CATALOG, SECOND/ID_CATALOG)">
<xsl:value-of select="/XML/METADATA/ANGLES"/>
</xsl:if>
<br/><br/>
</xsl:for-each>
</AS>
</xsl:template>
</xsl:stylesheet>
This XSLT will be applied to very large XML files, so I am trying to find the most efficient way.
Thank you very much in advance!
It seems like you want to look up some metadata metadata based on the ID_CATALOG value.
An efficient way to do this is by using a key. You can define a key on the top level:
<xsl:key name="metadata-by-id_catalog" match="METADATA" use="ID_CATALOG"/>
And then you can look up the ANGLES value using the key for a given ID_CATALOG value like this:
<xsl:value-of select="key('metadata-by-id_catalog', FIRST/ID_CATALOG)/ANGLES"/>
and this:
<xsl:value-of select="key('metadata-by-id_catalog', SECOND/ID_CATALOG)/ANGLES"/>

Two phase XSLT transformation converting string to XML first

I have the following XML:
<?xml version="1.0" encoding="utf-8"?>
<string>
<Table>
<Rows>
<Row Id="0">
<Column Name="INS_NAME" XPath="Ins.Name">Jane</Column>
<Column Name="INS_LASTNAME" XPath="Ins.LastName">Smith</Column>
</Row>
<Row Id="1">
<Column Name="INS_NAME" XPath="Ins.Name">Joe</Column>
<Column Name="INS_LASTNAME" XPath="Ins.LastName">Miller</Column>
</Row>
<Row Id="2">
<Column Name="INS_NAME" XPath="Ins.Name">George</Column>
<Column Name="INS_LASTNAME" XPath="Ins.LastName">Ramsey</Column>
</Row>
</Rows>
</Table>
</string>
and I would like to transform it to this XML using a single XSLT:
<?xml version="1.0" encoding="utf-8"?>
<Customers>
<Customer><Name>Jane</Name><LastName>Smith</LastName></Customer>
<Customer><Name>Joe</Name><LastName>Miller</LastName></Customer>
<Customer><Name>George</Name><LastName>Ramsey</LastName></Customer>
</Customers>
I can do it with two different XSLT's:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:value-of select="/" disable-output-escaping="yes" />
</xsl:template>
</xsl:stylesheet>
and then:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<Customers>
<xsl:for-each select="Table/Rows/Row">
<Customer>
<Name><xsl:value-of select="Column[#Name='INS_NAME']" /></Name>
<LastName><xsl:value-of select="Column[#Name='INS_LASTNAME']" /></LastName>
</Customer>
</xsl:for-each>
</Customers>
</xsl:template>
</xsl:stylesheet>
I have been reading about multi phase transformations but I can't seem to get it. I have tried saving the first XSLT in a variable but it seems disable-output-escaping="yes" does not work when saving to a variable.
Can anybody help?
Thank you.
New information (Edit)
I am now translating the string this way:
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="stringXml">
<?xml version="1.0" encoding="utf-8"?>
<xsl:value-of select="translate(translate(/,'>','>'),'<','<')" />
</xsl:variable>
...
How can I do a transformation on the resulting XML stored in stringXML?
Final Solution (Edit)
<msxml:script implements-prefix="myLib" language="C#">
<msxml:assembly name="System.Web"/>
<msxml:using namespace="System.Web"/>
<![CDATA[
public System.Xml.XPath.XPathNodeIterator convertText(string text)
{
XmlDocument doc = new XmlDocument();
doc.PreserveWhitespace = true;
doc.LoadXml(text);
return doc.CreateNavigator().Select("/");
}
]]>
</msxml:script>
it seems disable-output-escaping="yes" does not work when saving to a
variable.
Your observation is correct.
DOE only affects the serialization of the (final) result of the transformation and isn't applied on intermediary trees.
Here is what the W3C XSLT 1.0 specification explicitly says:
"An XSLT processor will only be able to disable output escaping if it
controls how the result tree is output. This may not always be the
case. For example, the result tree may be used as the source tree for
another XSLT transformation instead of being output."
The same negative answer holds for trying to use a variable, whose value is a string, containing a textual representation of an XML document.
I had a similar situation where I needed to parse an escaped XML inside my actual XML. I will post up my solution to also help someone else. Please also note that I am also using Saxon-PE parser.
In my situation I have the original XML that contains an escaped XML in a child node. I needed to get the inner XML inside the RootNode of the escaped XML.
Source XML:
<?xml version="1.0" encoding="utf-8"?>
<MyTestXml>
<SomeXmlStuff>
<Text1>Hello</Text1>
<Text2>World</Text2>
</SomeXmlStuff>
<SomeEscapedXml><RootNode><FirstNode>Hello</FirstNode><SecondNode>World</SecondNode><ThirdNode>Again</ThirdNode></RootNode></SomeEscapedXml>
</MyTestXml>
When you unescaped the XML, it looks like this:
<RootNode>
<FirstNode>Hello</FirstNode>
<SecondNode>World</SecondNode>
<ThirdNode>Again</ThirdNode>
</RootNode>
With the following XSLT transformation is applied on the source XML:
<?xml version='1.0' encoding='utf-8' ?>
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:saxon="http://saxon.sf.net/"
exclude-result-prefixes="xsl saxon">
<xsl:template match="/">
<MyOutput>
<xsl:call-template name="GetRootNodeInnerXml">
<xsl:with-param name="escapedXml" select="MyTestXml/SomeEscapedXml" />
</xsl:call-template>
</MyOutput>
</xsl:template>
<xsl:template name="GetRootNodeInnerXml">
<xsl:param name="escapedXml" required="yes" />
<xsl:copy-of select="saxon:parse($escapedXml)/RootNode/node()"/>
<!-- You can also use this line below if you're not using saxon parser. Just make sure your parser supports XSL 3.0 -->
<!--
<xsl:copy-of select="fn:parse-xml($escapedXml)/RootNode/node()" xmlns:fn="http://www.w3.org/2005/xpath-functions"/>
-->
</xsl:template>
</xsl:stylesheet>
This gives you the following output:
<?xml version='1.0' ?>
<MyOutput>
<FirstNode>Hello</FirstNode>
<SecondNode>World</SecondNode>
<ThirdNode>Again</ThirdNode>
</MyOutput>