Search code examples
xmlxsltxslt-grouping

Group elements in XSLT even if grouping key is missing


I have the following XML:

<cars>
  <car type='toyota'>
   <model>Yaris</model>
   <year>1998</year>
   <company>TOYOTA</company>
  </car>
  <car type='kia'>
   <model>Optima</model>
   <year>2002</year>
   <company>KIA</company>
  </car>
  <car type='kia'>
   <model>CERATO</model>
   <year>2009</year>
   <company>KIA</company>
  </car>
  <car type='bmw'>
   <model>M3</model>
   <year>2016</year>
   <company>BMW</company>
  </car>
  <car type='bmw'>
   <model>X5</model>
   <year>2010</year>
  </car>
  <car type='bmw'>
   <model>335i</model>
   <year>2010</year>
   <company>BMW</company>
  </car>
 </cars>

I want to group the cars by company element with sorting (alpha, ascending) on the same element. the output should be something like:

BMW: M3, X5, 335i
KIA: Optima, CERATO
TOYOTA: Yaris

The thing is that the car element might not contain a company node, In this case the car/@type value must be used to add the element to the correct group. How can I map the value of the @type attribute to the correct group that is based on the company value?


Solution

  • A little know feature of XSLT (at least in version 2.0) is that when you create a list, e.g. (xx, yy, zz), this list actually containts only existing values. If e.g. xx value was empty, it would not be a part of the result list.

    So if you write [1] after it, you actually get the first non-empty element from the expression list between parentheses.

    In your comment, as of 5.08, you asked for a solution that other type shoud be treated as TOYOTA. It is possible, using if ... then ... else ..., in this case: if (@type = 'other') then 'TOYOTA' else @type

    So you can write the script the following way:

    <?xml version="1.0" encoding="UTF-8" ?>
    <xsl:transform version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
      <xsl:output method="text"/>
    
      <xsl:template match="cars">
        <xsl:for-each-group select="car" group-by="upper-case(
          (company, if (@type = 'other') then 'TOYOTA' else @type)[1])">
          <xsl:sort select="current-grouping-key()"/>
          <xsl:value-of select="current-grouping-key()"/>
          <xsl:text>: </xsl:text>
          <xsl:value-of select="string-join(current-group()/model, ', ')"/>
          <xsl:text>&#xA;</xsl:text>
        </xsl:for-each-group>
      </xsl:template>
    </xsl:transform>
    

    As you can see:

    • (company, if (@type = 'other') then 'TOYOTA' else @type) is the source list (argument of upper-case),
    • [1] takes first element from what has been created.

    I moved the call to upper-case to the "outer level", assuming that model can also be written in lower case.