简体   繁体   中英

XSL grouping xml by element

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.

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

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'.

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.

Besides good answers with grouping method, this stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM