0

A little question about XML/XSL.

I have the following XML:

<Employees>
    <Employee> <ID>1</ID> <WeekID>1</WeekID> <DayID>1</DayID> <Hours>5</Hours> </Employee>
    <Employee> <ID>1</ID> <WeekID>1</WeekID> <DayID>1</DayID> <Hours>4</Hours> </Employee>
    <Employee> <ID>1</ID> <WeekID>1</WeekID> <DayID>2</DayID> <Hours>7</Hours> </Employee>
    <Employee> <ID>1</ID> <WeekID>2</WeekID> <DayID>1</DayID> <Hours>5</Hours> </Employee>
    <Employee> <ID>1</ID> <WeekID>2</WeekID> <DayID>3</DayID> <Hours>8</Hours> </Employee>
    <Employee> <ID>2</ID> <WeekID>1</WeekID> <DayID>1</DayID> <Hours>5</Hours> </Employee>
    <Employee> <ID>2</ID> <WeekID>2</WeekID> <DayID>4</DayID> <Hours>4</Hours> </Employee>
</Employees>

I want to hierarchize this XML like this:

<Employees>
    <Employee>
        <ID>1</ID>
        <Weeks>
            <Week>
                <WeekID>1</WeekID>
                <Days>
                    <Day>
                        <DayID>1</DayID>
                        <Hours>5</Hours>
                        <Hours>4</Hours>
                    </Day>
                    <Day>
                        <DayID>2</DayID>
                        <Hours>7</Hours>
                    </Day>
                </Days>
            </Week>
            <Week>
                <WeekID>2</WeekID>
                <Days>
                    <Day>
                        <DayID>1</DayID>
                        <Hours>5</Hours>
                    </Day>
                    <Day>
                        <DayID>3</DayID>
                        <Hours>8</Hours>
                    </Day>
                </Days>
            </Week>
        </Weeks>
    </Employee>
    <Employee>
        <ID>2</ID>
        <Weeks>
            <Week>
                <WeekID>1</WeekID>
                <Days>
                    <Day>
                        <DayID>1</DayID>
                        <Hours>5</Hours>
                    </Day>
                </Days>
            </Week>
            <Week>
                <WeekID>2</WeekID>
                <Days>
                    <Day>
                        <DayID>4</DayID>
                        <Hours>4</Hours>
                    </Day>
                </Days>
            </Week>
        </Weeks>
    </Employee>
</Employees>

Is this possible ? If so, what would be the XSL transformation to apply (with XSLT 1.0 only) ?

Thanks, Florent

Drinking coffee prevents you from sleeping. On the other hand, sleeping prevents you from drinking coffee.

  • Group with `xsl:for-each-group`, it seems you want to do this three times in a nested way. – Martin Honnen Nov 15 '20 at 14:01
  • This is a *grouping* question. Grouping in XSLT 1.0 is best done using the Muenchian method: http://www.jenitennison.com/xslt/grouping/muenchian.html. For an example of multi-level Muenchian grouping see: https://stackoverflow.com/a/58525214/3016153. – michael.hor257k Nov 15 '20 at 16:43

2 Answers2

0

In XSLT 2 or later, you can simply nest three xsl:for-each-group instructions:

  <xsl:template match="Employees">
    <xsl:copy>
      <xsl:for-each-group select="Employee" group-by="ID">
        <xsl:copy>
          <xsl:copy-of select="ID"/>
          <Weeks>
            <xsl:for-each-group select="current-group()" group-by="WeekID">
              <Week>
                <xsl:copy-of select="WeekID"/>
                <Days>
                  <xsl:for-each-group select="current-group()" group-by="DayID">
                    <Day>
                      <xsl:copy-of select="DayID"/>
                      <xsl:copy-of select="current-group()/Hours"/>
                    </Day>
                  </xsl:for-each-group>
                </Days>
              </Week>
            </xsl:for-each-group>
          </Weeks>
        </xsl:copy>
      </xsl:for-each-group>
    </xsl:copy>
  </xsl:template>

With XSLT 1, use Muenchian grouping with concatenated key values for the three levels:

  <xsl:key name="emp" match="Employee" use="ID"/>
  <xsl:key name="week" match="Employee" use="concat(ID, '|', WeekID)"/>
  <xsl:key name="day" match="Employee" use="concat(ID, '|', WeekID, '|', DayID)"/>
    
  <xsl:template match="Employees">
    <xsl:copy>
      <xsl:for-each select="Employee[generate-id() = generate-id(key('emp', ID)[1])]">
        <xsl:copy>
          <xsl:copy-of select="ID"/>
          <Weeks>
            <xsl:for-each select="key('emp', ID)[generate-id() = generate-id(key('week', concat(ID, '|', WeekID))[1])]">
              <Week>
                <xsl:copy-of select="WeekID"/>
                <Days>
                  <xsl:for-each select="key('week', concat(ID, '|', WeekID))[generate-id() = generate-id(key('day', concat(ID, '|', WeekID, '|', DayID))[1])]">
                    <Day>
                      <xsl:copy-of select="DayID"/>
                      <xsl:copy-of select="key('day', concat(ID, '|', WeekID, '|', DayID))/Hours"/>
                    </Day>
                  </xsl:for-each>
                </Days>
              </Week>
            </xsl:for-each>
          </Weeks>
        </xsl:copy>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
  • Thank you so much Martin. Unfortunately, I cannot apply XLST 2, I would need a solution in XSLT 1.0 (I add this precision in my initial request) – Florent Bignier Nov 15 '20 at 15:20
  • @FlorentBignier, I have added an example for XSLT 1 although it would be interesting to know which platform you are one that you don't have access to XSLT 2 or 3 with Saxon being available for Java, .NET, C/C++, Python, JavaScript and Node.js. – Martin Honnen Nov 15 '20 at 20:35
-1

I found a solution:

<xsl:stylesheet version="1.0" 
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/Employees">
<Employees>
<xsl:for-each select="Employee">
<xsl:variable name="EmployeeID" select="ID" />
<xsl:if test="not(preceding-sibling::Employee[ID=$EmployeeID])">
<Employee>
<ID><xsl:value-of select="$EmployeeID"/></ID>
<Weeks>

<xsl:for-each select="../Employee[ID=$EmployeeID]">
<xsl:variable name="WeekID" select="WeekID" />
<xsl:if test="not(preceding-sibling::Employee[ID=$EmployeeID and WeekID=$WeekID])">
<Week>
<WeekID><xsl:value-of select="$WeekID"/>
</WeekID>
<Days>

<xsl:for-each select="../Employee[ID=$EmployeeID and WeekID=$WeekID]">
<xsl:variable name="DayID" select="DayID" />
<xsl:if test="not(preceding-sibling::Employee[ID=$EmployeeID and WeekID=$WeekID and DayID=$DayID])">
<Day>
<DayID><xsl:value-of select="$DayID"/></DayID>

<xsl:for-each select="../Employee[ID=$EmployeeID and WeekID=$WeekID and DayID=$DayID]">
<xsl:variable name="Hours" select="Hours" />
<xsl:if test="not(preceding-sibling::Employee[ID=$EmployeeID and WeekID=$WeekID and DayID=$DayID and Hours=$Hours])">
<Hours><xsl:value-of select="Hours"/></Hours>
</xsl:if>
</xsl:for-each>

</Day>
</xsl:if>
</xsl:for-each>

</Days>
</Week>
</xsl:if> 
</xsl:for-each>

</Weeks>
</Employee>
</xsl:if>         
</xsl:for-each>
</Employees>
</xsl:template>

</xsl:stylesheet>

If someone knows the Muenchian method and can tell me how to transpose this XSLT...