XSLT 1.0 Grouping

Tag: xslt , xslt-grouping Author: fericlv Date: 2013-05-26

I need to apply XSLT 1.0 transformation to following XML:

<ProfitLossFinancials>
  <ProfitLossFinancial>
    <Year>2013</Year>
    <Turnover>13</Turnover>
    <Profit>13</Profit>
  </ProfitLossFinancial>
  <ProfitLossFinancial>
    <Year>2012</Year>
    <Turnover>12</Turnover>
    <Profit>12</Profit>
  </ProfitLossFinancial>
</ProfitLossFinancials>
<BalanceSheetFinancials>
  <BalanceSheetFinancial>
    <Year>2013</Year>
    <FixedAssets>13</FixedAssets>
  </BalanceSheetFinancial>
  <BalanceSheetFinancial>
    <Year>2011</Year>
    <FixedAssets>11</FixedAssets>
  </BalanceSheetFinancial>
</BalanceSheetFinancials>

to have following output:

<Financials>
  <Financial>
    <Year>2013</Year>
    <Turnover>13</Turnover>
    <Profit>13</Profit>
    <FixedAssets>13</FixedAssets>
  </Financial>
  <Financial>
    <Year>2012</Year>
    <Turnover>12</Turnover>
    <Profit>12</Profit>
    <FixedAssets/>
  </Financial>
  <Financial>
    <Year>2011</Year>
    <Turnover/>
    <Profit/>
    <FixedAssets>11</FixedAssets>
  </Financial>
</Financials>

How to do that?

Best Answer

Please try this:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
  <xsl:key name="kFinancialByYear"
           match="*[self::ProfitLossFinancial or self::BalanceSheetFinancial]"
           use="Year" />
  <xsl:variable name="af" 
                select="//*[self::ProfitLossFinancial or 
                            self::BalanceSheetFinancial]"/>

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

  <xsl:template match="/*">
    <Financials>
      <xsl:apply-templates select="$af[generate-id() = 
                                      generate-id(key('kFinancialByYear', Year)[1])]"
                           mode="group" />
    </Financials>
  </xsl:template>

  <xsl:template match="*" mode="group">
    <xsl:variable name="thisYearValues" select="key('kFinancialByYear', Year)" />
    <Financial>
      <xsl:apply-templates select="Year" />
      <Turnover>
        <xsl:value-of select="$thisYearValues/Turnover"/>
      </Turnover>
      <Profit>
        <xsl:value-of select="$thisYearValues/Profit"/>
      </Profit>
      <FixedAssets>
        <xsl:value-of select="$thisYearValues/FixedAssets"/>
      </FixedAssets>
    </Financial>
  </xsl:template>
</xsl:stylesheet>

When run on this input:

<n>
  <ProfitLossFinancials>
    <ProfitLossFinancial>
      <Year>2013</Year>
      <Turnover>13</Turnover>
      <Profit>13</Profit>
    </ProfitLossFinancial>
    <ProfitLossFinancial>
      <Year>2012</Year>
      <Turnover>12</Turnover>
      <Profit>12</Profit>
    </ProfitLossFinancial>
  </ProfitLossFinancials>
  <BalanceSheetFinancials>
    <BalanceSheetFinancial>
      <Year>2013</Year>
      <FixedAssets>13</FixedAssets>
    </BalanceSheetFinancial>
    <BalanceSheetFinancial>
      <Year>2011</Year>
      <FixedAssets>11</FixedAssets>
    </BalanceSheetFinancial>
  </BalanceSheetFinancials>
</n>

The result is:

<Financials>
  <Financial>
    <Year>2013</Year>
    <Turnover>13</Turnover>
    <Profit>13</Profit>
    <FixedAssets>13</FixedAssets>
  </Financial>
  <Financial>
    <Year>2012</Year>
    <Turnover>12</Turnover>
    <Profit>12</Profit>
    <FixedAssets></FixedAssets>
  </Financial>
  <Financial>
    <Year>2011</Year>
    <Turnover></Turnover>
    <Profit></Profit>
    <FixedAssets>11</FixedAssets>
  </Financial>
</Financials>

comments:

Looks way overkill to me...
@NilsWerner That's because you made a much shorter answer that doesn't work.
@NilsWerner - this is a correct solution, as it produces the wanted output. It takes into account all possible fields for each possible year.
when ProfitLossFinancial and BalanceSheetFinancial both contain an element with the same name - can we distinguish them in Financial item?
@jlp: To distinguish same element names from different parent use self::NAME. E.g: for Turnover from ProfitLossFinancial: "$thisYearValues/self::ProfitLossFinancial/Turnover"in the soltion above or <xsl:value-of select="$yearData/self::ProfitLossFinancial/Turnover"/> in my soltuion.

Other Answer1

Here are more straightforward solution using for-each and key:

<?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:key match="Year" name="kyears" use="."/>

    <xsl:template match="/*">
        <Financials>
            <xsl:for-each select="//Year[generate-id() = 
                                    generate-id(key('kyears', .)[1])]"  >
                <xsl:variable name="year" select="." />
                <xsl:variable name="yearData" select="key('kyears', $year)/.." />
                <Financial>
                    <xsl:copy-of select="$year"/>
                    <Turnover>
                        <xsl:value-of select="$yearData/Turnover"/>
                    </Turnover>
                    <Profit>
                        <xsl:value-of select="$yearData/Profit"/>
                    </Profit>
                    <FixedAssets>
                        <xsl:value-of select="$yearData/FixedAssets"/>
                    </FixedAssets>
                </Financial>
            </xsl:for-each>
        </Financials>
    </xsl:template>

</xsl:stylesheet>

Which generate the following output:

<?xml version="1.0"?>
<Financials>
  <Financial>
    <Year>2013</Year>
    <Turnover>13</Turnover>
    <Profit>13</Profit>
    <FixedAssets>13</FixedAssets>
  </Financial>
  <Financial>
    <Year>2012</Year>
    <Turnover>12</Turnover>
    <Profit>12</Profit>
    <FixedAssets/>
  </Financial>
  <Financial>
    <Year>2011</Year>
    <Turnover/>
    <Profit/>
    <FixedAssets>11</FixedAssets>
  </Financial>
</Financials>

Other Answer2

<xsl:template match="ProfitLossFinancials">
    <Financials>
        <xsl:apply-templates select="ProfitLossFinancial"/>
    </Financials>
</xsl:template>

<xsl:template match="ProfitLossFinancial">
    <Financial>
        <xsl:copy-of select="./*"/>
        <xsl:copy-of select="../../BalanceSheetFinancials/BalanceSheetFinancial[Year = current()/Year]/FixedAssets"/>
    </Financial>
</xsl:template>

comments:

This does not produce the wanted output.
Small typo, fixed it. Also, more helpful error descriptions are appreciated.
Unfortunately, this is more than a small typo. If these are the only two templates you propose, then all the text() elements are output directly to the document; additionally, this only produces two <Financial> blocks of elements - a block for 2011 is missing.
The FixedAssets element for 2013 is also missing.
I don't try to give complete out of the box solutions but instead building blocks that the OP has to integrate into his solution. It should be obvious to anyone that the declaration, xsl:stylesheet and other templates are missing. But you are right, I missed the fact, that 2011 doesnt have a ProfitLossFinancial element...