Here is my source XML
<?xml version="1.0" encoding="UTF-8"?>
<Workers>
<Worker>
<ID>12345</ID>
<Amount>$850.50</Amount>
<CurrentPayments>2</CurrentPayments>
</Worker>
<Worker>
<ID>45678</ID>
<Amount>$6500.50</Amount>
<CurrentPayments>5</CurrentPayments>
</Worker>
<Worker>
<ID>12345</ID>
<Amount>$3200.20</Amount>
<CurrentPayments>2</CurrentPayments>
</Worker>
<Worker>
<ID>12345</ID>
<Amount>$6500.50</Amount>
<CurrentPayments>2</CurrentPayments>
</Worker>
</Workers>
Worker
with ID
value 12345
appears three times on the source file with same value of CurrentPayments
i.e. 2
When above file gets transformed, position value of CurrentPayments
should get incremented based on the number of times that element CurrentPayments
appears on the source file.
In this case, first occurrence of Worker
with ID
value 12345
should have CurrentPayments
value as 3(i.e. the value of CurrentPayments
on source xml is 2 incremented by 1 - first occurrence of Worker
with ID
value 12345
)
Second occurrence of Worker
with ID
value 12345
should have CurrentPayments
value as 4(i.e. the value of CurrentPayments
on source xml is 2 incremented by 2 - second occurrence of Worker
with ID
value 12345
)
Third occurrence of Worker
with ID
value 12345
should have CurrentPayments
value as 5(i.e. the value of CurrentPayments
on source xml is 2 incremented by 3 - third occurrence of Worker
with ID
value 12345
)
Desired output is as below.
<?xml version="1.0" encoding="UTF-8"?>
<Workers>
<Worker>
<ID>12345</ID>
<Amount>$850.50</Amount>
<CurrentPayments>3</CurrentPayments>
</Worker>
<Worker>
<ID>45678</ID>
<Amount>$6500.50</Amount>
<CurrentPayments>6</CurrentPayments>
</Worker>
<Worker>
<ID>12345</ID>
<Amount>$3200.20</Amount>
<CurrentPayments>4</CurrentPayments>
</Worker>
<Worker>
<ID>12345</ID>
<Amount>$6500.50</Amount>
<CurrentPayments>5</CurrentPayments>
</Worker>
</Workers>
I have tried using position()
, comparing value of current node with child nodes of same element using xpath axes etc.. for e.g. <xsl:value-of select="count(//.[(following::text() = text())])+position()"/>
Here is my current 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" version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:mode on-no-match="shallow-copy"/>
<xsl:template match="CurrentPayments">
<xsl:for-each select=".">
<CurrentPayments>
<xsl:value-of select=".+ position()"/>
</CurrentPayments>
<!--<CurrentPayments>
<xsl:value-of select="count(//.[(following::text() = text())])+position()"/>
</CurrentPayments> -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
please Can you advise as to how I can get this to working ? thank you
Added below
Martin Honnen's XSLT 3.0 solution works perfectly fine for above requirement. However, I noted that CurrentPayments
may not be present for all <Worker>
.In this case, source XML would look like this(last node of <Worker>
with ID
123458
doesn't have CurrentPayments
and expected output should have element CurrentPayments
created with a value 1
<?xml version="1.0" encoding="UTF-8"?>
<Workers>
<Worker>
<ID>12345</ID>
<Amount>$850.50</Amount>
<CurrentPayments>2</CurrentPayments>
</Worker>
<Worker>
<ID>45678</ID>
<Amount>$6500.50</Amount>
<CurrentPayments>5</CurrentPayments>
</Worker>
<Worker>
<ID>12345</ID>
<Amount>$3200.20</Amount>
<CurrentPayments>2</CurrentPayments>
</Worker>
<Worker>
<ID>12345</ID>
<Amount>$6500.50</Amount>
<CurrentPayments>2</CurrentPayments>
</Worker>
<Worker>
<ID>123458</ID>
<Amount>$6500.50</Amount>
</Worker>
</Workers>
I added a new template <xsl:template match="Worker[not(CurrentPayments)]">
to have element CurrentPayments
created with value 1
. Could you advise if this is the only option i have?
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:output method="xml" indent="yes"/>
<xsl:accumulator name="worker-count" as="map(xs:integer, xs:integer)" initial-value="map{}">
<xsl:accumulator-rule match="Worker/ID"
select="let $id := xs:integer(.)
return
if (map:contains($value, $id))
then map:put($value, $id, $value($id) + 1)
else map:put($value, $id, 1)"/>
</xsl:accumulator>
<xsl:mode on-no-match="shallow-copy" use-accumulators="worker-count"/>
<xsl:template match="CurrentPayments">
<xsl:choose>
<xsl:when test=".!=''">
<xsl:copy>{. + accumulator-before('worker-count')(xs:integer(../ID))}</xsl:copy>
</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="Worker[not(CurrentPayments)]">
<xsl:copy>
<xsl:apply-templates/>
<CurrentPayments>1</CurrentPayments>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
With XSLT 3, I think this is a task for an accumulator:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
exclude-result-prefixes="#all"
expand-text="yes">
<xsl:accumulator name="worker-count" as="map(xs:integer, xs:integer)" initial-value="map{}">
<xsl:accumulator-rule match="Worker/ID"
select="let $id := xs:integer(.)
return
if (map:contains($value, $id))
then map:put($value, $id, $value($id) + 1)
else map:put($value, $id, 1)"/>
</xsl:accumulator>
<xsl:mode on-no-match="shallow-copy" use-accumulators="worker-count"/>
<xsl:template match="CurrentPayments">
<xsl:copy>{. + accumulator-before('worker-count')(xs:integer(../ID))}</xsl:copy>
</xsl:template>
</xsl:stylesheet>
In that code sample, an accumulator stores in a map the ID
of a Worker
plus the number of occurrences of that Worker
, the rule for CurrentPayments
can then simply access the accumulator.