Search code examples
xslt-2.0xslt-3.0

Increment the position of a node based on the value of child node


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 123458doesn'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 CurrentPaymentscreated 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>

Solution

  • 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.