I am trying to fetch one field looking at another field using XSLT Maps - streaming

I am trying to use streaming (XSLT 3.0) to process the report output because of huge volume. I have to use XSLT because of specific output format. While transforming the report output, I need to lookup "Supplier_Invoice_number" based on a combination of Customer_Invoice and Supplier stored in "Customer_Invoice_and_Supplier" at the line level. I need to show "Supplier_Invoice_number" at the line level.
I have created XSLT maps to store the value of "Customer_Invoice_and_Supplier" and "Supplier_Invoice_number" so that while transforming the lines i can fetch "Supplier_Invoice_number" to then show at the line level in the output. Below is the XSLT I have created, but I am getting blank for "Supplier_Invoice_number". I am new to XSLT maps and streaming, therefore would really appreciate if someone can guide me to the solution.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:map="http://www.w3.org/2005/xpath-functions/map" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:wd="urn:com.workday/bsvc" xmlns:wd1="urn:com.workday.report/INT1109_CR_REV_Lookup_Supplier_Invoice_for_Customer_Invoice" xmlns:wd2="urn:com.workday.report/INT1109_CR_REV_Customer_Invoices_to_Connect" exclude-result-prefixes="xs" version="3.0">
<xsl:mode streamable="yes" on-no-match="shallow-skip" use-accumulators="SupplierInvoiceLookup CurrentLookupValue"/>
<xsl:output method="text"/>
<xsl:accumulator name="CurrentLookupValue" as="xs:string" initial-value="''" streamable="yes">
<xsl:accumulator-rule match="wd1:Customer_Invoice_and_Supplier/text()" select="."/>
</xsl:accumulator>
<xsl:accumulator name="SupplierInvoiceLookup" as="map(xs:string,xs:string)" initial-value="map{}" streamable="yes">
<xsl:accumulator-rule match="wd1:Supplier_Invoice_Number/text()" select="map:put($value, string(.), accumulator-before('CurrentLookupValue'))"/>
</xsl:accumulator>
<xsl:strip-space elements="*"/>
<xsl:template match="AggregatedData">
<xsl:for-each select="wd2:Report_Data/wd2:Report_Entry/copy-of()">
<xsl:text>{
"Company": "</xsl:text>
<xsl:value-of select="wd2:Company"/>
<xsl:iterate select="wd2:lines">
<xsl:text> {
</xsl:text>
<xsl:text> "sequence": "</xsl:text>
<xsl:value-of select="wd2:sequence"/>
<xsl:text>",
</xsl:text>
<xsl:text> "sales_item_id": "</xsl:text>
<xsl:value-of select="wd2:sales_item_id"/>
<xsl:text>",
</xsl:text>
<xsl:text> "supplier_invoice_no": "</xsl:text>
<xsl:variable name="supplier_invoice_no" select="accumulator-before('SupplierInvoiceLookup')( normalize-space( #wd2:Customer_Invoice_and_Supplier ) )"/>
<xsl:value-of select="accumulator-before('SupplierInvoiceLookup')( normalize-space( #wd2:Customer_Invoice_and_Supplier ) )"/>
<xsl:text>",
</xsl:text>
</xsl:iterate>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
Sample XML -
<?xml version="1.0" encoding="utf-8"?>
<AggregatedData>
<wd:Report_Data xmlns:wd="urn:com.workday.report/INT1109_CR_REV_Lookup_Supplier_Invoice_for_Customer_Invoice">
<wd:Report_Entry>
<wd:Supplier_Invoice_Lines_group>
<wd:Customer_Invoice_and_Supplier>INV-201900000024Dell Receivables LP</wd:Customer_Invoice_and_Supplier>
<wd:Supplier_Invoice_Number>SI-00000047</wd:Supplier_Invoice_Number>
</wd:Supplier_Invoice_Lines_group>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Supplier_Invoice_Lines_group>
<wd:Customer_Invoice_and_Supplier>INV-201900000024Charles Case [C]</wd:Customer_Invoice_and_Supplier>
<wd:Supplier_Invoice_Number>SI-00000050</wd:Supplier_Invoice_Number>
</wd:Supplier_Invoice_Lines_group>
</wd:Report_Entry>
</wd:Report_Data>
<wd:Report_Data xmlns:wd="urn:com.workday.report/INT1109_CR_REV_Customer_Invoices_to_Connect">
<wd:Report_Entry>
<wd:Company>Financial/wd:Company>
<wd:lines>
<wd:sequence>a8</wd:sequence>
<wd:sales_item_id>Data - Enterprise License</wd:sales_item_id>
<wd:Customer_Invoice_and_Supplier>INV-201900000024Dell Receivables LP</wd:Customer_Invoice_and_Supplier>
</wd:lines>
<wd:lines>
<wd:sequence>a9</wd:sequence>
<wd:sales_item_id>TBA Trade Reports ATS Cncl</wd:sales_item_id>
<wd:Customer_Invoice_and_Supplier>INV-201900000024Charles Case [C]</wd:Customer_Invoice_and_Supplier>
</wd:lines>
</wd:Report_Entry>
</wd:Report_Data>
</AggregatedData>

For some reason you have used #wd2:Customer_Invoice_and_Supplier to select an attribute although your input has element data, so I think you want
<xsl:template match="AggregatedData">
<xsl:for-each select="wd2:Report_Data/wd2:Report_Entry/copy-of()">
<xsl:text>{
"Company": "</xsl:text>
<xsl:value-of select="wd2:Company"/>
<xsl:iterate select="wd2:lines">
<xsl:text> {
</xsl:text>
<xsl:text> "sequence": "</xsl:text>
<xsl:value-of select="wd2:sequence"/>
<xsl:text>",
</xsl:text>
<xsl:text> "sales_item_id": "</xsl:text>
<xsl:value-of select="wd2:sales_item_id"/>
<xsl:text>",
</xsl:text>
<xsl:text> "supplier_invoice_no": "</xsl:text>
<xsl:value-of select="accumulator-before('SupplierInvoiceLookup')( normalize-space( wd2:Customer_Invoice_and_Supplier ) )"/>
<xsl:text>",
</xsl:text>
</xsl:iterate>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
And I think you want the map to be the other way around in terms of key and values so the accumulators would do
<xsl:accumulator name="CurrentLookupValue" as="xs:string" initial-value="''" streamable="yes">
<xsl:accumulator-rule match="wd1:Customer_Invoice_and_Supplier/text()" select="string()"/>
</xsl:accumulator>
<xsl:accumulator name="SupplierInvoiceLookup" as="map(xs:string,xs:string)" initial-value="map{}" streamable="yes">
<xsl:accumulator-rule match="wd1:Supplier_Invoice_Number/text()" select="map:put($value, accumulator-before('CurrentLookupValue'), string(.))"/>
</xsl:accumulator>
https://xsltfiddle.liberty-development.net/ncdD7mu
If you want to output JSON then note that XSLT 3 with XPath 3.1 maps and arrays and the output method json does allow that in a comfortable way without struggling to output pairs of braces and element names. But that might be better solved in a separate question where you show us the JSON you want.

Related

Counter generation following a range in xslt

I am trying to generate a counter and I do it successfully using the position() function in xslt.
How can I create a counter within a range -1001 - 9999 such that the first counter is 1001 in place of 1.
in put xml
<ROOT>
<Row>
<Var>11</Var>
</Row>
<Row>
<Var>11</Var>
</Row>
<Row>
<Var>3</Var>
</Row>
<Row>
<Var>43</Var>
</Row>
<Row>
<Var>51</Var>
</Row>
and the desired o/p is
<ROOT>
<Row>
<Var>11</Var>
<seq>1001</seq>
</Row>
<Row>
<Var>11</Var>
<seq>1002</seq>
</Row>
<Row>
<Var>3</Var>
<seq>1003</seq>
</Row>
<Row>
<Var>43</Var>
<seq>1004</seq>
</Row>
<Row>
<Var>51</Var>
<seq>1005</seq>
</Row>
..
Thanks,
Vicky
I achieved the req as below:
<xsl:template match="/">
<ROOT>
<xsl:apply-templates />
</ROOT>
</xsl:template>
<xsl:template match="ROOT/Row">
<Row>
<Var>
<xsl:value-of select="Var"/>
</Var>
<seq>
<xsl:value-of select="1000 + position()" />
</seq>
</Row>
</xsl:template>
Well, XPath 2 and later literally allows you to write 1000 to 9999 to create a sequence of those numbers. I am not sure how that fits into your problem as you haven't provided any context like input you have and output you want. In XSLT 3 (not 2) the xsl:number element often used to count or number things also has a start-at attribute, see https://www.w3.org/TR/xslt-30/#element-number.
As you seem to want to number elements from the input I think the right approach is to use xsl:number so with XSLT 3 you can simply do:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output indent="yes"/>
<xsl:template match="Row">
<xsl:copy>
<xsl:apply-templates/>
<seq>
<xsl:number start-at="1001"/>
</seq>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
http://xsltfiddle.liberty-development.net/jyyiVhq
with XSLT 2 you can of course use xsl:number in a variable and then at 1000 e.g.
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0"
exclude-result-prefixes="xs">
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:output indent="yes"/>
<xsl:template match="Row">
<xsl:copy>
<xsl:apply-templates/>
<seq>
<xsl:variable name="count" as="xs:integer"><xsl:number/></xsl:variable>
<xsl:value-of select="$count + 1000"/>
</seq>
</xsl:copy>
</xsl:template>
</xsl:transform>
http://xsltransform.hikmatu.com/jyyiVhn
I don't think it makes sense to use the to expression for your use case, it would rather be useful if you wanted to create a couple of new elements e.g.
<xsl:template match="body">
<xsl:copy>
<xsl:for-each select="1001 to 1100">
<seq>
<xsl:value-of select="."/>
</seq>
</xsl:for-each>
</xsl:copy>
</xsl:template>
Note that with XSLT 3.0 you can do
<seq>
<xsl:number start-at="1001" />
</seq>

exsl:node-set not retrieving attribute's value

Here is a toned down version of my use case. I have
XSL file for transformation
<?xml version="1.0"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
xmlns:exsl="http://exslt.org/common" extension-element-prefixes="exsl">
<xsl:output method="text"/>
<xsl:template match="Message">
<xsl:for-each select="ent">
<xsl:variable name="current_key" select="#key"/>
<xsl:variable name="current_type" select="#type"/>
<xsl:variable name="Match" select="exsl:node-set(msg)/ent"/>
<xsl:copy>
<xsl:copy-of select="exsl:node-set($Match)/#type"/>
<xsl:copy-of select="exsl:node-set($Match)/#key|exsl:node-set($Match)/translation/text()"/>
<!--- <xsl:copy-of select="exsl:node-set($Match)/#key|exsl:node-set($Match)/translation/text()|exsl:node-set($Match)/#type"/> Trial statement -->
</xsl:copy>
</xsl:for-each>
<xsl:call-template name = "Me" select="$Message"/>
</xsl:template>
</xsl:stylesheet>
And an input file as follows
<?xml version="1.0" encoding="utf-8"?>
<msg>
<ent key="key1" type="error">
<text>Error: Could not find </text>
<translation>Another Error similar to previous one.</translation>
</ent>
<ent key="key2" type="damage">
<text>Error2: Could not find2 </text>
<translation>Another Error2 similar to previous one.</translation>
</ent>
</msg>
I am using libXSLT in Perl as my transformation engine. My transformation script is already mentioned in this answer. Whenever I execute the script, I get the output as follows.
Error: Could not find
Another Error similar to previous one.
Error2: Could not find2
Another Error2 similar to previous one.
Why is the attribute type not getting printed? How do I retrieve it with the help of exsl:node-set or any other techniques? Also, can I include the attribute type in the trial statement in such a way that it will be in the output?
The following stylesheet:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/msg">
<xsl:for-each select="ent">
<xsl:text>KEY: </xsl:text>
<xsl:value-of select="#key"/>
<xsl:text>
TYPE: </xsl:text>
<xsl:value-of select="#type"/>
<xsl:text>
TEXT: </xsl:text>
<xsl:value-of select="text"/>
<xsl:text>
TRANSLATION: </xsl:text>
<xsl:value-of select="translation"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when applied to your input example, will produce:
KEY: key1
TYPE: error
TEXT: Error: Could not find
TRANSLATION: Another Error similar to previous one.
KEY: key2
TYPE: damage
TEXT: Error2: Could not find2
TRANSLATION: Another Error2 similar to previous one.

Sum only first occurrence on a node in XSLT

I'm running into a bit of a challenge. I'm working on summing the values contained in a child node without using a recursive template. I have a source similar to this:
<root>
<entry>
<recordID>001</recordID>
<amount>500</amount>
</entry>
<entry>
<recordID>001</recordID>
<amount>200</amount>
</entry>
<entry>
<recordID>002</recordID>
<amount>500</amount>
</entry>
<entry>
<recordID>003</recordID>
<amount>400</amount>
</entry>
<entry>
<recordID>004</recordID>
<amount>100</amount>
</entry>
</root>
What I'm trying to do is ONLY sum the FIRST occurrence of a recordID and count those nodes. I.e. I do not want to include the 2nd node. So the desired output would be
Record Count: 4
Total Amount: 1500
I've attempted to this, but it only returns 0. I'm not sure if it's because you can only reference siblings inside of a for each or not. but here was my first attempt:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:template match='root'>
<xsl:value-of select='"Record Count: "'/>
<xsl:value-of select='count(entry[recordID != following-sibling::recordID])'/>
<xsl:value-of select='"Total Amount: "'/>
<xsl:value-of select='sum(entry[recordID != following-sibling::recordID]/amount)'/>
</xsl:template>
</xsl:stylesheet>
But this just returned 0. I also tried this, but it was also unsuccessful
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:template match='root'>
<xsl:value-of select='"Record Count: "'/>
<xsl:for-each-group select='entry' group-by='.[recordID != following-sibling::recordID]'>
<xsl:value-of select='count(current-group())'/>
</xsl:for-each-group>
<xsl:value-of select='"Total Amount: "'/>
<xsl:for-each-group select='entry' group-by='.[recordID != following-sibling::recordID]'>
<xsl:value-of select='sum(current-group()/amount)'/>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
If anyone could offer some insight into how I could accomplish this, or corrections in my current code - I would greatly appreciate it!
Thanks in advance for any feedback
Here is an XSLT 2.0 example using Muechian grouping (with a slight adaption of simply using is instead of generate-id as XSLT 2.0 is assumed):
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:key name="entry-by-id" match="entry" use="recordID"/>
<xsl:variable name="first-records" select="root/entry[. is key('entry-by-id', recordID)[1]]"/>
<xsl:template match='root'>
<xsl:value-of select='"Record Count: "'/>
<xsl:value-of select='count($first-records)'/>
<xsl:value-of select='"Total Amount: "'/>
<xsl:value-of select='sum($first-records/amount)'/>
</xsl:template>
</xsl:stylesheet>
With for-each-group you can use an approach like
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:template match='root'>
<xsl:variable name="first-entries" as="element(entry)*">
<xsl:for-each-group select='entry' group-by='recordID'>
<xsl:copy-of select='.'/>
</xsl:for-each-group>
</xsl:variable>
<xsl:value-of select='"Record Count: "'/>
<xsl:value-of select='count($first-entries)'/>
<xsl:value-of select='"Total Amount: "'/>
<xsl:value-of select='sum($first-entries/amount)'/>
</xsl:template>
</xsl:stylesheet>
I've updated your first XSLT:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:template match='root'>
<xsl:value-of select='"Record Count: "'/>
<xsl:value-of select='count(entry[not(recordID = preceding::entry/recordID)])'/>
<xsl:value-of select='"Total Amount: "'/>
<xsl:value-of select='sum(entry[not(recordID = preceding::entry/recordID)]/amount)'/>
</xsl:template>
</xsl:stylesheet>
Your XPATH, entry[recordID != following-sibling::recordID] won't give you the distinct entry elements(distinct by their recordIDs), but my XPATH will.
Reason,
recordID != following-sibling::recordID
means that you are checking if any of the following-sibling recordIDs have a different value than the current recordID(at the first place, there are no recordID siblings to any of the recordIDs in your input).
On the other hand when the xpath is:
entry[not(recordID = preceding::entry/recordID)]
The current recordID is matched with any of the preceding recordIDs, and the ones that match are ignored. This way only entry elements are grouped by recordID.
I suggest you to read about how != operator works against not():
a != (b,c,d) and not(a = (b,c,d)) are not same.

How do we test whether a date is within 180 days from today in xslt in datapower

I am writing an xslt for datapower and in that I am getting a date(Payment Date).I have to check whether that date(Paymentt Date) is within 180 days of current date
I am getting present date by the following
<xsl:variable name="timestamp" select="date:date-time()"/>
Now how to check the condition for 180 days
Following is my xslt
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:dp="http://www.datapower.com/extensions"
xmlns:date="http://exslt.org/dates-and-times"
xmlns:dpconfig="http://www.datapower.com/param/config"
extension-element-prefixes="dp"
exclude-result-prefixes="dp dpconfig"
>
<xsl:output method="xml"/>
<xsl:template match="/">
<PaymentDate><xsl:value-of select="dp:http-request-header('X-payment-date')"/></PaymentDate> (From request I am getting payment date)
<xsl:variable name="timestamp" select="date:date-time()"/> (From here I am getting present date)
<xsl:if (this is what I am confused)
Thanks
Calculating date difference in XSLT 1.0:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:date="http://exslt.org/dates-and-times"
extension-element-prefixes="date">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/">
<output>
<difference>
<xsl:call-template name="date-difference">
<xsl:with-param name="date1" select="input/originalDate" />
<xsl:with-param name="date2" select="date:date-time()" />
</xsl:call-template>
</difference>
</output>
</xsl:template>
<xsl:template name="date-difference">
<xsl:param name="date1"/>
<xsl:param name="date2"/>
<xsl:param name="JDN1">
<xsl:call-template name="JDN">
<xsl:with-param name="date" select="$date1" />
</xsl:call-template>
</xsl:param>
<xsl:param name="JDN2">
<xsl:call-template name="JDN">
<xsl:with-param name="date" select="$date2" />
</xsl:call-template>
</xsl:param>
<xsl:value-of select="$JDN2 - $JDN1"/>
</xsl:template>
<xsl:template name="JDN">
<xsl:param name="date"/>
<xsl:param name="year" select="substring($date, 1, 4)"/>
<xsl:param name="month" select="substring($date, 6, 2)"/>
<xsl:param name="day" select="substring($date, 9, 2)"/>
<xsl:param name="a" select="floor((14 - $month) div 12)"/>
<xsl:param name="y" select="$year + 4800 - $a"/>
<xsl:param name="m" select="$month + 12*$a - 3"/>
<xsl:value-of select="$day + floor((153*$m + 2) div 5) + 365*$y + floor($y div 4) - floor($y div 100) + floor($y div 400) - 32045" />
</xsl:template>
</xsl:stylesheet>
When applied to the following input XML:
<input>
<originalDate>2013-09-15</originalDate>
</input>
The result will be:
<?xml version="1.0" encoding="UTF-8"?>
<output>
<difference>179</difference>
</output>
if the transformation is performed today, 2014-03-13.
IBM Datapower provides extension to existing EXSLT function. There is a function called 'difference' with following signature.
difference()
Returns the duration between the first date and the second date.
Syntax
date:difference(start-dateTime, end-dateTime)
You can utilize this function to find out the date-difference. For more information about extension function available in datapower see below link. Go to Chapter 5 and look for functions for manipulating 'date time'.
http://pic.dhe.ibm.com/infocenter/wsdatap/v3r8m1/topic/xm70/ExtensionFunctions.pdf
-Ajitabh
DataPower Supports Date:Difference
difference()
Returns the duration between the first date and the second date.
Syntax
date:difference(start-dateTime, end-dateTime)
You can pass current date as end-DateTime , date which wants to check as start-DateTime
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs"
xmlns:date="http://exslt.org/dates-and-times" version="1.0">
<!--<xsl:key name="lang" match="element" use="#language"></xsl:key>-->
<xsl:template match="/">
<xsl:variable name="date1" select="date:date-time()"/>
<xsl:variable name="date2" select="'2013-09-15'"/>
<output>
<difference>
<xsl:value-of select="translate(date:difference($date2, $date1),'PD','')"/>
</difference>
</output>
</xsl:template>
</xsl:stylesheet>

Convert date time format in XSLT

I have a date value in a field in XML file in this format:
<Date value="4/1/2013 5:13:41 PM"/>
I want to convert it into a standard XSD format:
2013-04-01T17:13:41.000Z
How can I do that in my XSL transform? I can use both 1.0 and 2.0 stylesheet versions.
So...I was bored and hadn't played with xsl:analyze-string before. Here's a regex-based solution :
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" version="2.0">
<xsl:template match="/item/date">
<xsl:analyze-string select="#value" regex="([0-9]+)/([0-9]+)/([0-9]+) ([0-9]+):([0-9]+):([0-9]+) (PM|AM)">
<xsl:matching-substring>
<xsl:variable name="month" select="number(regex-group(1))"/>
<xsl:variable name="day" select="number(regex-group(2))"/>
<xsl:variable name="year" select="number(regex-group(3))"/>
<xsl:variable name="hours">
<xsl:choose>
<xsl:when test="regex-group(7) = 'PM'">
<xsl:value-of select="12 + number(regex-group(4))"/>
</xsl:when>
<xsl:otherwise><xsl:value-of select="number(regex-group(4))"/></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="minutes" select="number(regex-group(5))"/>
<xsl:variable name="seconds" select="number(regex-group(6))"/>
<xsl:variable name="dateTime" select="xs:dateTime( concat($year, '-', format-number($month, '00'), '-', format-number($day, '00'), 'T', format-number($hours, '00'), ':', format-number($minutes, '00'), ':', format-number($seconds, '00'), 'Z') )" />
<reformattedDate>
<xsl:value-of select="$dateTime"/>
</reformattedDate>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:template>
</xsl:stylesheet>
I ran this against a test xml file like this :
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<item>
<date value="4/1/2013 5:13:41 PM"/>
</item>
And the output is this :
<?xml version="1.0" encoding="UTF-8"?>
<reformattedDate xmlns:xs="http://www.w3.org/2001/XMLSchema">2013-04-01T17:13:41Z</reformattedDate>
If you want to format the output more precisely, as already recommended, you can use the format-date function.
Here is an XSLT 1.0 version, using substring-before and substring-after.
Thanks to adhocgeek for the XML input.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/item">
<xsl:copy>
<xsl:apply-templates select="date"/>
</xsl:copy>
</xsl:template>
<xsl:template match="date">
<xsl:copy>
<xsl:variable name="date" select="substring-before(#value, ' ')"/>
<xsl:variable name="M" select="substring-before($date, '/')"/>
<xsl:variable name="D-Y" select="substring-after($date, '/')"/>
<xsl:variable name="D" select="substring-before($D-Y, '/')"/>
<xsl:variable name="Y" select="substring-after($D-Y, '/')"/>
<xsl:variable name="time-ampm" select="substring-after(#value, ' ')"/>
<xsl:variable name="time" select="substring-before($time-ampm, ' ')"/>
<xsl:variable name="ampm" select="substring-after($time-ampm, ' ')"/>
<xsl:variable name="h" select="substring-before($time, ':')"/>
<xsl:variable name="m-s" select="substring-after($time, ':')"/>
<xsl:variable name="m" select="substring-before($m-s, ':')"/>
<xsl:variable name="s" select="substring-after($m-s, ':')"/>
<xsl:variable name="hh">
<xsl:choose>
<xsl:when test="$ampm = 'PM'">
<xsl:value-of select="format-number($h + 12, '00')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="format-number($h, '00')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:value-of select="concat($Y, '-', $M, '-', $D, 'T', $hh, ':', $m, ':', $s)"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
XML
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<item>
<date value="4/1/2013 5:13:41 PM"/>
</item>
output
<?xml version="1.0" encoding="utf-8"?>
<item>
<date>2013-4-1T05:13:41</date>
</item>
Have a look at ESXLT. Many XSL implementations already support the functions out of the box, and they provide fallbacks in case yours does not support it.
If you have fully-compliant implementation, you can do this:
<?xml ...
xmlns.date="http://exslt.org/dates-and-times"
...
<xsl:variable name="xsdate" select="date:parse-date(Date/#value, 'M/d/yyyy h:mm:ss a')" />
<xsl:value-of select="$xsdate" />
I cheated and used the result of date:parse directly. One often uses the date:format function to format the date into a particular format.
I've honestly been struggling with getting the "fallback" functions from EXSLT working for me in Apache Xalan. (Using non-core EXSLT date functions with Xalan Java)
IMO it would be better to have the date in xs:dateTime format in the first place, and convert to some localized format afterward. You can use the date:format function to format the date as necessary, and the date:format function enjoys much wider support amongst XSLT processors.
Using XSLT 2.0 you can construct an xs:dateTime value by extracting the components from Date/#value using string functions, then you can format if needed using format-dateTime: http://www.w3.org/TR/xslt20/#format-date. Extracting the date components like year, month, day should be straightforward, obviously for the time you need to some more work to pay attention to the PM or AM 12-hour clock.
#adhockgeek's answer worked almost perfectly for me. I only had to add a condition so that it not add 12 to the hour if the value was currently 12. IE, 12:15 PM != 24:15 AM.
I created a template from his code that can be called repeatedly within a transform:
<xsl:template name="FormatDate">
<xsl:param name="dt"/>
<xsl:analyze-string select="$dt" regex="([0-9]+)/([0-9]+)/([0-9]+) ([0-9]+):([0-9]+):([0-9]+) (PM|AM)">
<xsl:matching-substring>
<xsl:variable name="month" select="number(regex-group(1))"/>
<xsl:variable name="day" select="number(regex-group(2))"/>
<xsl:variable name="year" select="number(regex-group(3))"/>
<xsl:variable name="hours">
<xsl:choose>
<xsl:when test="regex-group(7) = 'PM' and regex-group(4) != '12'">
<xsl:value-of select="12 + number(regex-group(4))"/>
</xsl:when>
<xsl:otherwise><xsl:value-of select="number(regex-group(4))"/></xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:variable name="minutes" select="number(regex-group(5))"/>
<xsl:variable name="seconds" select="number(regex-group(6))"/>
<xsl:variable name="dateTime" select="xs:dateTime(concat($year, '-', format-number($month, '00'), '-', format-number($day, '00'), 'T', format-number($hours, '00'), ':', format-number($minutes, '00'), ':', format-number($seconds, '00')))" />
<xsl:value-of select="$dateTime"/>
</xsl:matching-substring>
</xsl:analyze-string>
</xsl:template>
Code with substring-before and substring-after works, but more checks should be added:
12AM => 00 and 12PM => 12 something like this:
<xsl:variable name="hh">
<xsl:choose>
<xsl:when test="$ampm = 'AM' and $h='12'">00</xsl:when>
<xsl:when test="$ampm = 'PM' and $h!='12'">
<xsl:value-of select="format-number($h + 12, '00')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="format-number($h, '00')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>