2

As I understand it, using for-each loops when unnecessary is bad form. Can someone show me how I might convert the nested for-loops with grouping in this XSL into individual templates? This seems easy when the XML is hierarchical, but for flat XML, I have yet to figure out the XPATH expression or other syntax needed to do this.

Sample XML data:

<?xml version = "1.0"?> 
<?xml-stylesheet type = "text/xsl" href = "time_detail_employee_m.xsl"?> 
<Employees>
  <Employee>
    <COMPANY_ID>83207</COMPANY_ID>        
    <PRJ_PROJECT_ID>104</PRJ_PROJECT_ID>
    <PRJ_PROJECT_NAME>Portal</PRJ_PROJECT_NAME>
    <PERSON_ID>5881</PERSON_ID>
    <TM_FIRST_NAME>Dave</TM_FIRST_NAME>
    <TM_LAST_NAME>Morgan</TM_LAST_NAME>
    <SR_ID>3075</SR_ID>
    <SR_TITLE>Shoe Page</SR_TITLE>
    <TM_BEGIN_DT>2015-12-11T00:00:00</TM_BEGIN_DT>
    <TM_BEGIN_TIME>10:45:00</TM_BEGIN_TIME>
    <TM_END_TIME>16:30:00</TM_END_TIME>
    <TM_TIME_CD>REG</TM_TIME_CD>
    <TM_BILLABLE>F</TM_BILLABLE>        
    <TM_WEEK>50</TM_WEEK>
    <TM_CALCULATED_TIME>5.750000</TM_CALCULATED_TIME>        
  </Employee>
  <Employee>
    ...
  </Employee>
  ...
</Employees>

XSL stylesheet:

    <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:key name="group-by-person" match="Employee" use="PERSON_ID" />    
   <xsl:key name="group-by-week" match="Employee" use="concat(PERSON_ID,'|',TM_WEEK)" /> 
   <xsl:key name="group-by-day" match="Employee" use="concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT)" /> 

   <xsl:variable name="space"><xsl:text> </xsl:text></xsl:variable>

   <xsl:template match="/">
      <html><body>   
         <xsl:apply-templates />
      </body></html>
   </xsl:template>

   <xsl:template match="Employees">
      <xsl:for-each select="Employee[count(. | key('group-by-person', PERSON_ID)[1]) = 1]">
         <xsl:sort select="TM_LAST_NAME" />
         <p><xsl:value-of select="TM_FIRST_NAME" /><xsl:value-of select="$space"/><xsl:value-of select="TM_LAST_NAME" /></p><br />

               <!-- begin week grouping -->         
               <xsl:for-each select="key('group-by-person', PERSON_ID)[count(. | key('group-by-week', concat(PERSON_ID,'|',TM_WEEK))[1]) = 1]">
                  <xsl:sort select="TM_WEEK" data-type="number"/>
                  <p><xsl:value-of select="TM_WEEK" /></p><br/>

                      <!-- begin day grouping -->
                      <xsl:for-each select="key('group-by-week', concat(PERSON_ID,'|',TM_WEEK))[count(. | key('group-by-day', concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT))[1]) = 1]">
                          <xsl:sort select="TM_BEGIN_DT" />
                          <xsl:value-of select="substring-before(TM_BEGIN_DT,'T')" />
                          <br/>
                          <xsl:for-each select="key('group-by-day', concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT))">
                              <p><xsl:value-of select="TM_BEGIN_TIME" /><xsl:value-of select="$space"/><xsl:value-of select="TM_END_TIME" /></p><br/>
                          </xsl:for-each>
                          <br/><xsl:text>daily sum = </xsl:text>
                          <xsl:value-of select="sum(key('group-by-day', concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT))/TM_CALCULATED_TIME)" />
                          <br/>
                      </xsl:for-each>
                   <!-- end day grouping -->
                   <br/><xsl:text>weekly sum = </xsl:text>
                   <xsl:value-of select="sum(key('group-by-week', concat(PERSON_ID,'|',TM_WEEK))/TM_CALCULATED_TIME)" />
                   <br/>
               </xsl:for-each>               
               <!-- end week grouping -->
          <br/><xsl:text>person_id sum = </xsl:text>
          <xsl:value-of select="sum(key('group-by-person', PERSON_ID)/TM_CALCULATED_TIME)" />
          <br/>  
      </xsl:for-each>
   </xsl:template>   
</xsl:stylesheet>

Sample output, something on the order of:

Name 1 (based on PERSON_ID)
    Week 1 (based on TM_WEEK)
        Monday (based on TM_BEGIN_DT)
            time1 - time2 (TM_BEGIN_TIME - TM_END_TIME)
            time3 - time4
        Tuesday
            time1 - time2
    Week 2
        Thursday
            time1 - time2
            time3 - time4
            time5 - time6
Name 2
    Week 1
        Wednesday
            time1 - time2
Name 3, etc.
sk8pickel
  • 43
  • 4
  • Could you post a sample of your desired output? It's a lot easier to help if you show what you're shooting for. – Eiríkr Útlendi Apr 05 '17 at 17:37
  • The output should reflect the groupings defined in the key declarations. That is, for every PERSON_ID, their associated TM_WEEKs, and TM_BEGIN_DTs within each week will be listed. – sk8pickel Apr 05 '17 at 19:03
  • I'd like to reiterate the request. Seeing what you need on output is easier than trying to figure it out from your code. As writing teachers have often said to me, _"Show, don't tell."_ :) Also, your sample input only includes one `` and one `` for the given ``, so sorting on those won't do anything. Is your sample input representative? – Eiríkr Útlendi Apr 05 '17 at 20:16
  • Reading your XSL, I have a growing sense that your input is not representative -- you appear to be grouping multiple `` elements by their `` children, suggesting that there can be multiple `` elements with the same `` values. Is this correct? If so, it would be helpful if you could update your sample input to show this, in addition to providing sample output. – Eiríkr Útlendi Apr 05 '17 at 20:29
  • I only showed one of the Employee nodes, because the XML file is large. But assume multiple Employee nodes similar to the one shown. And yes, the XSL currently finds all matching s, then s for that , then s for that , and begin and end times for that day. I am curious if these same actions can be accomplished with templates instead of nested for-each stmts. Will update with sample output. – sk8pickel Apr 05 '17 at 21:03

1 Answers1

2

It should be a straight-forward task, as the xpath expressions will be the same whether you use xsl:for-each or xsl:apply-templates. The only thing to consider is that, in your example at least, you may end up with multiple templates matching the same element (Employee in this case). However, you can get around this by using the mode attribute

Consider this xsl:for-each

  <xsl:for-each select="Employee[count(. | key('group-by-person', PERSON_ID)[1]) = 1]">
     <xsl:sort select="TM_LAST_NAME" />
     <!-- Inner code -->
  </xsl:for-each>

Replace this with a single xsl:apply-templates, like so

<xsl:apply-templates select="Employee[count(. | key('group-by-person', PERSON_ID)[1]) = 1]" mode="person" />

Then, move the inner code from the for-each into a template match

<xsl:template match="Employee" mode="person">
    <!-- Inner code -->
</xsl:template>

You can repeat these steps for the nested xsl:for-each in exactly the same manner.

Try this XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
   <xsl:key name="group-by-person" match="Employee" use="PERSON_ID" />    
   <xsl:key name="group-by-week" match="Employee" use="concat(PERSON_ID,'|',TM_WEEK)" /> 
   <xsl:key name="group-by-day" match="Employee" use="concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT)" /> 

   <xsl:output method="html" indent="yes" />

   <xsl:variable name="space"><xsl:text> </xsl:text></xsl:variable>

   <xsl:template match="/">
      <html><body>   
         <xsl:apply-templates />
      </body></html>
   </xsl:template>

   <xsl:template match="Employees">
      <xsl:apply-templates select="Employee[count(. | key('group-by-person', PERSON_ID)[1]) = 1]" mode="person">
         <xsl:sort select="TM_LAST_NAME" />
      </xsl:apply-templates>
   </xsl:template>   

   <xsl:template match="Employee" mode="person">
     <p><xsl:value-of select="TM_FIRST_NAME" /><xsl:value-of select="$space"/><xsl:value-of select="TM_LAST_NAME" /></p><br />
       <!-- begin week grouping -->         
       <xsl:apply-templates select="key('group-by-person', PERSON_ID)[count(. | key('group-by-week', concat(PERSON_ID,'|',TM_WEEK))[1]) = 1]" mode="week">
          <xsl:sort select="TM_WEEK" data-type="number"/>
       </xsl:apply-templates>               
       <!-- end week grouping -->
        <br/><xsl:text>person_id sum = </xsl:text>
      <xsl:value-of select="sum(key('group-by-person', PERSON_ID)/TM_CALCULATED_TIME)" />
      <br/>  
   </xsl:template>

   <xsl:template match="Employee" mode="week">
      <p><xsl:value-of select="TM_WEEK" /></p><br/>
      <!-- begin day grouping -->
      <xsl:apply-templates select="key('group-by-week', concat(PERSON_ID,'|',TM_WEEK))[count(. | key('group-by-day', concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT))[1]) = 1]" mode="day">
          <xsl:sort select="TM_BEGIN_DT" />
      </xsl:apply-templates>
       <!-- end day grouping -->
       <br/><xsl:text>weekly sum = </xsl:text>
       <xsl:value-of select="sum(key('group-by-week', concat(PERSON_ID,'|',TM_WEEK))/TM_CALCULATED_TIME)" />
       <br/>
   </xsl:template>

   <xsl:template match="Employee" mode="day">
      <xsl:value-of select="substring-before(TM_BEGIN_DT,'T')" />
      <br/>
      <xsl:for-each select="key('group-by-day', concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT))">
          <p><xsl:value-of select="TM_BEGIN_TIME" /><xsl:value-of select="$space"/><xsl:value-of select="TM_END_TIME" /></p><br/>
      </xsl:for-each>
      <br/><xsl:text>daily sum = </xsl:text>
      <xsl:value-of select="sum(key('group-by-day', concat(PERSON_ID,'|',TM_WEEK,'|',TM_BEGIN_DT))/TM_CALCULATED_TIME)" />
      <br/>
   </xsl:template>
</xsl:stylesheet>

Using xsl:for-each should not really be considered "bad form" in many cases. The thing to bear in mind is that xsl:for-each is a mapping construct, and not a loop. (The XSLT processor is free to process the selected nodes in parallel). In your case, use of xsl:for-each has led to excessive nesting, which can make the code harder to read, but apart from that, there was nothing really wrong with it.

Tim C
  • 70,053
  • 14
  • 74
  • 93