Getting invalid date issue while comparing dates in XSLT code - xpath

I have a requirement wherein I have to validate couple of scenarios: the offer start date should fall before offer end date and the offer start date account should fall after account start date. If any of the scenario is not met error should be thrown.
Offer start date and offer end date values will appear in space separated formats in xml tag and xml tags respectively.
Below is the sample xml code:
<Accounts>
<Account>
<AccountStartDate>2020-12-01<AccountStartDate>
<offerStartDate>2020-10-02 2020-11-02</offerStartDate>
<offerEndDate>2019-10-02 2019-11-02</offerEndDate>
</Account>
</Accounts>
Below is the sample xslt code:
<xsl:for-each select="Accounts/Account">
<xsl:variable name="offerSDate" select="offerStartDate"/>
<xsl:variable name="offerEDate" select="offerEndDate"/>
<xsl:if test="$offerSDate > xs:date(AccountStartDate)">
<Error>
<xsl:text>Error: Invalid offer Date
</xsl:text>
</Error>
</xsl:if>
<xsl:if test="$offerSDate > $offerEDate">
<Error>
<xsl:text>Error: Invalid offer Date
</xsl:text>
</Error>
</xsl:if>
</xsl:for-each>
After execution of the xslt code, I am getting the invalid date "2020-10-02 2020-11-02""issue.

If you want to do a separate comparison for each date in offerStartDate, then you could do (in XSLT 2.0) either:
<xsl:for-each select="Account">
<xsl:if test="some $offerStartDate in tokenize(offerStartDate, ' ') satisfies xs:date($offerStartDate) gt xs:date(AccountStartDate)">
<Error>error message</Error>
</xsl:if>
</xsl:for-each>
or (depending on what meaning your test should have):
<xsl:for-each select="Account">
<xsl:if test="every $offerStartDate in tokenize(offerStartDate, ' ') satisfies xs:date($offerStartDate) gt xs:date(AccountStartDate)">
<Error>error message</Error>
</xsl:if>
</xsl:for-each>

Probably the easiest way to do it with only XSLT is to convert your XML from:
<Accounts>
<Account>
<AccountStartDate>2020-12-01</AccountStartDate>
<offerStartDate>2020-10-02 2020-11-02</offerStartDate>
<offerEndDate>2019-10-02 2019-11-02</offerEndDate>
</Account>
</Accounts>
To something like:
<Accounts>
<Account>
<AccountStartDate>2020-12-01</AccountStartDate>
<offer>
<offerStartDate>2020-10-02</offerStartDate>
<offerEndDate>2019-10-02</offerEndDate>
</offer>
<offer>
<offerStartDate>2020-11-02</offerStartDate>
<offerEndDate>2019-11-02</offerEndDate>
</offer>
</Account>
</Accounts>

Related

How to select name and get value from <ul><li> with xpath?

I'm looking for an easier way to select a value after a name in an html ul li string.
Data is:
<xsl:value-of select="custom_options/custom_option/value" />
Result: <ul><li><strong>Breedte (mm):</strong> 2080</li><li><strong>Hoogte (mm) incl. Kast:</strong> 1420</li><li><strong>Kastmaat:</strong> 150</li><li><strong>Type kast:</strong> Afgeschuind</li><li><strong>Kastkleur:</strong> RAL 6009 Spargroen</li></ul>
Now I use a substring solution.
<xsl:value-of select="substring-before(substring-after(custom_options/custom_option/value, 'Breedte (mm):</strong>'), '</li>')"/>
<xsl:value-of select="substring-before(substring-after(custom_options/custom_option/value, 'Type kast:</strong>'), '</li>')"/>
I would like an xpath solution like:
<xsl:value-of select="#Breedte (mm):" /> 2080
<xsl:value-of select="#Type kast:" /> Afgeschuind
I am using xsl 1.0
I would use this:
<xsl:value-of select="//ul/li/text()[preceding-sibling::strong='Breedte (mm):']"/>
and this:
<xsl:value-of select="//ul/li/text()[preceding-sibling::strong='Type kast:']"/>

How to get sum of an attribute value which is referenced by id multiple times with xpath in xslt 1.0?

I really do hope that my title is at least a bit clear.
important: i can only use xslt 1.0 because the project needs to work with the MSXML XSLT processor.
What I try to do:
I generate documents containing information about rooms. Rooms have walls, I need the sum of wall area of these per room.
The input xml file I get is dynamically created by another program.
Changing the structure of the input xml file is not the solution, trust me, it's needed like that and is much more complex than I show you here.
My XML (the innerArea attribute in the wall element has to get summed up):
<root>
<floor id="30" name="EG">
<flat name="Wohnung" nr="1">
<Room id="49" area="93.08565">
<WallSegments>
<WallSegment id="45"/>
<WallSegment id="42"/>
<WallSegment id="39"/>
</WallSegments>
</Room>
</flat>
</floor>
<components>
<Wall id="20" innerArea="20.7654"/>
<wallSegment id="45" wall="20">[...]</wallSegment>
<Wall id="21" innerArea="12.45678"/>
<wallSegment id="42" wall="21">[...]</wallSegment>
<Wall id="22" innerArea="17.8643"/>
<wallSegment id="39" wall="22">[...]</wallSegment>
</components>
</root>
With my XSLT I was able to reach the values of the walls which belong to a room.
But I have really no idea how I could get the sum of the value out of that.
My XSLT:
<xsl:for-each select="flat/Room">
<xsl:for-each select="WallSegments/WallSegment">
<xsl:variable name="curWallSegId" select="#id"/>
<xsl:for-each select="/root/components/wallSegment[#id = $curWallSegId]">
<xsl:variable name="curWallId" select="#wall"/>
<xsl:for-each select="/root/components/Wall[#id = $curWallId]">
<!--I didn't expect that this was working, but at least I tried :D-->
<xsl:value-of select="sum(#AreaInner)"/>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
Desired Output should be something like...
[...]
<paragraph>
Room 1:
Wall area: 51.09 m²
[...]
</paragraph>
[...]
So I hope I described my problem properly. If not: I am sorry, you may beat me right into the face x)
It's best to use keys to get "related" data. Place this at the top of your stylesheet, outside of any template:
<xsl:key name="wall" match="components/Wall" use="#id" />
<xsl:key name="wallSegment" match="components/wallSegment" use="#id" />
Then:
<xsl:for-each select="flat/Room">
<paragraph>
<xsl:text>Room </xsl:text>
<xsl:value-of select="position()"/>
<xsl:text>:
Wall area: </xsl:text>
<xsl:value-of select="format-number(sum(key('wall', key('wallSegment', WallSegments/WallSegment/#id)/#wall)/#innerArea), '0.00m²')"/>
<xsl:text>
</xsl:text>
</paragraph>
</xsl:for-each>
will return:
<paragraph>Room 1:
Wall area: 51.09m²</paragraph>
If what you need it's the area of every room, this is a way of getting it:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="/root/floor">
<xsl:for-each select="flat/Room">
<xsl:variable name="currentRoomSegmentsIds" select="WallSegments/WallSegment/#id"/>
<xsl:variable name="currentRoomWallsIds" select="/root/components/wallSegment[#id = $currentRoomSegmentsIds]/#wall"/>
<xsl:variable name="currentRoomWallsInnerAreas" select="/root/components/Wall[#id = $currentRoomWallsIds]/#innerArea"/>
Id of the room = <xsl:value-of select="#id"/>.
Area of the room = <xsl:value-of select="sum($currentRoomWallsInnerAreas)"/>
</xsl:for-each> <!-- Enf of for each room -->
</xsl:template>
</xsl:stylesheet>
This produces the following result:
Id of the room = 49.
Area of the room = 51.08648

XSLT concatenate input from several nodes in a single output

I'm trying to work out a transformation that will process an input with several Flights with Departure and Arrival into a single output with the complete route for the flights.
Input is as follows:
<FlightTrip>
<flights>
<departureAirport>
<airportCode>LocB</airportCode>
</departureAirport>
<departureTime>2013-03-28T10:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocC</airportCode>
</arrivalAirport>
</flights>
<flights>
<departureAirport>
<airportCode>LocA</airportCode>
</departureAirport>
<departureTime>2013-03-27T15:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocB</airportCode>
</arrivalAirport>
</flights>
<flights>
<departureAirport>
<airportCode>LocC</airportCode>
</departureAirport>
<departureTime>2013-03-30T14:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocD</airportCode>
</arrivalAirport>
</flights>
</FlightTrip>
The desired output would be this:
<FullTrip>LocA LocB LocC LocD</FullTrip>
I've tried to use foreach inside the output variable but I can't get it right. I also need to sort the input based on the departure date as the Flights can be in a different order (as per the sample input).
Any ideas of how to achieve this?
Thanks a lot!
Bruno
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="FlightTrip">
<FullTrip>
<xsl:apply-templates select="flights">
<xsl:sort select="departureTime"/>
</xsl:apply-templates>
</FullTrip>
</xsl:template>
<xsl:template match="flights">
<xsl:value-of select="departureAirport/airportCode"/><xsl:text> </xsl:text>
<xsl:if test="position()=last()">
<xsl:value-of select="arrivalAirport/airportCode"/>
</xsl:if>
</xsl:template>
</xsl:transform>
Will produce:
<FullTrip>LocA LocB LocC LocD</FullTrip>
Working example
Thanks to Joepie for the enlightenment. I had to modify it a bit to get it to work in my environment, ended up using foreach as below:
<xsl:template match="/">
<xsl:variable name="locations">
<xsl:for-each select="/FlightTrip/flights">
<xsl:sort select="departureTime" order="ascending" data-type="text"/>
<xsl:value-of select="concat(departureAirport/airportCode,' - ')"/>
<xsl:if test="position() = last()">
<xsl:value-of select="arrivalAirport/airportCode"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<FullTrip>
<xsl:value-of select="$locations"/>
</FullTrip>
</xsl:template>
When applied to the example produces the output below:
<FullTrip>LocA - LocB - LocC - LocD</FullTrip>
Thanks again!

Longer node in XPath

I'd like to use XPath to retrieve the longer of two nodes.
E.g., if my XML is
<record>
<url1>http://www.google.com</url1>
<url2>http://www.bing.com</url2>
</record>
And I do document.SelectSingleNode(your XPath here)
I would expect to get back the url1 node. If url2 is longer, or there is no url1 node, I'd expect to get back the url2 node.
Seems simple but I'm having trouble figuring it out. Any ideas?
This works for me, but it is ugly. Cannot you do the comparison outside XPath?
record/*[starts-with(name(),'url')
and string-length(.) > string-length(preceding-sibling::*[1])
and string-length(.) > string-length(following-sibling::*[1])]/text()
<xsl:for-each select="*">
<xsl:sort select="string-length(.)" data-type="number"/>
<xsl:if test="position() = last()">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
Even works in XSLT 1.0!
Use this single XPath expression:
/*/*[not(string-length(preceding-sibling::*|following-sibling::*)
>
string-length()
)
]
XSLT - based verification:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<xsl:copy-of select=
"/*/*[not(string-length(preceding-sibling::*|following-sibling::*)
>
string-length()
)
]"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<record>
<url1>http://www.google.com</url1>
<url2>http://www.bing.com</url2>
</record>
the Xpath expression is evaluated and the result of this evaluation (the selected element) is copied to the output:
<url1>http://www.google.com</url1>

XSLT - How to speed up a complex for-each

I am new to XSLT and i'm having a few speed issues with the following for-each statement. I was hoping someone could give me some pointers as how to optimise this please?
The for-each below is looping through about 4mb of XML. It is testing to ensure that each hotel node has a description and a destination. It is also testing that each hotel has a rating greater than 2 but not 6. The possible values for the rating in the XML are 0, 1, 2, 3, 4, 5 or 6. Ideally i would like it to only select ratings 3, 4 or 5 and ignore the others.
<for-each select="response/results/hotel[
not(#description = '') and
#rating > '2' and
not(#rating = '6') and
not(#destination = '') ]">
<call-template name="hotelparams"/>
<call-template name="upropdata"/>
<call-template name="request"/>
<call-template name="Newline"/>
</for-each>
As request I have added the templates that are being called below. The output is creating tab delimited text files which are then imported in mySQL. By the way please ignore the upropdata template, it will be removed shortly...
<xsl:template name="hotelparams">
<xsl:value-of select="#itemcode"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#cheapestcurrency"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#cheapestprice"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#checkin"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#checkout"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#description"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#destair"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#destination"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#destinationid"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#engine"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#hotelname"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#image"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#nights"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#rating"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#resultkey"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#resultno"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#supplierdestination"/><xsl:value-of select="$tab"/>
<xsl:value-of select="#type"/></xsl:template>
<xsl:template name="upropdata">
<xsl:value-of select="$tab"/>\N<xsl:value-of select="$tab"/>\N<xsl:value-of select="$tab"/>\N<xsl:value-of select="$tab"/>\N<xsl:value-of select="$tab"/>\N<xsl:value-of select="$tab"/>2011-01-01</xsl:template>
<xsl:template name="request">
<xsl:for-each select="/response/request/method"><xsl:value-of select="$tab"/><xsl:value-of select="./#sessionkey"/></xsl:for-each></xsl:template>
<xsl:template name="Newline">
<xsl:text>
</xsl:text></xsl:template>
How about ...
<xsl:for-each select="response/results/hotel
[not(#description = '')]
[#rating = (3,4,5)]">
<xsl:call-template name="hotelparams"/>
<xsl:call-template name="upropdata"/>
<xsl:call-template name="request"/>
<xsl:call-template name="Newline"/>
</xsl:for-each>
Note: I have not included a check for destination, because you did not specify its node name.
Also, if you can eliminate the possibility of empty description attributes (that is to say hotels will have a non empty description or no description attribute at all), then you can use this slightly abbreviated form...
<xsl:for-each select="response/results/hotel
[not(#description)]
[#rating = (3,4,5)]">
<xsl:call-template name="hotelparams"/>
etc...
</xsl:for-each>
Also note, an alternate form for the second predicate would be...
[#rating = (3 to 5)]
One could write...
[(#rating > 2) and (#rating < 6)]
or
[#rating > 2][#rating < 6]
... but I suspect that this would be less efficient, because #rating would have to be fetched twice.
The for-each below is looping through about 4mb of XML. It is testing
to ensure that each hotel node has a description and a destination. It
is also testing that each hotel has a rating greater than 2 but not 6.
The possible values for the rating in the XML are 0, 1, 2, 3, 4, 5 or
6. Ideally i would like it to only select ratings 3, 4 or 5 and ignore the others.
<for-each select="response/results/hotel[
not(#description = '') and
#rating > '2' and
not(#rating = '6') and
not(#destination = '') ]">
<call-template name="hotelparams"/>
<call-template name="upropdata"/>
<call-template name="request"/>
<call-template name="Newline"/>
</for-each>
I believe that the reason for the performance problem is in the templates that are being called (and not provided in the question) -- not in the xsl:for-each itself.
It can be re-written in different alternative ways, but the performance gains would be minimal (milliseconds), if any at all.
Do note, that the provided code doesn't check for the existence of a #destination attribute at all. Any hotel element that satisfies the other conditions, but has no destination attribute is selected.
Exactly the same is true for the description attribute.
One correct way of specifying the xsl:for-each is:
<xsl:for-each select="response/results/hotel[
string(#description)
and
#rating > 2
and
not(#rating > 5)
and
string(#destination)
]">
<xsl:call-template name="hotelparams"/>
<xsl:call-template name="upropdata"/>
<xsl:call-template name="request"/>
<xsl:call-template name="Newline"/>
</xsl:for-each>
Update:
The OP has now provided the code of the called templates.
I will use the following for the hotelparams template:
<xsl:sequence select=
"string-join
(
(#itemcode,
#cheapestcurrency,
#cheapestprice,
#checkin,
#checkout,
#description,
#destair,
#destination,
#destinationid,
#engine,
#hotelname,
#image,
#nights,
#rating,
#resultkey,
#resultno,
#supplierdestination,
#type),
$tab
)
"/>
I would replace the template upropdata with:
this code:
<xsl:sequence select="' \N \N \N \N \N2011-01-01'"/>
Or, if $tab really can be something different than , I will calculate this only once and place the result in a global variable:
<xsl:variable name="vUPropData" select=
"concat($tab,'\N',$tab,'\N',$tab,'\N'$tab,'\N',$tab,'\N2011-01-01')"/>
and then just have:
<xsl:sequence select="$vUPropData"/>
I would replace the request template with:
this code:
<xsl:sequence select=
"concat($tab,string-join(/response/request/method/#sessionkey, $tab))"/>
As this doesn't depend on any context node (is an absolute expression), I would calculate this only once and put it in a global variable (as in the previous case) and only reference this global variable.
Finally, it is not meaningful to generate the same single character in a named template. I will replace the Newline template with a global variable or with a global parameter.
I believe that after this refactoring, the code might execute significantly faster.

Resources