tags:

views:

66

answers:

4

Im working with XSLT1.0 (Processor can't handle 2.0) and have a problem trying to group the output of an xml structure:

<row>
<order>
<text> some order text 1
</text>
</order>
</row>

<row>
<payment>
<text> some payment text 1
</text>
</payment>
</row>

<row>
<order>
<text> some order text 2
</text>
</order>
</row>

<row>
<contact>
<text> some contact details 1
</text>
</contact>
</row>

<row>
<contact>
<text> some contact details 2
</text>
</contact>
</row>

Today we select all rows and call apply template for each (each type has its own template that writes out its body), that creates an output like:

Order: some order text1
Order: some order text2
Payment: some payment text1
Contact: some contact details1
Contact: some contact details2

But what I would like is to (in XSLT 1.0) to group the output so that:

Order

  1. some order text1
  2. some order text2

Payment

  1. some payment text1

Contact

  1. some contact details1
  2. some contact details2

Obviously there are many other element types than order,payment and contact involved here so selecting by explicit element names is not a solution.

EDIT

Ty, some great answers, how would the Muenchian grouping solution change if I had a structure of say

<customers>
  <person>
    <row>....</row> (row is same as above)
    <row>....</row>
  </person>
  <person>
    <row>....</row>
    <row>....</row>
    <row>....</row>
  </person>

Then the key:

  <xsl:key name="type" match="row/*" use="local-name()"/>

Would select all rows across all persons which is not what I wanted. Thanks for great responses too.

+1  A: 

Try:

<xsl:template match="(parent element-whatever contains the 'row' elements)">
  <xsl:apply-templates>
    <xsl:sort select="name(*)" />
  </xsl:apply-templates>
</xsl:template>

This sorts the row elements by the name of the the first child.

This template adds in a header:

<xsl:template match="row">
    <xsl:copy>
    <xsl:if test="not(preceding-sibling::*[name(*) = name(current()/*)])">
      <!-- Output header here -->
      <xsl:value-of select="name(*)" />
    </xsl:if>
    <xsl:apply-templates select="@* | node()"/>
  </xsl:copy>
</xsl:template>

The test basically says 'Output this if there's no previous siblings with the same name'.

Flynn1179
Except this will not print out the section header ("Order").
LarsH
P.S. But I like the idea of using sorting as a simple way to group things. +1
LarsH
True I suppose; I interpreted the output as a description rather than the exact text. I've edited in a way of adding in a header. EDIT: Heh, just noticed, that's more or less what your answer does :) I just didn't go into as much detail formatting the output.
Flynn1179
+4  A: 

Doing this in XSLT 1.0 you need to use Muenchian grouping, but is easier (in my opinion) to solve with xsl:for-each-group in XSLT 2.0.

The following XSLT 1.0 stylesheet will do what you ask, The key is to use a key (doh!) which will allow you to group on the nodes local name.

Input:

<?xml version="1.0" encoding="UTF-8"?>
<root>
  <row>
    <order>
      <text>some order text 1</text>
    </order>
  </row>

  <row>
    <payment>
      <text>some payment text 1</text>
    </payment>
  </row>

  <row>
    <order>
      <text>some order text 2</text>
    </order>
  </row>

  <row>
    <contact>
      <text>some contact details 1</text>
    </contact>
  </row>

  <row>
    <contact>
      <text>some contact details 2</text>
    </contact>
  </row>
</root>

XSLT:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
  <xsl:output method="text"/>
  <xsl:key name="type" match="row/*" use="local-name()"/>

  <xsl:template match="root">
    <xsl:for-each select="row/*[
      generate-id() = generate-id(key('type', local-name())[1])]">
      <xsl:value-of select="local-name()"/>
      <xsl:text>&#x0a;</xsl:text>
      <xsl:for-each select="key('type', local-name())">
        <xsl:value-of select="concat('  ', position(), '. ')"/>
        <xsl:apply-templates select="text"/>
        <xsl:text>&#x0a;</xsl:text>
      </xsl:for-each>
    </xsl:for-each>
  </xsl:template>

</xsl:stylesheet>

Output:

order
  1. some order text 1
  2. some order text 2
payment
  1. some payment text 1
contact
  1. some contact details 1
  2. some contact details 2
Per T
+1 Cheers for Muenchian. This is likely to perform better than my answer, for large data sets.
LarsH
+1 Good answer, besides the "brick" template...
Alejandro
Ty, some great answers, how would the Muenchian grouping solution change if I had a structure of say Customers containing Persons having rows (such as above), then the key defined above would iterate over all rows across all persons ? I would like it to iterate over all rows for each person element.
This is not what you initially asked for. Accept an answer to your first question and ask another one. Also, please provide the code you've tried to used but failed with, together with sample XML and also desired output for your new example.
Per T
@user408346: If you are going to group by two key (person and row child name) you will have to express that in your key definition. But, please, ask new question.
Alejandro
+1  A: 

Building on @Flynn's answer...

If you have this template for the parent (not shown in your sample):

<xsl:template match="row-parent">
  <xsl:apply-templates select="row">
    <xsl:sort select="name(*[1])" />
  </xsl:apply-templates>
</xsl:template>

Note that by selecting "row", instead of the default (all children, including text nodes), we avoid selecting text nodes that contain whitespace, and which are undesirable for our output.

Then in order to add the section headings, the template for processing the children uses a conditional to see if this is the first row of its section:

<xsl:template match="row">
   <xsl:variable name="childName" select="name(*[1])"/>
   <!-- if this is the first row with an element child of this name -->
   <xsl:if test="not(preceding-sibling::row[name(*[1]) = $childName])">
      <xsl:value-of select="concat('&#10;',
         translate(substring($childName, 1, 1), $lower, $upper),
         substring($childName, 2), '&#10;&#10;')"/>
   </xsl:if>

Then output the data for each row of that group, with the formatting you want:

   <xsl:number level="any" count="row[name(*[1]) = $childName]" format=" 1. "
      from="row-parent"/>
   <xsl:value-of select="normalize-space(*[1])"/>
   <xsl:text>&#10;</xsl:text>
</xsl:template>

As usual, $lower and $upper are defined at the top of the template (or stylesheet) as

<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>

And make the stylesheet use the 'text' output method:

<xsl:output method="text"/>

The output of the above stylesheet on your input (within a <row-parent> wrapper) is:

Contact

 1. some contact details 1
 2. some contact details 2

Order

 1. some order text 1
 2. some order text 2

Payment

 1. some payment text 1

Alternatively, and more robustly, you can use Muenchian grouping: first to group the rows by child element name, then to (output the header for each group and) process all rows within the group.

LarsH
+1  A: 

Besides good answers with grouping method, this stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"&gt;
    <xsl:output method="text"/>
    <xsl:variable name="vSort" select="'|order|payment|contact|'"/>
    <xsl:template match="/">
        <xsl:apply-templates select="*/row">
            <xsl:sort select="string-length(
                                 substring-before($vSort,
                                                  concat('|',
                                                         name(),
                                                         '|')))"/>
        </xsl:apply-templates>
    </xsl:template>
    <xsl:template match="row/*">
        <xsl:variable name="vName" select="name()"/>
        <xsl:variable name="vNumber">
            <xsl:number level="any" count="*[name()=$vName]" from="/"/>
        </xsl:variable>
        <xsl:if test="$vNumber = 1">
            <xsl:value-of select="concat(translate(substring(name(),1,1),
                                                       'opc',
                                                       'OPC'),
                                             substring(name(),2),
                                             '&#xA;')"/>
        </xsl:if>
        <xsl:value-of select="concat($vNumber,'. ',text,'&#xA;')"/>
    </xsl:template>
</xsl:stylesheet>

Output (with a well formed input):

Order
1.  some order text 1
Payment
1.  some payment text 1
2.  some order text 2
Contact
1.  some contact details 1
2.  some contact details 2
Alejandro
Unfortunately, the OP already said that there are many other element types than order/payment/contact, so the select on the `xsl:sort` is likely to be inadequate. Why 1,2,4 though? Surely 1,2,3 would do in this instance?
Flynn1179
@Flynn1179: There is no such thing as general solutions. This transformation produce the desired output. Your sort order (`name(*)`) doesn't: `contact` < `order` < `payment`. The power of 2 sequence is bit mask habit. See my edit for a more reusable sort expression.
Alejandro
Yeah, if you need a specific sort order then a general solution isn't going to work. I was operating on the assumption that the order of the groups in the output wasn't important (or alphabetical would do); if it is, then I agree it's necessary to explicitly declare it in some fashion, in which case a list of `<xsl:apply-templates select="row[order]" />` etc. would do the job, but the OP said that wasn't a solution.
Flynn1179