How to get row count and total sum of fields value through XSLT? - xslt

I have a file format requirement as following where I need to get row count and total sum of the field. In the example there are three rows with the column that has amount values and in the fourth row, i would need total row count and sum of the amounts to be populated.
Alex, Scot 10091978 90.00
Brad, Ander 23061976 40.00
Rodney, Philips 04091983 35.00
03 165.00
XML Input
<wd:Report_Data
xmlns:wd="urn:com.workday.report/INT001_Test_Reward_Report">
<wd:Report_Entry>
<wd:Worker_group>
<wd:Employee_ID>12344</wd:Employee_ID>
<wd:dateOfBirth>1975-05-06</wd:dateOfBirth>
<wd:lastName>Alex</wd:lastName>
<wd:firstName>Scot</wd:firstName>
<wd:Pay_Frequency>M1</wd:Pay_Frequency>
</wd:Worker_group>
<wd:Payroll_Result_group>
<wd:Payment_Date>2019-04-02</wd:Payment_Date>
</wd:Payroll_Result_group>
<wd:Payroll_Result_Line_group>
<wd:Result_Line_Amount>40</wd:Result_Line_Amount>
</wd:Payroll_Result_Line_group>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Worker_group>
<wd:Employee_ID>12355</wd:Employee_ID>
<wd:dateOfBirth>1958-05-06</wd:dateOfBirth>
<wd:lastName>Hyrus</wd:lastName>
<wd:firstName>Marike</wd:firstName>
<wd:Pay_Frequency>M1</wd:Pay_Frequency>
</wd:Worker_group>
<wd:Payroll_Result_group>
<wd:Payment_Date>2019-04-04</wd:Payment_Date>
</wd:Payroll_Result_group>
<wd:Payroll_Result_Line_group>
<wd:Result_Line_Amount>10</wd:Result_Line_Amount>
</wd:Payroll_Result_Line_group>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Worker_group>
<wd:Employee_ID>12366</wd:Employee_ID>
<wd:dateOfBirth>1986-09-10</wd:dateOfBirth>
<wd:lastName>Elite</wd:lastName>
<wd:firstName>Samsung</wd:firstName>
<wd:Pay_Frequency>M1</wd:Pay_Frequency>
</wd:Worker_group>
<wd:Payroll_Result_group>
<wd:Payment_Date>2019-04-02</wd:Payment_Date>
</wd:Payroll_Result_group>
<wd:Payroll_Result_Line_group>
<wd:Result_Line_Amount>30</wd:Result_Line_Amount>
</wd:Payroll_Result_Line_group>
</wd:Report_Entry>
</wd:Report_Data>
XSLT I have tried so far
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wd="urn:com.workday.report/INT001_Test_Reward_Report"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:this="urn:this-stylesheet"
version="2.0" exclude-result-prefixes="xs this">
<!--**************************************************************************************-->
<!-- Date XSLT Attachment Design -->
<!-- 06-17-2019 Transformation to transform file format from xml to .txt(fixed ) -->
<!--**************************************************************************************-->
<!-- 06-17-2019 Initial Version -->
<!--**************************************************************************************-->
<!--The final output file will be text formatted with an encoding of *NA*-->
<xsl:output encoding="UTF-8" method="text" use-character-maps="custom-char-filter" />
<!--declare global variables here such as delimiter and linefeed-->
<xsl:variable name="vDelimiter" select="''"/>
<xsl:variable name="linefeed" select="'
'" />
<xsl:variable name="linefeed2" select="'
'"/>
<xsl:variable name="vWrapQuotes" select="false()"/>
<xsl:variable name="vDateFormat" select="'[Y0001][M01][D01]'"/>
<xsl:variable name="vDateTimeFormat" select="'[D01]-[MN,*-3]-[Y0001]T[H01]:[m01]:[s01]'"/>
<xsl:variable name="pDateValue" select="current-date()"/>
<xsl:variable name="SumAmount" select="0"/>
<xsl:variable name="i" select="0"/>
<!--Root Template-->
<xsl:template match="wd:Report_Data">
<!--declare root variables here such as current datetime or other repeating values-->
<xsl:value-of select="this:setOutput(this:formatDate($pDateValue))" />
<xsl:value-of select="$linefeed"/>
<!--Process detail records-->
<xsl:apply-templates select="wd:Report_Entry" />
<xsl:text>T</xsl:text>
<xsl:value-of select="$i"/>
<xsl:value-of select="$SumAmount"/>
</xsl:template>
<!--Detail Record Template-->
<xsl:template match="wd:Report_Entry">
<!--
Detail Record
-->
<!--Record Type - -->
<xsl:text>D</xsl:text>
<xsl:value-of select="this:setOutput(this:fixedWidth(wd:Worker_group/wd:Employee_ID, ' ', 8, 'left'))" />
<xsl:value-of select="this:setOutput(this:fixedWidth((this:formatDate(wd:Worker_group/wd:dateOfBirth)), ' ', 8, 'left'))" />
<xsl:value-of select="this:setOutput(this:fixedWidth(wd:Worker_group/wd:lastName, ' ', 40,'left'))" />
<xsl:value-of select="this:setOutput(this:fixedWidth(wd:Worker_group/wd:firstName, ' ', 40, 'left'))" />
<xsl:value-of select="this:setOutput(this:fixedWidth(wd:Payroll_Result_Line_group/wd:Result_Line_Amount, ' ', 9, 'left'))" />
<xsl:value-of select="this:setOutput(this:fixedWidth(wd:Worker_group/wd:Pay_Frequency, ' ', 2, 'left'))" />
<xsl:value-of select="this:setOutput(this:fixedWidth((this:formatDate(wd:Payroll_Result_group/wd:Payment_Date)), ' ', 8, 'left'))" />
<xsl:variable name="Amount" select="this:setOutput(wd:Payroll_Result_Line_group/wd:Result_Line_Amount)"/>
<xsl:value-of select="$Amount"/>
<xsl:variable name="i" select="position()"/>
<xsl:value-of select="$i"/>
<xsl:variable name="SumAmount" select="$SumAmount + $Amount"/>
<xsl:value-of select="$linefeed" />
</xsl:template>
<!--******************************************************************************************** -->
<!--This function returns the input value followed by a delmiter -->
<!--******************************************************************************************** -->
<xsl:function name="this:setOutput">
<xsl:param name="value" />
<xsl:value-of select="this:setOutput($value,false())"/>
</xsl:function>
<!--******************************************************************************************** -->
<!--This function returns the input value followed by a delmiter -->
<!--******************************************************************************************** -->
<xsl:function name="this:setOutput">
<xsl:param name="value" />
<xsl:param name="finalValue" as="xs:boolean"/>
<xsl:variable name="vWrappedValue">
<xsl:value-of select="if($vWrapQuotes = false()) then $value else this:wrapQuotes($value)"/>
</xsl:variable>
<xsl:value-of select="if ($finalValue = false()) then concat(string($vWrappedValue),$vDelimiter) else $vWrappedValue"/>
</xsl:function>
<!--******************************************************************************************** -->
<!--This function is designed to wrap a string in quotes if required -->
<!--******************************************************************************************** -->
<xsl:function name="this:wrapQuotes">
<xsl:param name="field"/>
<xsl:value-of select="'"'"/>
<xsl:value-of select="replace($field,'"','""')"/>
<xsl:value-of select="'"'"/>
</xsl:function>
<!--******************************************************************************************** -->
<!--This function is designed to pad a string to the left or right to a fixed number of characters-->
<!-- str: a string input -->
<!-- chr: a character used for padding -->
<!-- len: a number of defining the total length of the output -->
<!-- dir: the direction of the alignment default is left; right if specified) -->
<!-- Note: if something is aligned left it will be padded on the right and vice versa -->
<!--******************************************************************************************** -->
<xsl:function name="this:padString">
<xsl:param name="str" />
<xsl:param name="chr" />
<xsl:param name="len" />
<xsl:param name="dir" />
<xsl:variable name="padLength">
<xsl:value-of select="$len - string-length($str)" />
</xsl:variable>
<xsl:variable name="pad">
<xsl:for-each select="1 to $padLength">
<xsl:value-of select="$chr" />
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="if ($dir = 'right') then concat($str,$pad) else concat($pad,$str)" />
</xsl:function>
<!--******************************************************************************************** -->
<!--This function is designed to align a string to the left or right to a fixed number of characters-->
<!-- str: a string input -->
<!-- chr: a character used for padding -->
<!-- len: a number of defining the total length of the output -->
<!-- dir: the direction of the alignment default is left; right if specified) -->
<!-- Note: if something is aligned left it will be padded on the right and vice versa -->
<!--******************************************************************************************** -->
<xsl:function name="this:fixedWidth">
<xsl:param name="str" />
<xsl:param name="chr" />
<xsl:param name="len" />
<xsl:param name="dir" />
<xsl:variable name="padLength">
<xsl:value-of select="$len - string-length($str)" />
</xsl:variable>
<xsl:variable name="pad">
<xsl:for-each select="1 to $padLength">
<xsl:value-of select="$chr" />
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="if ($dir = 'right') then substring(concat($pad,$str),1,$len) else substring(concat($str,$pad),1,$len)" />
</xsl:function>
<!--******************************************************************************************** -->
<!--This function can be modified as needed for the appropriate date format -->
<!--******************************************************************************************** -->
<xsl:function name="this:formatDate">
<xsl:param name="pDateValue" />
<xsl:value-of select="if (xs:string($pDateValue) = '') then '' else format-date(xs:date($pDateValue), $vDateFormat)" />
</xsl:function>
<xsl:function name="this:formatDateTime">
<xsl:param name="pDateTimeValue" />
<xsl:value-of select="if ($pDateTimeValue = '') then '' else format-dateTime(xs:dateTime($pDateTimeValue),$vDateTimeFormat )" />
</xsl:function>
<!--************************************************************************************************* -->
<!--Character Maps can be used to replace special and/or foreign characters where appropriate -->
<!--The big avantage is that these don't have to be called explicitly, the processor will handle this -->
<!--while writing the output. -->
<!--************************************************************************************************* -->
<xsl:character-map name="custom-char-filter">
<xsl:output-character character="*" string="" />
<xsl:output-character character=":" string="" />
<xsl:output-character character="^" string="" />
<xsl:output-character character="~" string="" />
</xsl:character-map>
</xsl:stylesheet>
I have also tried with few xslt functions (Total count, total row count),
but nothing seems working.
Thanks,
Jithendra.

Consider the following simplified example:
XML
<wd:Report_Data xmlns:wd="urn:com.workday.report/INT001_Test_Reward_Report">
<wd:Report_Entry>
<wd:Worker_group>
<wd:lastName>Alex</wd:lastName>
<wd:firstName>Scot</wd:firstName>
</wd:Worker_group>
<wd:Payroll_Result_Line_group>
<wd:Result_Line_Amount>90</wd:Result_Line_Amount>
</wd:Payroll_Result_Line_group>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Worker_group>
<wd:lastName>Brad</wd:lastName>
<wd:firstName>Ander</wd:firstName>
</wd:Worker_group>
<wd:Payroll_Result_Line_group>
<wd:Result_Line_Amount>40</wd:Result_Line_Amount>
</wd:Payroll_Result_Line_group>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Worker_group>
<wd:lastName>Rodney</wd:lastName>
<wd:firstName>Philips</wd:firstName>
</wd:Worker_group>
<wd:Payroll_Result_Line_group>
<wd:Result_Line_Amount>35</wd:Result_Line_Amount>
</wd:Payroll_Result_Line_group>
</wd:Report_Entry>
</wd:Report_Data>
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:wd="urn:com.workday.report/INT001_Test_Reward_Report">
<xsl:output method="text" encoding="UTF-8" />
<xsl:template match="/wd:Report_Data">
<!-- records -->
<xsl:for-each select="wd:Report_Entry">
<xsl:value-of select="wd:Worker_group/wd:lastName"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="wd:Worker_group/wd:firstName"/>
<xsl:text> </xsl:text>
<xsl:value-of select="wd:Payroll_Result_Line_group/wd:Result_Line_Amount"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<!-- summary -->
<xsl:value-of select="count(wd:Report_Entry)"/>
<xsl:text> </xsl:text>
<xsl:value-of select="sum(wd:Report_Entry/wd:Payroll_Result_Line_group/wd:Result_Line_Amount)"/>
</xsl:template>
</xsl:stylesheet>
Result
Alex, Scot 90
Brad, Ander 40
Rodney, Philips 35
3 165

Related

XSLT Help - Compare preceding node and counter increment if the date is different

I have xml where each worker can have multiple events. The events need not be grouped but if there are two events with same Effective_Date, I'd like to increment the sequence. Below is sample xml:
XML:
<?xml version="1.0" encoding="UTF-8"?>
<wd:Report_Data xmlns:wd="urn:com.workday.report/ERP-HCM-CR_ASSIGNMENTS_REPORT_V4">
<wd:Report_Entry>
<wd:Worker_group>
<wd:UNIQUE_IDENTIFIER>2168636</wd:UNIQUE_IDENTIFIER>
</wd:Worker_group>
<wd:Caregiver_Events_group>
<wd:ASSIGNMENT_NUMBER>2168636-03012008E01</wd:ASSIGNMENT_NUMBER>
<wd:EFFECTIVE_DATE>2008-03-01-08:00</wd:EFFECTIVE_DATE>
<wd:EFFECTIVE_SEQUENCE>1</wd:EFFECTIVE_SEQUENCE>
</wd:Caregiver_Events_group>
<wd:Caregiver_Events_group>
<wd:ASSIGNMENT_NUMBER>2168636-03012008E01</wd:ASSIGNMENT_NUMBER>
<wd:EFFECTIVE_DATE>2019-12-01-08:00</wd:EFFECTIVE_DATE>
<wd:EFFECTIVE_SEQUENCE>1</wd:EFFECTIVE_SEQUENCE>
</wd:Caregiver_Events_group>
<wd:Caregiver_Events_group>
<wd:ASSIGNMENT_NUMBER>2168636-03012008E01</wd:ASSIGNMENT_NUMBER>
<wd:EFFECTIVE_DATE>2019-12-01-08:00</wd:EFFECTIVE_DATE>
<wd:EFFECTIVE_SEQUENCE>1</wd:EFFECTIVE_SEQUENCE>
</wd:Caregiver_Events_group>
</wd:Report_Entry>
<wd:Report_Entry>
<wd:Worker_group>
<wd:UNIQUE_IDENTIFIER>2188946</wd:UNIQUE_IDENTIFIER>
</wd:Worker_group>
<wd:Caregiver_Events_group>
<wd:ASSIGNMENT_NUMBER>2188946-04272015E01</wd:ASSIGNMENT_NUMBER>
<wd:EFFECTIVE_DATE>2015-04-27-07:00</wd:EFFECTIVE_DATE>
<wd:EFFECTIVE_SEQUENCE>1</wd:EFFECTIVE_SEQUENCE>
</wd:Caregiver_Events_group>
<wd:Caregiver_Events_group>
<wd:COUNTRY_CODE>US</wd:COUNTRY_CODE>
<wd:ASSIGNMENT_NUMBER>2188946-04272015E01</wd:ASSIGNMENT_NUMBER>
<wd:EFFECTIVE_DATE>2019-12-01-08:00</wd:EFFECTIVE_DATE>
<wd:EFFECTIVE_SEQUENCE>1</wd:EFFECTIVE_SEQUENCE>
</wd:Caregiver_Events_group>
<wd:Caregiver_Events_group>
<wd:ASSIGNMENT_NUMBER>2188946-04272015E01</wd:ASSIGNMENT_NUMBER>
<wd:EFFECTIVE_DATE>2019-12-01-08:00</wd:EFFECTIVE_DATE>
<wd:EFFECTIVE_SEQUENCE>1</wd:EFFECTIVE_SEQUENCE>
</wd:Caregiver_Events_group>
</wd:Report_Entry>
</wd:Report_Data>
Portion of xslt:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="xs"
xmlns:wd="urn:com.workday.report/ERP-HCM-CR_ASSIGNMENTS_REPORT_V4" version="2.0">
<xsl:output method="text"/>
<xsl:variable name="linefeed" select="'
'"/>
<xsl:variable name="pipe" select="'|'"/>
<xsl:variable name="EffectiveStartDate" select="'01-01-1951'"/>
<xsl:variable name="EffectiveEndDate" select="'4712-12-31'"/>
<xsl:variable name="i" select="position()"/>
<xsl:param name="quote">"</xsl:param>
<xsl:template match="/">
<!-- File Header Record -->
<xsl:call-template name="Write-Header-Record0"/>
<!-- File Detail Layout -->
<xsl:for-each select="wd:Report_Data/wd:Report_Entry">
<xsl:for-each select="wd:Caregiver_Events_group">
<xsl:call-template name="Write-Detail-Record"/>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
<xsl:template name="Write-Header-Record0">
<xsl:text>Unique ID|Assignment Number|Effective Date|Sequence</xsl:text>
<xsl:value-of select="$linefeed"/>
</xsl:template>
<xsl:template name="Write-Detail-Record">
<xsl:value-of select="../wd:Worker_group/wd:UNIQUE_IDENTIFIER"/>
<xsl:value-of select="$pipe"/>
<xsl:value-of select="wd:ASSIGNMENT_NUMBER"/>
<xsl:value-of select="$pipe"/>
<xsl:value-of select="format-date(wd:EFFECTIVE_DATE, '[Y0001]-[M01]-[D01]')"/>
<xsl:value-of select="$pipe"/>
<xsl:choose>
<xsl:when
test="(preceding-sibling::node()[wd:EFFECTIVE_DATE = current()/wd:EFFECTIVE_DATE])">
<xsl:value-of
select="preceding-sibling::node()/wd:EFFECTIVE_SEQUENCE + current()/wd:EFFECTIVE_SEQUENCE"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="wd:EFFECTIVE_SEQUENCE"/>
</xsl:otherwise>
</xsl:choose>
<xsl:value-of select="$linefeed"/>
</xsl:template>
</xsl:stylesheet>
Can someone please assist. I know the value-of select cannot add two elements. If a worker (report_entry) has three events (caregiver_events_group) with same effective date, I would expect to see sequence of 1, 2, 3 on three rows.
Expecting output:
2168636|2168636-03012008E01|2008-03-01-08:00|1
2168636|2168636-03012008E01|2019-12-01-08:00|1
2168636|2168636-03012008E01|2019-12-01-08:00|2
2188946|2188946-04272015E01|2015-04-27-07:00|1
2188946|2188946-04272015E01|2019-12-01-08:00|1
2188946|2188946-04272015E01|2019-12-01-08:00|2
Would something like this work for you:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xpath-default-namespace="urn:com.workday.report/ERP-HCM-CR_ASSIGNMENTS_REPORT_V4">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/Report_Data">
<!-- header -->
<xsl:text>Unique ID|Assignment Number|Effective Date|Sequence
</xsl:text>
<!-- data -->
<xsl:for-each select="Report_Entry">
<xsl:variable name="id" select="Worker_group/UNIQUE_IDENTIFIER" />
<xsl:for-each-group select="Caregiver_Events_group" group-by="EFFECTIVE_DATE">
<xsl:variable name="date" select="format-date(EFFECTIVE_DATE, '[Y0001]-[M01]-[D01]')"/>
<xsl:for-each select="current-group()">
<xsl:value-of select="$id"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="ASSIGNMENT_NUMBER"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="$date"/>
<xsl:text>|</xsl:text>
<xsl:value-of select="position()"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:for-each-group>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Count the total of preceding siblings and comments

I have many files that resemble the following a.xml file, although they are much larger:
<?xml version="1.0" encoding="UTF-8"?>
<a version="3.0">
<b bb="P1">
<!--============== b:P1 c:1 ==============-->
<c cc="1">
<d dd="61">d1
</d>
</c>
<!--============== b:P1 c:2 ==============-->
<c cc="2">
<d dd="17">d2
</d>
</c>
</b>
</a>
For each c there is only one preceding comment.
I want to output a file with the same structure of the following a.csv file:
1|1|a|0| |0| |0|
2|1|a|1|b|0| |0|
3|1|a|1|b|1|!|0|
3|1|a|1|b|2|c|0|
4|1|a|1|b|2|c|1|d
3|1|a|1|b|3|!|0|
3|1|a|1|b|4|c|0|
4|1|a|1|b|4|c|1|d
It represents the hierarchical tree for a.xml:
Field 1 is the hierarchical level. For instance a has level 1, b has level 2, etc.
Fields 2, 4, 6 and 8 are equal to:
if the current node's level is less than the current field's level then 0
else the total number of preceding siblings and comments plus one
Field 3, 5, 7 and 9 are equal to:
if the current node's level is less than the current field's level then " "
else either "!" if the current node is preceded by a comment or the node's name
In this example level 3 contains comments.
I cannot find a good way to do a for-each that includes both nodes and comments. When I use <xsl:for-each select="*"> I only loop through the nodes.
Because of that, I've come out with the following xslt, that checks if the current node is preceded by a comment:
<?xml version="1.0"?>
<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="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<xsl:for-each select="*">
<xsl:variable name="elm01" select="local-name()" />
<xsl:text>1</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|0| |0| |0|</xsl:text>
<xsl:text>
</xsl:text>
<xsl:for-each select="*">
<xsl:variable name="elm02" select="local-name()" />
<xsl:text>2</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<xsl:text>|0| |0|</xsl:text>
<xsl:text>
</xsl:text>
<xsl:for-each select="*">
<xsl:variable name="elm03" select="local-name()" />
<xsl:if test="preceding-sibling::comment()[1]">
<xsl:text>3</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*)+1"/>
<xsl:text>|!</xsl:text>
<xsl:text>|0|</xsl:text>
<xsl:text>
</xsl:text>
</xsl:if>
<xsl:text>3</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<!-- TODO: I want to count the total of preceding siblings and comments -->
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*|comment())+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm03" />
<xsl:text>|0|</xsl:text>
<xsl:text>
</xsl:text>
<xsl:for-each select="*">
<xsl:variable name="elm04" select="local-name()" />
<xsl:text>4</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm03" />
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm04" />
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However when I run the following command:
xsltproc a.xslt a.xml > a.csv
I get the following a.csv file:
1|1|a|0| |0| |0|
2|1|a|1|b|0| |0|
3|1|a|1|b|1|!|0|
3|1|a|1|b|1|c|0|
4|1|a|1|b|1|c|1|d
3|1|a|1|b|2|!|0|
3|1|a|1|b|2|c|0|
4|1|a|1|b|2|c|1|d
Please notice that field 6 is incorrect:
it is equal to 1 both for the 1st comment and the 1st node c and its children
it is equal to 2 both for the 2nd comment and the 2nd node c and its children
Do you have any solutions to suggest?
SOLUTION (by Tim)
I can now get the correct output by using the following xslt file:
<?xml version="1.0"?>
<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="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="/">
<xsl:for-each select="*">
<xsl:variable name="elm01" select="local-name()" />
<xsl:text>1</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|0| |0| |0|</xsl:text>
<xsl:text>
</xsl:text>
<xsl:for-each select="*">
<xsl:variable name="elm02" select="local-name()" />
<xsl:text>2</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<xsl:text>|0| |0|</xsl:text>
<xsl:text>
</xsl:text>
<xsl:for-each select="*">
<xsl:variable name="elm03" select="local-name()" />
<xsl:if test="preceding-sibling::comment()[1]">
<xsl:text>3</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*|preceding-sibling::comment())"/>
<xsl:text>|!</xsl:text>
<xsl:text>|0|</xsl:text>
<xsl:text>
</xsl:text>
</xsl:if>
<xsl:text>3</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*|preceding-sibling::comment())+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm03" />
<xsl:text>|0|</xsl:text>
<xsl:text>
</xsl:text>
<xsl:for-each select="*">
<xsl:variable name="elm04" select="local-name()" />
<xsl:text>4</xsl:text>
<xsl:text>|</xsl:text><xsl:value-of select="count(../../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm01" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../../preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm02" />
<xsl:text>|</xsl:text><xsl:value-of select="count(../preceding-sibling::*|../preceding-sibling::comment())+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm03" />
<xsl:text>|</xsl:text><xsl:value-of select="count(preceding-sibling::*)+1"/>
<xsl:text>|</xsl:text><xsl:copy-of select="$elm04" />
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Alternatively you can also use the xslt in Tim's reply, that gets rid of repetitions.
There expression you want is this...
<xsl:value-of select="count(preceding-sibling::*|preceding-sibling::comment()) + 1" />
Or this would work too...
<xsl:value-of select="count(preceding-sibling::node()[self::*|self::comment()]) + 1" />
But you can also use xsl:number
<xsl:number count="*|comment()" />
Your stylesheet does seem a bit over-complicated though, with much repetition. Try this more generic one instead. This recursively calls each level, passing in the constructed line each call to save having to build it each time.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text" />
<xsl:param name="maxLevel" select="4" />
<xsl:template match="*|comment()">
<xsl:param name="level" select="1" />
<xsl:param name="prev" />
<xsl:variable name="new">
<xsl:value-of select="$prev" />
<xsl:text>|</xsl:text>
<xsl:number count="*|comment()" />
<xsl:text>|</xsl:text>
<xsl:choose>
<xsl:when test="self::*">
<xsl:value-of select="local-name()" />
</xsl:when>
<xsl:otherwise>
<xsl:text>!</xsl:text>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
<xsl:value-of select="$level" />
<xsl:value-of select="$new" />
<xsl:call-template name="pad">
<xsl:with-param name="levels" select="$maxLevel - $level" />
</xsl:call-template>
<xsl:text>
</xsl:text>
<xsl:apply-templates select="*|comment()">
<xsl:with-param name="level" select="$level + 1" />
<xsl:with-param name="prev" select="$new" />
</xsl:apply-templates>
</xsl:template>
<xsl:template name="pad">
<xsl:param name="levels" />
<xsl:if test="$levels > 0">
<xsl:text>|0| </xsl:text>
<xsl:call-template name="pad">
<xsl:with-param name="levels" select="$levels - 1" />
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
See it in action at http://xsltfiddle.liberty-development.net/jyRYYiy

XSL : Replace value inside tag

Input:
<Remarks>Random data## B2B## abc,controls,free text ## B2B## random data</Remarks>
The XSL should replace
"## B2B## abc,controls,free text ## B2B##"
in the Remarks tag with
"value1:abc,value2:controls,value3:free text"
Desired output:
<Remarks>Random data,value1:abc,value2:controls,value3:free text,random data</Remarks>
Note : the values inside ## B2B## tags are unknown and change everytime, for now I gave the sample values.
Assuming there will always be exactly one "placeholder" delimited by ## B2B##, and that it will always contain exactly 3 comma-delimited "tokens", you could do this quite simply by:
<xsl:template match="Remarks">
<xsl:copy>
<xsl:value-of select="substring-before(., '## B2B##')" />
<xsl:variable name="placeholder" select="substring-before(substring-after(., '## B2B##'), '## B2B##')" />
<xsl:text> value1: </xsl:text>
<xsl:value-of select="substring-before($placeholder, ',')" />
<xsl:text> value2: </xsl:text>
<xsl:value-of select="substring-before(substring-after($placeholder, ','), ',')" />
<xsl:text> value3: </xsl:text>
<xsl:value-of select="substring-after(substring-after($placeholder, ','), ',')" />
<xsl:value-of select="substring-after(substring-after(., '## B2B##'), '## B2B##')" />
</xsl:copy>
</xsl:template>
This solution is somewhat complex. It uses one template to extract the string between the tags and another to separate this string by the delimiter. What string is replaced by what other string is outsourced to another file containing the mapping (replacement.xml). Because XSLT-1.0 returns RTFs(Resulting Tree Fragments) in variables, the replacement is hardcoded in the template. In XSLT-2.0 you could use a variable for that making the solution more flexible.
Here is the replacement.xml containing the mappings:
<?xml version="1.0" encoding="UTF-8"?>
<replacement>
<r src="abc">value1</r>
<r src="controls">value2</r>
<r src="free text">value3</r>
</replacement>
And the XSLT incorporating these mappings into the string:
<?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="/lines/Remarks">
<xsl:variable name="tagName" select="'## B2B##'" />
<xsl:variable name="subStrToReplace">
<xsl:call-template name="strInTag">
<xsl:with-param name="str" select="text()" />
<xsl:with-param name="tagName" select="$tagName" />
</xsl:call-template>
</xsl:variable>
<xsl:variable name="replacementString">
<xsl:call-template name="splitAndReplace">
<xsl:with-param name="pText" select="$subStrToReplace" />
</xsl:call-template>
</xsl:variable>
<Remarks>
<xsl:value-of select="concat(normalize-space(substring-before(text(),$tagName)), ',',$replacementString, normalize-space(substring-after(substring-after(text(),$tagName),$tagName)))" />
</Remarks>
</xsl:template>
<xsl:template name="strInTag">
<xsl:param name="str" />
<xsl:param name="tagName" />
<xsl:value-of select="normalize-space(substring-before(substring-after($str,$tagName),$tagName))" />
</xsl:template>
<xsl:template name="splitAndReplace">
<xsl:param name="pText"/>
<xsl:if test="string-length($pText) > 0">
<xsl:variable name="vNextItem" select="substring-before(concat($pText, ','), ',')"/>
<xsl:value-of select="concat($vNextItem,':',document('replacement.xml')/replacement/r[#src = $vNextItem]/text(),',')"/>
<xsl:call-template name="splitAndReplace">
<xsl:with-param name="pText" select="substring-after($pText, ',')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>

XSL to capture URL Params

Not able to extract the params using XSLT,
for eg.: http://www.example.com/AB/100/123456/09/8
using XSLT need to extract like var1=AB, Var2=100, Var3=123456, var4=09, var5=8, All the params are mandatory. and var3 can accept 1-999999, could somebody share some ideas
tried Substring but it didn't help much
Though the input is not clear, just as example: given an input XML
<root>
<url>http://www.example.com/AB/100/123456/09/8</url>
</root>
following XSLT
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" omit-xml-declaration="yes" encoding="UTF-8" indent="yes" />
<xsl:strip-space elements="*" />
<xsl:template match="/">
<xsl:apply-templates />
</xsl:template>
<xsl:template match="url">
<xsl:call-template name="parse">
<xsl:with-param name="url" select="substring-after(.,'//')" />
<xsl:with-param name="position" select="1" />
</xsl:call-template>
</xsl:template>
<xsl:template name="parse">
<xsl:param name="url" />
<xsl:param name="position" />
<xsl:choose>
<xsl:when test="contains(substring-after($url, '/'),'/')">
<xsl:value-of select="concat('Var', $position, ': ')" />
<xsl:value-of select="substring-before(substring-after($url, '/'),'/')" />
<xsl:text>, </xsl:text>
<xsl:call-template name="parse">
<xsl:with-param name="url" select="substring-after($url, '/')" />
<xsl:with-param name="position" select="$position + 1" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat('Var', $position, ': ')" />
<xsl:value-of select="substring-after($url,'/')" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:transform>
produces the ouput
Var1: AB, Var2: 100, Var3: 123456, Var4: 09, Var5: 8
using the template parse which recursively calls itself when the url contains a /.
For every call the parameter url is reduced using substring-after() and the parameter position is incremented.

Using XSLT to generate nodes based on pipe-delimited attributes

(first off: I'm terribly sorry that you have to look at this document structure; it's hideous)
I have the following XML document:
<MENUS>
<MENU id="192748" servedate="20120213" mealid="3" mealname="Lunch" menuname="Cafeteria" menuid="26" totalcount="200">Cafeteria</MENU>
<RECIPES>
<NUTRIENTS>Calories~Energy (kcal)~kcal|Protein~Protein~gm|Fat~Fat~gm|Carbs~Total Carbohydrates~gm|Cholestrol~Cholesterol~mg|Calcium~Calcium~mg|Sodium~Sodium~mg|Iron~Iron~mg|</NUTRIENTS>
<RECIPE id="6461-200" plucode="" shortname="Chipotle Spinach" numservings="100" portion="4 ounces" isselected="0" ismainitem="0" group="On the Side" publishingdescription="Chipotle Spinach" publishingtext="" enticingdescription="" price="1.53" category="Vegetables" productionarea="Hot Production" nutrients="152|2.3|13.8|6.5|0|74|346|1.85|" nutrientsuncertain="0|0|0|0|0|0|0|0|">Chipotle Spinach,4U</RECIPE>
<RECIPE id="6586-300" plucode="" shortname="Asiago Crusted Chix" numservings="120" portion="3-3/4 ounces" isselected="0" ismainitem="0" group="Main Fare" publishingdescription="Asiago Crusted Chicken" publishingtext="" enticingdescription="" price="2.25" category="Chicken" productionarea="Hot Production" nutrients="203|19.6|7.6|13.2|56|124|387|1.37|" nutrientsuncertain="0|0|0|0|0|0|0|0|">Asiago Crusted Chicken,4U</RECIPE>
<!-- any number of <RECIPE> elements ... -->
</RECIPES>
</MENUS>
The <NUTRIENTS> element contains a pipe-delimited string; the components of this string need to somehow become elements for each <RECIPE>. Furthermore, the values of these new elements are specified by looking at the corresponding position within the pipe-delimited string found in <RECIPE>\<nutrients>.
The overall structure I'm shooting for is:
All of the attributes for a <RECIPE> element are converted into child elements.
The elements of <RECIPE>/<nutrients> map to the same position
within the <NUTRIENTS> element.
Using XSLT 1.0.
So, here would be my expected structure:
<?xml version="1.0"?>
<MENUS>
<MENU id="192748" servedate="20120213" mealid="3" mealname="Lunch" menuname="Cafeteria" menuid="26" totalcount="200">Cafeteria</MENU>
<RECIPES>
<NUTRIENTS>Calories~Energy (kcal)~kcal|Protein~Protein~gm|Fat~Fat~gm|Carbs~Total Carbohydrates~gm|Cholestrol~Cholesterol~mg|Calcium~Calcium~mg|Sodium~Sodium~mg|Iron~Iron~mg|</NUTRIENTS>
<RECIPE>
<id>6461-200</id>
<plucode/>
<shortname>Chipotle Spinach</shortname>
<numservings>100</numservings>
<portion>4 ounces</portion>
<isselected>0</isselected>
<ismainitem>0</ismainitem>
<group>On the Side</group>
<publishingdescription>Chipotle Spinach</publishingdescription>
<publishingtext/>
<enticingdescription/>
<price>1.53</price>
<category>Vegetables</category>
<productionarea>Hot Production</productionarea>
<nutrients>152|2.3|13.8|6.5|0|74|346|1.85|</nutrients>
<nutrientsuncertain>0|0|0|0|0|0|0|0|</nutrientsuncertain>
<CaloriesEnergykcalkcal>152</CaloriesEnergykcalkcal>
<ProteinProteingm>2.3</ProteinProteingm>
<FatFatgm>13.8</FatFatgm>
<CarbsTotalCarbohydrates>6.5</CarbsTotalCarbohydrates>
<CholestrolCholestrolmg>0</CholestrolCholestrolmg>
<CalciumCalciummg>74</CalciumCalciummg>
<SodiumSodiummg>346</SodiumSodiummg>
<IronIronmg>1.85</IronIronmg>
</RECIPE>
<RECIPE>
<id>6586-300</id>
<plucode/>
<shortname>Asiago Crusted Chix</shortname>
<numservings>120</numservings>
<portion>3-3/4 ounces</portion>
<isselected>0</isselected>
<ismainitem>0</ismainitem>
<group>Main Fare</group>
<publishingdescription>Asiago Crusted Chicken</publishingdescription>
<publishingtext/>
<enticingdescription/>
<price>2.25</price>
<category>Chicken</category>
<productionarea>Hot Production</productionarea>
<nutrients>203|19.6|7.6|13.2|56|124|387|1.37|</nutrients>
<nutrientsuncertain>0|0|0|0|0|0|0|0|</nutrientsuncertain>
<CaloriesEnergykcalkcal>203</CaloriesEnergykcalkcal>
<ProteinProteingm>19.6</ProteinProteingm>
<FatFatgm>7.6</FatFatgm>
<CarbsTotalCarbohydrates>13.2</CarbsTotalCarbohydrates>
<CholestrolCholestrolmg>56</CholestrolCholestrolmg>
<CalciumCalciummg>124</CalciumCalciummg>
<SodiumSodiummg>387</SodiumSodiummg>
<IronIronmg>1.37</IronIronmg>
</RECIPE>
<!-- ... -->
</RECIPES>
</MENUS>
(notice, again, that I don't care about the field names we use for these new data points [which begin after <nutrientsuncertain>]; however, bonus points if you would like to show me how to relatively easily specify some sort of array, for lack of a better term, of field names)
Here's my current XSLT, which achieves goal #1; it's goal #2 that I'm stumped on:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="no" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- Template #1 - Identity Transform -->
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<!-- Template #2 - Convert all of a <RECIPE> element's attributes to child elements -->
<xsl:template match="RECIPE/#*">
<xsl:element name="{name()}">
<xsl:value-of select="." />
</xsl:element>
</xsl:template>
<!-- Template #3 - Remove extraneous text from each <RECIPE> -->
<xsl:template match="RECIPE/text()" />
</xsl:stylesheet>
That's it. Thanks so much for your help!
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="exsl"
version="1.0">
<xsl:output method="xml" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="*|#*|text()">
<xsl:copy>
<xsl:apply-templates select="*|#*|text()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="RECIPE">
<xsl:copy>
<xsl:apply-templates select="#*"/>
<xsl:variable name="nutrients-table-tmp">
<xsl:call-template name="tokenize-table">
<xsl:with-param name="text" select="../NUTRIENTS/text()"/>
<xsl:with-param name="delimiter-row" select="'|'"/>
<xsl:with-param name="delimiter-col" select="'~'"/>
</xsl:call-template>
</xsl:variable>
<xsl:variable name="nutrients-table" select="exsl:node-set($nutrients-table-tmp)/table"/>
<xsl:variable name="nutrients">
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="#nutrients"/>
<xsl:with-param name="delimiter" select="'|'"/>
</xsl:call-template>
</xsl:variable>
<xsl:for-each select="exsl:node-set($nutrients)/token">
<xsl:variable name="pos" select="position()"/>
<xsl:variable name="value" select="text()"/>
<xsl:variable name="row" select="$nutrients-table/row[$pos]"/>
<xsl:variable name="name" select="$row/cell[1]/text()"/>
<xsl:variable name="description" select="$row/cell[2]/text()"/>
<xsl:variable name="unit" select="$row/cell[3]/text()"/>
<xsl:element name="{$name}">
<xsl:attribute name="unit">
<xsl:value-of select="$unit"/>
</xsl:attribute>
<xsl:value-of select="$value"/>
</xsl:element>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:template match="RECIPE/#*">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="text"/>
<xsl:param name="delimiter" select="' '"/>
<xsl:choose>
<xsl:when test="contains($text,$delimiter)">
<token>
<xsl:value-of select="substring-before($text,$delimiter)"/>
</token>
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="substring-after($text,$delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="$text">
<token>
<xsl:value-of select="$text"/>
</token>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template name="tokenize-table">
<xsl:param name="text"/>
<xsl:param name="delimiter-row"/>
<xsl:param name="delimiter-col"/>
<xsl:variable name="rows">
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="$text"/>
<xsl:with-param name="delimiter" select="$delimiter-row"/>
</xsl:call-template>
</xsl:variable>
<table>
<xsl:for-each select="exsl:node-set($rows)/token">
<xsl:variable name="items">
<xsl:call-template name="tokenize">
<xsl:with-param name="text" select="text()"/>
<xsl:with-param name="delimiter" select="$delimiter-col"/>
</xsl:call-template>
</xsl:variable>
<row>
<xsl:for-each select="exsl:node-set($items)/token">
<cell>
<xsl:value-of select="text()"/>
</cell>
</xsl:for-each>
</row>
</xsl:for-each>
</table>
</xsl:template>
</xsl:stylesheet>
Output:
<?xml version="1.0" encoding="utf-8"?>
<MENUS>
<MENU id="192748" servedate="20120213" mealid="3" mealname="Lunch" menuname="Cafeteria" menuid="26" totalcount="200">Cafeteria</MENU>
<RECIPES>
<NUTRIENTS>Calories~Energy (kcal)~kcal|Protein~Protein~gm|Fat~Fat~gm|Carbs~Total Carbohydrates~gm|Cholestrol~Cholesterol~mg|Calcium~Calcium~mg|Sodium~Sodium~mg|Iron~Iron~mg|</NUTRIENTS>
<RECIPE>
<id>6461-200</id>
<plucode/>
<shortname>Chipotle Spinach</shortname>
<numservings>100</numservings>
<portion>4 ounces</portion>
<isselected>0</isselected>
<ismainitem>0</ismainitem>
<group>On the Side</group>
<publishingdescription>Chipotle Spinach</publishingdescription>
<publishingtext/>
<enticingdescription/>
<price>1.53</price>
<category>Vegetables</category>
<productionarea>Hot Production</productionarea>
<nutrients>152|2.3|13.8|6.5|0|74|346|1.85|</nutrients>
<nutrientsuncertain>0|0|0|0|0|0|0|0|</nutrientsuncertain>
<Calories unit="kcal">152</Calories>
<Protein unit="gm">2.3</Protein>
<Fat unit="gm">13.8</Fat>
<Carbs unit="gm">6.5</Carbs>
<Cholestrol unit="mg">0</Cholestrol>
<Calcium unit="mg">74</Calcium>
<Sodium unit="mg">346</Sodium>
<Iron unit="mg">1.85</Iron>
</RECIPE>
<RECIPE>
<id>6586-300</id>
<plucode/>
<shortname>Asiago Crusted Chix</shortname>
<numservings>120</numservings>
<portion>3-3/4 ounces</portion>
<isselected>0</isselected>
<ismainitem>0</ismainitem>
<group>Main Fare</group>
<publishingdescription>Asiago Crusted Chicken</publishingdescription>
<publishingtext/>
<enticingdescription/>
<price>2.25</price>
<category>Chicken</category>
<productionarea>Hot Production</productionarea>
<nutrients>203|19.6|7.6|13.2|56|124|387|1.37|</nutrients>
<nutrientsuncertain>0|0|0|0|0|0|0|0|</nutrientsuncertain>
<Calories unit="kcal">203</Calories>
<Protein unit="gm">19.6</Protein>
<Fat unit="gm">7.6</Fat>
<Carbs unit="gm">13.2</Carbs>
<Cholestrol unit="mg">56</Cholestrol>
<Calcium unit="mg">124</Calcium>
<Sodium unit="mg">387</Sodium>
<Iron unit="mg">1.37</Iron>
</RECIPE>
</RECIPES>
</MENUS>
I am not 100% sure I understand your question, but I think you should take a look at the XSL tokenize function. If you combine this in a variable with the position() function you should be able to achieve that correlated output?
To further add to this, you can combine the name() with a (replace for 2.0/translate for 1.0) to get the element name automatically) within a for-each, extracting the positions.
Refer my implementation:-
XSLT File:
<?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" version="1.0" encoding="UTF-8" indent="yes"/>
</xsl:stylesheet>-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="no" indent="yes"/>
<xsl:strip-space elements="*"/>
<!-- Template #1 - Identity Transform -->
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<!-- Template #2 - Convert all of a <RECIPE> element's attributes to child elements -->
<xsl:template match="RECIPE/#*">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
<!-- Template #3 - Remove extraneous text from each <RECIPE> -->
<xsl:template match="RECIPE/text()"/>
<!-- Identifying the last attribute -->
<xsl:template match="RECIPE/#*[position()=last()]">
<xsl:element name="{name()}">
<xsl:value-of select="."/>
</xsl:element>
<!--- Call the String Tokenize template -->
<xsl:call-template name="tokenize">
<xsl:with-param name="string" select="/MENUS/RECIPES/NUTRIENTS/text()"/>
<xsl:with-param name="strValue" select="/MENUS/RECIPES/RECIPE/#nutrients"/>
</xsl:call-template>
</xsl:template>
<!--- String Tokenize -->
<xsl:template name="tokenize">
<xsl:param name="string"/>
<xsl:param name="strValue"/>
<xsl:param name="delimiter" select="'|'"/>
<xsl:choose>
<xsl:when test="$delimiter and contains($string, $delimiter) and contains($strValue, $delimiter)">
<xsl:variable name="subbef" select="translate(substring-before($string, $delimiter), '()~ ', '')"/>
<xsl:text disable-output-escaping="yes"><</xsl:text>
<xsl:value-of select="$subbef"/>
<xsl:text disable-output-escaping="yes">></xsl:text>
<xsl:value-of select="substring-before($strValue, $delimiter)"/>
<xsl:text disable-output-escaping="yes"></</xsl:text>
<xsl:value-of select="$subbef"/>
<xsl:text disable-output-escaping="yes">></xsl:text>
<xsl:call-template name="tokenize">
<xsl:with-param name="string" select="translate(substring-after($string, $delimiter), '()~ ', '')"/>
<xsl:with-param name="strValue" select="substring-after($strValue, $delimiter)"/>
<xsl:with-param name="delimiter" select="$delimiter"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:if test="string($string) and string($strValue)">
<xsl:text disable-output-escaping="yes"><</xsl:text>
<xsl:value-of select="$string"/>
<xsl:text disable-output-escaping="yes">></xsl:text>
<xsl:value-of select="$strValue"/>
<xsl:text disable-output-escaping="yes"></</xsl:text>
<xsl:value-of select="$string"/>
<xsl:text disable-output-escaping="yes">></xsl:text>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
OUTPUT:
<?xml version="1.0" encoding="UTF-8"?>
<MENUS>
<MENU id="192748" servedate="20120213" mealid="3" mealname="Lunch" menuname="Cafeteria" menuid="26" totalcount="200">Cafeteria</MENU>
<RECIPES>
<NUTRIENTS>Calories~Energy (kcal)~kcal|Protein~Protein~gm|Fat~Fat~gm|Carbs~Total Carbohydrates~gm|Cholestrol~Cholesterol~mg|Calcium~Calcium~mg|Sodium~Sodium~mg|Iron~Iron~mg|</NUTRIENTS>
<RECIPE>
<id>6461-200</id>
<plucode />
<shortname>Chipotle Spinach</shortname>
<numservings>100</numservings>
<portion>4 ounces</portion>
<isselected>0</isselected>
<ismainitem>0</ismainitem>
<group>On the Side</group>
<publishingdescription>Chipotle Spinach</publishingdescription>
<publishingtext />
<enticingdescription />
<price>1.53</price>
<category>Vegetables</category>
<productionarea>Hot Production</productionarea>
<nutrients>152|2.3|13.8|6.5|0|74|346|1.85|</nutrients>
<nutrientsuncertain>0|0|0|0|0|0|0|0|</nutrientsuncertain>
<CaloriesEnergykcalkcal>152</CaloriesEnergykcalkcal>
<ProteinProteingm>2.3</ProteinProteingm>
<FatFatgm>13.8</FatFatgm>
<CarbsTotalCarbohydratesgm>6.5</CarbsTotalCarbohydratesgm>
<CholestrolCholesterolmg>0</CholestrolCholesterolmg>
<CalciumCalciummg>74</CalciumCalciummg>
<SodiumSodiummg>346</SodiumSodiummg>
<IronIronmg>1.85</IronIronmg>
</RECIPE>
<RECIPE>
<id>6586-300</id>
<plucode />
<shortname>Asiago Crusted Chix</shortname>
<numservings>120</numservings>
<portion>3-3/4 ounces</portion>
<isselected>0</isselected>
<ismainitem>0</ismainitem>
<group>Main Fare</group>
<publishingdescription>Asiago Crusted Chicken</publishingdescription>
<publishingtext />
<enticingdescription />
<price>2.25</price>
<category>Chicken</category>
<productionarea>Hot Production</productionarea>
<nutrients>203|19.6|7.6|13.2|56|124|387|1.37|</nutrients>
<nutrientsuncertain>0|0|0|0|0|0|0|0|</nutrientsuncertain>
<CaloriesEnergykcalkcal>152</CaloriesEnergykcalkcal>
<ProteinProteingm>2.3</ProteinProteingm>
<FatFatgm>13.8</FatFatgm>
<CarbsTotalCarbohydratesgm>6.5</CarbsTotalCarbohydratesgm>
<CholestrolCholesterolmg>0</CholestrolCholesterolmg>
<CalciumCalciummg>74</CalciumCalciummg>
<SodiumSodiummg>346</SodiumSodiummg>
<IronIronmg>1.85</IronIronmg>
</RECIPE>
<!-- any number of <RECIPE> elements ... -->
</RECIPES>
</MENUS>