BWatkins
BWatkins

Reputation: 11

XSLT Nested Looping of Multiple Child Elements

I am stuck on a looping question and need some direction from the group. I am able to loop through one child, but having trouble adding the second.

This is my XML:

<wd:Report_Entry>
  <wd:Beneficiaries_-_All>
    <wd:EMPLID>00025</wd:EMPLID>
    <wd:Last>LastName</wd:Last>
    <wd:First>FirstName</wd:First>
    <wd:WID>key1234</wd:WID>
  </wd:Beneficiaries_-_All>
  <wd:Beneficiaries_-_All>
    <wd:EMPLID>00025</wd:EMPLID>
    <wd:Last>LastName</wd:Last>
    <wd:First>First2</wd:First>
    <wd:WID>key4567</wd:WID>
  </wd:Beneficiaries_-_All>
  <wd:Beneficiaries_-_People>
    <wd:DOB>Birth1</wd:DOB>
    <wd:ID>ID1</wd:ID>
    <wd:WID>key1234</wd:WID>
  </wd:Beneficiaries_-_People>
  <wd:Beneficiaries_-_People>
    <wd:DOB>Birth2</wd:DOB>
    <wd:ID>ID2</wd:ID>
    <wd:WID>key4567</wd:WID>
  </wd:Beneficiaries_-_People>
</wd:Report_Entry>

My XSLT is below

<xsl:output method="text"/>
<xsl:variable name="linefeed" select="'&#xA;'"></xsl:variable>

<xsl:template match="/">
 <xsl:for-each select="/wd:Report_Data/wd:Report_Entry">
   <xsl:for-each select="wd:Beneficiaries_-_All">
    <xsl:text>"</xsl:text>
    <xsl:value-of select="wd:EMPLID"/>
    <xsl:text>","</xsl:text>
    <xsl:value-of select="wd:Last"/>
    <xsl:text>","</xsl:text>
    <xsl:value-of select="wd:First"/>
    <xsl:text>","</xsl:text>
    <xsl:value-of select="$linefeed"/>
  </xsl:for-each>
  <xsl:for-each select="wd:Beneficiaries_-_People">
    <xsl:value-of select="wd:DOB"/>
    <xsl:text>","</xsl:text>
    <xsl:value-of select="wd:ID"/>
    <xsl:text>"</xsl:text>
  </xsl:for-each>
  <xsl:value-of select="$linefeed"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

My desired output is CSV like this:

"00025","LastName","First1","Birth1","ID1"
"00025","LastName","First2","Birth2","ID2"
"00026","LastName","First1","Birth1","ID1" (continues in this fashion)

But I am getting for each Report Entry:

"00025","LastName","First1","
"00025","LastName","First2","
Birth1","ID1" Birth2","ID2"

My research on this site indicates that <apply-templates> is preferred over <for-each>. The format gives me a problem when I try that method. Thanks group!

Upvotes: 0

Views: 945

Answers (2)

Tomalak
Tomalak

Reputation: 338108

Here is a solution that shows a few XSLT features:

  • template matching (as opposed to <xsl:for-each>)
  • XSL keys
  • multiple template modes

(I've used a bogus namespace URI for the wd prefix)

<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:wd="http://some.namespace/wd"
>
  <xsl:output method="text" />
  <xsl:key name="kPeople" match="wd:Beneficiaries_-_People" use="wd:WID" />

  <xsl:template match="/">
    <xsl:apply-templates select="wd:Report_Data/wd:Report_Entry/wd:Beneficiaries_-_All" />
  </xsl:template>

  <xsl:template match="wd:Beneficiaries_-_All">
    <xsl:apply-templates select="wd:EMPLID" mode="csv" />
    <xsl:apply-templates select="wd:Last" mode="csv" />
    <xsl:apply-templates select="wd:First" mode="csv" />
    <xsl:apply-templates select="key('kPeople', wd:WID)/wd:DOB" mode="csv" />
    <xsl:apply-templates select="key('kPeople', wd:WID)/wd:ID" mode="csv-nl" />
  </xsl:template>

  <xsl:template match="*" mode="csv">
    <xsl:value-of select="concat('&quot;', ., '&quot;,')" />
  </xsl:template>

  <xsl:template match="*" mode="csv-nl">
    <xsl:value-of select="concat('&quot;', ., '&quot;&#xA;')" />
  </xsl:template>
</xsl:stylesheet>

For your sample data this results in:

"00025","LastName","FirstName","Birth1","ID1"
"00025","LastName","First2","Birth2","ID2"

Gotchas:

  • Make sure that double quotes in values are properly handled, because otherwise the CSV will be broken. Either make sure you remove them or escape them properly. Getting this right is can be tricky.
  • This does not work if certain XML elements (for example <wd:DOB>) do not exist. For uniform input where the elements are always there but sometimes empty, everything is fine. If elements can be missing, the script would need to be changed.

In both cases: Know your data.


Template matching is a double-edged sword. It's great for input-driven transformations. The input XML is mapped through the templates in the stylesheet with minimal plumbing.

CSV on the other hand is a rigid output format, creating it in an input-driven fashion requires the input to be just as rigid. Since XML allows missing elements, there might not even be an element that can be turned into an output field. In this case, named templates and parameters can help:

Instead of

<xsl:apply-templates select="wd:EMPLID" mode="csv" />

we could have

<xsl:call-template name="csv">
  <xsl:with-param name="val" value="wd:EMPLID" />
</xsl:call-template>

and

<xsl:template name="csv">
  <xsl:param name="val" />
  <xsl:value-of select="concat('&quot;', $val, '&quot;,')" />
</xsl:call-template>

This would make sure that even when an element is missing, the empty field "", would still be printed.

Upvotes: 1

friedemann_bach
friedemann_bach

Reputation: 1458

If I understand it correctly, you just need a variable to make it work:

<xsl:for-each select="/wd:Report_Data/wd:Report_Entry">
    <xsl:for-each select="wd:Beneficiaries_-_All">
        <xsl:text>"</xsl:text>
        <xsl:value-of select="wd:EMPLID"/>
        <xsl:text>","</xsl:text>
        <xsl:value-of select="wd:Last"/>
        <xsl:text>","</xsl:text>
        <xsl:value-of select="wd:First"/>
        <xsl:text>","</xsl:text>
        <!-- new code starts here -->
        <xsl:variable name="WID" select="wd:WID"/>
        <xsl:value-of select="../wd:Beneficiaries_-_People[wd:WID = $WID]/wd:DOB"/>
        <xsl:text>","</xsl:text>
        <xsl:value-of select="../wd:Beneficiaries_-_People[wd:WID = $WID]/wd:ID"/>
        <xsl:text>"</xsl:text>
        <!-- end new code -->
        <xsl:value-of select="$linefeed"/>
    </xsl:for-each>
</xsl:for-each>

Hope this helps!

Upvotes: 0

Related Questions