Hi, this is the content of my XML file:
<?xml version="1.0" encoding="ISO-8859-1"?>
<mainNode>
<sub time="08:00">
<status id="2">On</status>
<status id="3">Off</status>
</sub>
<sub time="13:00">
<status id="4">On</status>
<status id="7">On</status>
</sub>
<sub time="16:00">
<status id="5">On</status>
<status id="6">On</status>
<status id="7">Off</status>
<status id="8">On</status>
</sub>
<sub time="20:00">
<status id="4">Off</status>
<status id="7">On</status>
</sub>
<sub time="23:59">
<status id="4">On</status>
<status id="7">On</status>
</sub>
</mainNode>
My program gets the current time:
if I get 15.59, I must retrieve all the status id of the next sub time (16.00):
<sub time="16:00">
<status id="5">On</status>
<status id="6">On</status>
<status id="7">Off</status>
<status id="8">On</status>
</sub>
With a second XPath query I must get all the status id of the previous sub time (13.00).
How to do it? I know SQL but I'm quite new to XPath. I accept urls to serious XPath resources too, if any. Thanks!
Here are two solutions:
I. XPath 1.0
This is one pair of XPath 1.0 expressions that select the required nodes:
/*/*
[translate(#time, ':','')
>
translate('15:59',':','')
][1]
selects the first sub node with time later than 15:59.
/*/*
[translate(#time, ':','')
<
translate('15:59',':','')
][last()]
selects selects the first sub node with the previous than 15:59 sub time.
We can include these in an XSLT transformation and check that the really wanted result is produced:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/">
First time after 15:59:
<xsl:copy-of select=
"/*/*
[translate(#time, ':','')
>
translate('15:59',':','')
][1]
"/>
First time before 15:59:
<xsl:copy-of select=
"/*/*
[translate(#time, ':','')
<
translate('15:59',':','')
][last()]
"/>
</xsl:template>
</xsl:stylesheet>
When the above transformation is applied on the originally provided XML document:
<mainNode>
<sub time="08:00">
<status id="2">On</status>
<status id="3">Off</status>
</sub>
<sub time="13:00">
<status id="4">On</status>
<status id="7">On</status>
</sub>
<sub time="16:00">
<status id="5">On</status>
<status id="6">On</status>
<status id="7">Off</status>
<status id="8">On</status>
</sub>
<sub time="20:00">
<status id="4">Off</status>
<status id="7">On</status>
</sub>
<sub time="23:59">
<status id="4">On</status>
<status id="7">On</status>
</sub>
</mainNode>
the wanted result is produced:
First time after 15:59:
<sub time="16:00">
<status id="5">On</status>
<status id="6">On</status>
<status id="7">Off</status>
<status id="8">On</status>
</sub>
First time before 15:59:
<sub time="13:00">
<status id="4">On</status>
<status id="7">On</status>
</sub>
Do note the following:
The use of the XPath translate() function to get rid of the colons
The use of the last() function in the second expression
There is no need to convert the time to seconds before the comparison
When used as part of an XML document (such as an XSLT stylesheet, the < operator must be escaped.
II. XPath 2.0
In XPath 2.0 we can use the following two expressions to produce select the desired nodes:
/*/*[xs:time(concat(#time,':00'))
gt
xs:time('15:59:00')
][1]
selects the first sub node with time later than 15:59.
/*/*[xs:time(concat(#time,':00'))
lt
xs:time('15:59:00')
][last()]
selects selects the first sub node with the previous than 15:59 sub time.
We can include these in an XSLT 2.0 transformation and check that the really wanted result is produced:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output omit-xml-declaration="yes"/>
<xsl:template match="/">
First time after 15:59:
<xsl:copy-of select=
"/*/*[xs:time(concat(#time,':00'))
gt
xs:time('15:59:00')
][1]
"/>
First time before 15:59:
<xsl:copy-of select=
"/*/*[xs:time(concat(#time,':00'))
lt
xs:time('15:59:00')
][last()]
"/>
</xsl:template>
</xsl:stylesheet>
When the above transformation is applied on the originally provided XML document (the same as in the first solution), the same wanted result is produced.
Do note the following:
In XPath 2.0 xs:time is a native data type. However, in order to construct an xs:time() from the values in the xml document, we have to concat to them the missing seconds part.
In XPath 2.0 xs:time values can be compared with the "atomic-value comarison operators" such as lt or gt.
Here is the ugly Xpath 1.0 solution:-
sub[number((substring-before(#time, ':')) * 60 + number(substring-after(#time, ':'))) > 959][1]
Note 959 = 15 * 60 + 59 which I'm sure you can do in your calling code.
Give that node the previous node can be accessed as:-
preceding-sibling::sub[1]
However a pragmatic, common sense solution would be to load the XML data into a set of data structures and use a language more suited to this task to look the items up.
Well as long as the time is HH:MM something like the following should work:
(I must excuse my syntax since I'm just dabbling without running, consider this pseudo-xpath):
xmlns:fn="http://www.w3.org/2005/02/xpath-functions"
//sub[fn:compare(#time,'12:59') > 0][1]/status
This should select all the elements where time is greater than 12:59 and then select the first of those elements.
You could also pass the value '12:59' as an external parameter into the xpath evaluation.
If you generate the xml yourself, you could change the way you store the time attribute using an integer value (ticks, for example), then you could do an easy numerical comparison using something like
//sub[#time > 1389893892]
Related
I use elementpath to handle some XPath queries. I have an XML with linear structure which contains a unique id attribute.
<items>
<item id="1">...</item>
<item id="2">...</item>
<item id="3">...</item>
... 500k elements
<item id="500003">...</item>
</items>
I want the parser to find the first occurence without traversing all the nodes. For example, I want to select //items/item[#id = '3'] and stop after iterating over 3 nodes only (not over 500k of nodes). It would be a nice optimization for many cases.
An example using XSLT 3 streaming with a static parameter for the XPath, then using xsl:iterate with xsl:break to produce the "early exit" once the first item sought has been found would be
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all">
<xsl:param name="path" static="yes" as="xs:string" select="'items/item[#id = ''3'']'"/>
<xsl:output method="xml"/>
<xsl:mode on-no-match="shallow-copy" streamable="yes"/>
<xsl:template match="/" name="xsl:initial-template">
<xsl:iterate _select="{$path}">
<xsl:if test="position() = 1">
<xsl:copy-of select="."/>
<xsl:break/>
</xsl:if>
</xsl:iterate>
</xsl:template>
</xsl:stylesheet>
You can run it with SaxonC EE (unfortunately streaming is only supported by EE) and Python with e.g.
import saxonc
with saxonc.PySaxonProcessor(license=True) as proc:
print("Test SaxonC on Python")
print(proc.version)
xslt30proc = proc.new_xslt30_processor()
xslt30proc.set_parameter('path', proc.make_string_value('/items/item[#id = "2"]'))
transformer = xslt30proc.compile_stylesheet(stylesheet_file='iterate-items-early-exit1.xsl')
xdm_result = transformer.apply_templates_returning_value(source_file='items-sample1.xml')
if transformer.exception_occurred:
print(transformer.error_message)
print(xdm_result)
Need your help to validate if below XSLT code is correct.
My input XML would look like below.
<?xml version="1.0" encoding="UTF-8"?>
<Workers>
<Worker>
<Batch_number>1234</Batch_number>
<Assignment>
<type>A</type>
<name>New York</name>
</Assignment>
<Assignment>
<type>A</type>
<name>Boston</name>
</Assignment>
<Assignment>
<type>A</type>
<name>Boston</name>
</Assignment>
<Assignment>
<type>A</type>
<name>Boston</name>
</Assignment>
<Assignment>
<type>B</type>
<name>Chicago</name>
</Assignment>
</Worker>
</Workers>
My output file should look like below after transformation
<?xml version="1.0" encoding="UTF-8"?>
<Update_Data>
<Records>
<Batch_number>1234</Batch_number>
<Record_Location>
<Location>
<order>1</order>
<name>New York</name>
</Location>
<Location>
<order>2</order>
<name>Boston</name>
</Location>
<Location>
<order>3</order>
<name>Boston</name>
</Location>
<Location>
<order>4</order>
<name>Boston</name>
</Location>
</Record_Location>
<Record_Location>
<Location>
<order>1</order>
<name>Chicago</name>
</Location>
</Record_Location>
</Records>
</Update_Data>
I have written following XSLT
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Update_Data>
<Records>
<Batch_number><xsl:value-of select="Workers/Worker/Batch_number"/></Batch_number>
<Record_Location>
<xsl:for-each select="Workers/Worker/Assignment[type='A']">
<Location>
<order>
<xsl:value-of select="position()"/>
</order>
<name>
<xsl:value-of select="name"/>
</name>
</Location>
</xsl:for-each>
</Record_Location>
<xsl:for-each select="Workers/Worker/Assignment[type='B']">
<Record_Location>
<Location>
<order>
<xsl:value-of select="position()"/>
</order>
<name>
<xsl:value-of select="name"/>
</name>
</Location>
</Record_Location>
</xsl:for-each>
</Records>
</Update_Data>
</xsl:template>
</xsl:stylesheet>
Requirements for transformation.
If value of is same and it's appearing multiple times in input XML, Location elements(under Record_Location tag) need to be created as many times as is exists in the file with respective values.
for .e.g. <type> is A appearing 4 times in input xml, so four <Location> elements need to be created tag should hold sequenced value.
<type> is B appearing only once, so only one element needs to be present in the output file.
Number of <Record_Location> elements in the output should be same as unique values of <type>. In my example, only two unique values are present i.e. A and B, so my output should have only two <Record_Location>
XSLT code that i have written yields expected result. But, I'm not sure if this is the efficient way to write the code for my requirement. Is there a way to consolidate the for-each conditions by not specifying type='A' or type='B' etc.. Input XML can contain multiple different types. Or is it even possible to write a code without a for-each loop
Can someone please take a look at this and suggest best approach please?
-thanks
Anoop
After incorporating Martin's suggestion in XSLT.. I got something like below.
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Update_Data>
<Records>
<Batch_number>
<xsl:value-of select="Workers/Worker/Batch_number"/>
</Batch_number>
<xsl:for-each-group select="Workers/Worker/Assignment" group-by="type">
<Record_Location>
<xsl:apply-templates select="current-group()"/>
</Record_Location>
</xsl:for-each-group>
</Records>
</Update_Data>
</xsl:template>
<xsl:template match="Assignment">
<Location>
<order>
<xsl:value-of select="position()"/>
</order>
<name>
<xsl:apply-templates select="name"/>
</name>
</Location>
</xsl:template>
</xsl:stylesheet>
TL;DR; Why can't I use the element name in the XPATH going against a msxsl:node-set? It always returns nothing, as if the node-set is empty, when debugging shows that it is not empty.
Details: I need to use a node-set in an XSLT 1.0 document because my source XML is missing an important node. Instead of having to rewrite the entire XSLT, I'd like to instead inject a node-set so that my XSLT processing can continue as normal. I would like to use XPATH on the node-set but I am not able to use the actual element names, instead only a * works, but I am not sure why, or how I can access the actual element names in the XPATH.
Here is my XML (example only, the XML document here is the least important, see XSLT):
<?xml version="1.0" encoding="utf-8" ?>
<?xml-stylesheet type="text/xsl" href="generic.xslt" ?>
<ParentNode xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" i:noNamespaceSchemaLocation="generic.xsd">
<SomeChildNode>text</SomeChildNode>
</ParentNode>
Here is my XSLT:
<?xml version="1.0" encoding="utf-16"?>
<xsl:stylesheet version="1.0" xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:a="http://schemas.datacontract.org/2004/07/MeM.BizEntities" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<xsl:output method="xml" indent="yes" encoding="utf-16" omit-xml-declaration="no" />
<!-- Global Variables, used in multiple places -->
<xsl:variable name="empty"/>
<!-- Match Templates -->
<xsl:template match="ParentNode">
<ArrayOfSalesOrder>
<xsl:for-each select="SomeChildNode">
<xsl:call-template name="SomeChildNodeTemplate">
<xsl:with-param name="order" select="."/>
</xsl:call-template>
</xsl:for-each>
</ArrayOfSalesOrder>
</xsl:template>
<xsl:template name="SomeChildNodeTemplate">
<xsl:variable name="someRTF">
<Items>
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
</xsl:variable>
<xsl:call-template name="ItemsTemplate">
<xsl:with-param name="items" select="msxsl:node-set($someRTF)"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="ItemsTemplate">
<xsl:param name="items"/>
<ItemsTransformed>
<xsl:for-each select="$items/Item">
<NewItem>
<NewCode>
<xsl:value-of select="Code"/>
</NewCode>
</NewItem>
</xsl:for-each>
</ItemsTransformed>
<ItemsTransformedThatWorksButNotHowIWant>
<xsl:for-each select="$items/*/*">
<NewItem>
<NewCode>
<xsl:value-of select="*[1]"/>
</NewCode>
<NewPrice>
<xsl:value-of select="*[2]"/>
</NewPrice>
<NewQuantity>
<xsl:value-of select="*[3]"/>
</NewQuantity>
</NewItem>
</xsl:for-each>
</ItemsTransformedThatWorksButNotHowIWant>
</xsl:template>
</xsl:stylesheet>
I would expect to be able to use XPATH to query into the node-set such that I can use their proper element names. This doesn't seem to be the case, and I'm struggling to understand why. I know there can be namespacing issues, but trying *:Item etc. doesn't work for me. I am able to use *[local-name()='Item'] but this seems like a horrible work around, not to mention that I'll have to rewrite any downstream templates and that is what I'm trying to avoid by using the node-set in the first place.
Result:
<?xml version="1.0" encoding="utf-16"?>
<ArrayOfSalesOrder xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2" xmlns:msxsl="urn:schemas-microsoft-com:xslt" xmlns:a="http://schemas.datacontract.org/2004/07/MeM.BizEntities" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<ItemsTransformed />
<ItemsTransformedThatWorksButNotHowIWant>
<NewItem>
<NewCode>code</NewCode>
<NewPrice>75</NewPrice>
<NewQuantity>1</NewQuantity>
</NewItem>
<NewItem>
<NewCode>code2</NewCode>
<NewPrice>100</NewPrice>
<NewQuantity>3</NewQuantity>
</NewItem>
</ItemsTransformedThatWorksButNotHowIWant>
</ArrayOfSalesOrder>
As you can see, I can get it to work with * but this is not very usable on a more complex structure. What am I doing wrong? Does this have to do with namespaces?
I would expect to see something under the <ItemsTransformed /> node, but instead it is just empty, and so far I can't get anything except the * to work.
The SO question below is what I was using, I thought I had an answer there, but I can't get the XPATH to work.
Reference:
XSLT 1.0 - Create node set and pass as a parameter
The problem here is that your stylesheet has a default namespace:
xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2"
Therefore, when you do:
<xsl:variable name="someRTF">
<Items>
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
</xsl:variable>
you are populating your variable with elements in the default namespace, so the variable actually contains:
<Items xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2">
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
Naturally, when you try later to select something like:
<xsl:for-each select="xyz:node-set($someRTF)/Items/Item">
you select nothing, because both Items and Item are in the default namespace and you're not calling them by their fully qualified name.
--- edit: ---
The problem can be easily solved by making sure that the root element of the variable - and by extension, all its descendants - are in no namespace.
Here's a simplified example (will run with any input):
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:variable name="someRTF">
<Items xmlns="">
<Item>
<Code>code</Code>
<Price>75</Price>
<Quantity>1</Quantity>
</Item>
<Item>
<Code>code2</Code>
<Price>100</Price>
<Quantity>3</Quantity>
</Item>
</Items>
</xsl:variable>
<xsl:template match="/">
<ArrayOfSalesOrder>
<ItemsTransformed>
<xsl:for-each select="exsl:node-set($someRTF)/Items/Item">
<NewItem>
<NewCode>
<xsl:value-of select="Code"/>
</NewCode>
</NewItem>
</xsl:for-each>
</ItemsTransformed>
</ArrayOfSalesOrder>
</xsl:template>
</xsl:stylesheet>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<ArrayOfSalesOrder xmlns="http://schemas.datacontract.org/2004/07/MeM.BizEntities.Integration.DataFeedV2">
<ItemsTransformed>
<NewItem>
<NewCode>code</NewCode>
</NewItem>
<NewItem>
<NewCode>code2</NewCode>
</NewItem>
</ItemsTransformed>
</ArrayOfSalesOrder>
I have the repeating structure of XML and I want to use the max() and min() function present in the xslt 2.0 to fetch the maximum and minimum dates.
I am using like max(Detail/Startdate) and it is returning me NaN as the result. Could anyone help me out here?
<Info dataSource="source">
<Detail>
<StartDate>20121211</StartDate>
<EndDate>20130112</EndDate>
</Detail>
<Detail>
<StartDate>20121211</StartDate>
<EndDate>20130112</EndDate>
</Detail>
<Detail>
<StartDate>20121211</StartDate>
<EndDate>20130112</EndDate>
</Detail>
<Detail>
<StartDate>20121218</StartDate>
<EndDate>20130114</EndDate>
</Detail>
</Info>
For the sample you posted and the XSLT
<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:template match="Info">
<xsl:value-of select="max(Detail/StartDate), max(Detail/EndDate), min(Detail/StartDate), min(Detail/EndDate)"/>
</xsl:template>
</xsl:stylesheet>
Saxon 9.5 gives me 2.0121218E7 2.0130114E7 2.0121211E7 2.0130112E7 and not NaN. Obviously treating those date values as doubles is not the ideal approach so I would suggest to use something alike
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf">
<xsl:function name="mf:date" as="xs:date">
<xsl:param name="date" as="xs:string"/>
<xsl:sequence select="xs:date(concat(substring($date, 1, 4), '-', substring($date, 5, 2), '-', substring($date, 7)))"/>
</xsl:function>
<xsl:template match="Info">
<xsl:value-of select="max(Detail/StartDate/mf:date(.)), max(Detail/EndDate/mf:date(.)), min(Detail/StartDate/mf:date(.)), min(Detail/EndDate/mf:date(.))"/>
</xsl:template>
</xsl:stylesheet>
to convert to xs:date first and then to compute a max or min date and not a max or min number.
If you still have problems then post enough information for us to reproduce the problem.
<customer>
<item>
<BILLNO>1</BILLNO>
<product>ABC</product>
<AMT>20</AMT>
</item>
<item>
<BILLNO>2</BILLNO>
<product>GHK</product>
<AMT>30</AMT>
</item>
<item>
<BILLNO>1</BILLNO>
<product>XYZ</product>
<AMT>20</AMT>
</item>
</customer>
I am trying to take grand sum of distinct values using xslt1.0.
I want the output like this using muenchian method.each bill will have multiple products. at the end of the day i need total number of bills and total amount
<sales>
<totalbills>2</totalbills>
<totalamount>50</totalamount>
</sales>
Thanks for help
ram
This Xslt stylesheet:
<?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:key name="item-key" match="item" use="BILLNO/text()"/>
<xsl:template match="/customer">
<root>
<xsl:for-each select="item[generate-id() = generate-id(key('item-key', BILLNO/text()))]">
<sales>
<totalbills>
<xsl:value-of select="count(../item[BILLNO = current()/BILLNO])"/>
</totalbills>
<totalamount>
<xsl:value-of select="sum(../item[BILLNO = current()/BILLNO]/AMT)"/>
</totalamount>
</sales>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
renders the following ouptut:
<?xml version="1.0" encoding="utf-8"?>
<root>
<sales>
<totalbills>2</totalbills>
<totalamount>40</totalamount>
</sales>
<sales>
<totalbills>1</totalbills>
<totalamount>30</totalamount>
</sales>
</root>
This short and simpler transformation (no xsl:for-each, no .., no text() useage):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kBills" match="item" use="BILLNO"/>
<xsl:variable name="vdistItems" select=
"/*/*[generate-id() = generate-id(key('kBills', BILLNO)[1])]"/>
<xsl:template match="/*">
<sales>
<totalbills><xsl:value-of select="count($vdistItems)"/></totalbills>
<totalamount><xsl:value-of select="sum($vdistItems/AMT)"/></totalamount>
</sales>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<customer>
<item>
<BILLNO>1</BILLNO>
<product>ABC</product>
<AMT>20</AMT>
</item>
<item>
<BILLNO>2</BILLNO>
<product>GHK</product>
<AMT>30</AMT>
</item>
<item>
<BILLNO>1</BILLNO>
<product>XYZ</product>
<AMT>20</AMT>
</item>
</customer>
produces the exact wanted, correct result:
<sales>
<totalbills>2</totalbills>
<totalamount>50</totalamount>
</sales>
Explanation: Appropriate use of
The Muenchian method for grouping.
The sum() function.