0
votes

Here is my input XML that I need to transform using XSLT 2.0

<?xml version="1.0" encoding="UTF-8"?>
<Workers>
    <Worker>
        <id>1234</id>
        <loc>New York</loc>
        <Days>1</Days>
        <StartDate>2019-01-26</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <Batch>A</Batch>
        <Days>3</Days>
        <Units>2</Units>
        <StartDate>2019-02-01</StartDate>
    </Worker>
</Workers>

Current Output

<?xml version="1.0" encoding="UTF-8"?>
<Workers>
    <Worker>
        <id>1234</id>
        <loc>New York</loc>
        <Days>1</Days>
        <StartDate>2019-01-26</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>1</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-02</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>2</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-03</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>3</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-04</StartDate>
    </Worker>
</Workers>

Expected Output

<?xml version="1.0" encoding="UTF-8"?>
<Workers>
    <Worker>
        <id>1234</id>
        <loc>New York</loc>
        <Days>1</Days>
        <StartDate>2019-01-26</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>1</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-01</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>2</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-02/StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>3</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-03</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>1</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-01</StartDate>
    </Worker>
    <Worker>
        <id>2345</id>
        <loc>Boston</loc>
        <RecordNumber>2</RecordNumber>
        <WorkerDays>1</WorkerDays>
        <StartDate>2019-02-02/StartDate>
    </Worker>
</Workers>

My requirements are

a.) If element <Batch> is not present in a child node, that child node should be copied as it appears in the XML. In the above XML, the first worker child node should be copied as it appears in the XML as it doesn’t have <Batch> element in it.

b.) If element <Batch> is present in a child node, that child node needs to be split into multiple child nodes based on following two conditions

1.) Number of child nodes need to be created as many number of times as the value of element <Days>. In this case, <Days> has 3 as a value, so 3 child nodes need to be created and each one of those child node should have <StartDate> incremented by one and create a new element <RecordNumber> that should hold the value of that loop.

2.) Child node again needs to be split based on the value of element <Units>. In above XML, <Units> is 2, so two times child node needs to be created and <StartDate> needs to be incremented by one each time it creates and and create a new element <RecordNumber> that should hold the value of that loop

Current XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:functx="http://www.functx.com" xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all">
    <xsl:output method="xml" omit-xml-declaration="no" indent="yes"/>
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="Workers/Worker[exists(Batch)]">
        <xsl:variable name="start" select="1"/>
        <xsl:variable name="counter" select="Days"/>
        <xsl:variable name="Records" select="."/>
        <xsl:for-each select="$start to $counter">
            <xsl:apply-templates select="$Records" mode="replicate">
                <xsl:with-param name="data" select="."/>
            </xsl:apply-templates>
        </xsl:for-each>
    </xsl:template>
    <xsl:template match="Workers/Worker" mode="replicate">
        <xsl:param name="data"/>
        <Worker>
            <id>
                <xsl:value-of select="id"/>
            </id>
            <loc>
                <xsl:value-of select="loc"/>
            </loc>
            <RecordNumber>
                <xsl:value-of select="$data"/>
            </RecordNumber>
            <WorkerDays>1</WorkerDays>
            <StartDate>
                <xsl:value-of select="xs:date(StartDate) + xs:dayTimeDuration('P1D') * $data"/>
            </StartDate>
        </Worker>
    </xsl:template>
</xsl:stylesheet>

Issues:

  1. Value of <StartDate> is wrong - seems like actual <StartDate> is missing

  2. XSLT is not splitting child node based on at all.

  3. <Worker> child node should appear 6 times in expected output where as i get only 4 now.

Can someone help me fix the issue please?

Thanks

1

1 Answers

1
votes

From your expected output I see that you need 2 loops in a sequence:

  • the first - executed Days number of times,
  • the second - executed Units number of times,

both for consecutive dates, starting from the input date.

Note also that in for-each loop the context item is changed, so in case of both for-each loops, the context item is the current numer of execution, taken form select attribute.

This is the reason that in each loop, when calling Worker template, dayNo parameter has been given as a dot and currElem is just the element, for which Worker[Batch] template was called.

As far as the output StartDate is concerned, the dayTimeDuration must be added $dayNo - 1 number of times.

Another useful additions / changes are:

  • <xsl:strip-space elements="*"/> at the beginning, causing "better" indentation of the output.
  • <xsl:sequence .../> - a shorter form to copy source elements to the output. It is also worth to note that it runs much quicker and consumes much less memory.
  • I deleted some variables used 1 time only.
  • You used a variable called data. IMHO this name is "too general", so I changed it to currElem (the current element).

So the whole script can look like below:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:xs="http://www.w3.org/2001/XMLSchema" exclude-result-prefixes="#all">
  <xsl:output method="xml" omit-xml-declaration="no" indent="yes"/>
  <xsl:strip-space elements="*"/>

  <xsl:template match="Worker[Batch]">
    <xsl:variable name="currElem" select="."/>
    <xsl:for-each select="1 to Days">
      <xsl:apply-templates select="$currElem" mode="replicate">
        <xsl:with-param name="dayNo" select="."/>
      </xsl:apply-templates>
    </xsl:for-each>
    <xsl:for-each select="1 to Units">
      <xsl:apply-templates select="$currElem" mode="replicate">
        <xsl:with-param name="dayNo" select="."/>
      </xsl:apply-templates>
    </xsl:for-each>
  </xsl:template>

  <xsl:template match="Worker" mode="replicate">
    <xsl:param name="dayNo"/>
    <Worker>
      <xsl:sequence select="id, loc"/>
      <RecordNumber><xsl:value-of select="$dayNo"/></RecordNumber>
      <WorkerDays>1</WorkerDays>
      <StartDate>
        <xsl:value-of select="xs:date(StartDate) + xs:dayTimeDuration('P1D') * ($dayNo - 1)"/>
      </StartDate>
    </Worker>
  </xsl:template>

  <xsl:template match="@*|node()">
    <xsl:copy><xsl:apply-templates select="@*|node()"/></xsl:copy>
  </xsl:template>
</xsl:stylesheet>

For a working example see http://xsltransform.net/asnmyP/1

The first version is under http://xsltransform.net/asnmyP (if you wanted to compare them).