Alp
Alp

Reputation: 29739

Get substring of each element in a list and get them distinct in XSLT

I need to transform an XML file with XSLT, and this task is kinda tricky.

I have attributes with the name attr_1000_a whereas the number and the suffix are dynamic, so that attr_2000_b is valid too.

Further, there are <row> elements which combine related data. I need to transform them so that equally numbered attributes (i.e. attr_1000_a and attr_1000_b) are put into the same element.

Let me give you an example. Following input XML:

<root>
  <row id="1">
    <foo attr_1000_a="true">1</foo>
    <foo attr_1000_b="true">2</foo>
    <foo attr_1000_c="true">3</foo>
  </row>
  <row id="2">
    <foo attr_1000_a="true" attr_1000_b="true" attr_1000_c="true">10</foo>
    <foo attr_2000_a="true" attr_2000_b="true" attr_2000_c="true">20</foo>
  </row>
  <row id="3">
    <foo attr_1000_a="true" attr_2000_a="true" attr_3000_a="true">100</foo>
    <foo attr_1000_b="true" attr_2000_b="true" attr_3000_b="true">200</foo>
    <foo attr_1000_c="true" attr_2000_c="true" attr_3000_c="true">300</foo>
  </row>
</root>

You can see that the attributes can be combined in several ways, which makes the transformation difficult. Each attribute is unique in each <row> but can be located in any <foo> element. Also, each <foo> can have an arbitrary number of attributes.

The desired result:

<result>
  <row id="1">
    <field attr="1000">
      <a>1</a>
      <b>2</b>
      <c>3</c>
    </field>
  </row>
  <row id="2">
    <field attr="1000">
      <a>10</a>
      <b>10</b>
      <c>10</c>
    </field>
    <field attr="2000">
      <a>20</a>
      <b>20</b>
      <c>20</c>
    </field>
  </row>
  <row id="3">
    <field attr="1000">
      <a>100</a>
      <b>200</b>
      <c>300</c>
    </field>
    <field attr="2000">
      <a>100</a>
      <b>200</b>
      <c>300</c>
    </field>
    <field attr="3000">
      <a>100</a>
      <b>200</b>
      <c>300</c>
    </field>
  </row>
</result>

I think i have to somehow get a list of all numbers in a row (for example 1000, 2000 and 3000) and then iterate through all elements that have such an attriute.

How can i do this using XSLT? Is this even possible?

Upvotes: 1

Views: 1496

Answers (2)

jos
jos

Reputation: 823

Quick and Dirty, this xslt

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

    <xsl:output indent="yes"/>

    <xsl:template match="root">
        <result>
         <xsl:apply-templates select="@*|node()"/>
        </result>
    </xsl:template>

    <xsl:template match="foo/@*">
        <xsl:element name="{substring-after(local-name(),'000_')}">
            <xsl:value-of select=".."/>
        </xsl:element>
    </xsl:template>

    <xsl:template match="row">
        <row id="{@id}">
            <xsl:for-each-group select="foo/@*" group-by="substring(local-name(),1,9)">
                <field attr="{substring-after(current-grouping-key(),'attr_')}">
                   <xsl:apply-templates select="current-group()"/>
                </field>
            </xsl:for-each-group>
        </row>
    </xsl:template>

    <xsl:template match="foo">
        <xsl:apply-templates  select="@*"/>
    </xsl:template>

     <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>   
</xsl:stylesheet>

applied to this input

<?xml version="1.0" encoding="UTF-8"?>
<root>
    <row id="1">
        <foo attr_1000_a="true">1</foo>
        <foo attr_1000_b="true">2</foo>
        <foo attr_1000_c="true">3</foo>
    </row>
    <row id="2">
        <foo attr_1000_a="true" attr_1000_b="true" attr_1000_c="true">10</foo>
        <foo attr_2000_a="true" attr_2000_b="true" attr_2000_c="true">20</foo>
    </row>
    <row id="3">
        <foo attr_1000_a="true" attr_2000_a="true" attr_3000_a="true">100</foo>
        <foo attr_1000_b="true" attr_2000_b="true" attr_3000_b="true">200</foo>
        <foo attr_1000_c="true" attr_2000_c="true" attr_3000_c="true">300</foo>
    </row>
</root>

yields this result

<?xml version="1.0" encoding="UTF-8"?>
<result>
    <row id="1">
      <field attr="1000">
         <a>1</a>
         <b>2</b>
         <c>3</c>
      </field>
   </row>
    <row id="2">
      <field attr="1000">
         <a>10</a>
         <b>10</b>
         <c>10</c>
      </field>
      <field attr="2000">
         <a>20</a>
         <b>20</b>
         <c>20</c>
      </field>
   </row>
    <row id="3">
      <field attr="1000">
         <a>100</a>
         <b>200</b>
         <c>300</c>
      </field>
      <field attr="2000">
         <a>100</a>
         <b>200</b>
         <c>300</c>
      </field>
      <field attr="3000">
         <a>100</a>
         <b>200</b>
         <c>300</c>
      </field>
   </row>
</result>

the magic is in

    <xsl:element name="{substring-after(local-name(),'000_')}">
        <xsl:value-of select=".."/>
    </xsl:element>

this creates the a/b/c elements with a dynamic name and travels up one node to get the value from the parent node (we're currently in the attribute).

and in

        <xsl:for-each-group select="foo/@*" group-by="substring(local-name(),1,9)">
            <field attr="{substring-after(current-grouping-key(),'attr_')}">
               <xsl:apply-templates select="current-group()"/>
            </field>
        </xsl:for-each-group>

which regroups all attributes (foo/@*) using part of their name (substring(local-name(),1,9)). The first are subsequently available as current-group(), the latter as current-grouping-key(), as you can see.

Upvotes: 3

Martin Honnen
Martin Honnen

Reputation: 167696

Here is a sample stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:strip-space elements="*"/>
<xsl:output indent="yes"/>

<xsl:key name="k1" 
  match="row/foo/@*" 
  use="concat(generate-id(../..), '|', substring-before(substring-after(local-name(), '_'), '_'))"/>

<xsl:template match="root">
  <result>
    <xsl:apply-templates/>
  </result>
</xsl:template>

<xsl:template match="row">
  <xsl:copy>
    <xsl:copy-of select="@*"/>
    <xsl:apply-templates select="foo/@*[generate-id() = generate-id(key('k1', concat(generate-id(../..), '|', substring-before(substring-after(local-name(), '_'), '_')))[1])]" mode="field"/>
  </xsl:copy>
</xsl:template>

<xsl:template match="foo/@*" mode="field">
  <field attr="{substring-before(substring-after(local-name(), '_'), '_')}">
    <xsl:apply-templates select="key('k1', concat(generate-id(../..), '|', substring-before(substring-after(local-name(), '_'), '_')))"/>
  </field>
</xsl:template>

<xsl:template match="foo/@*">
  <xsl:element name="{substring-after(substring-after(local-name(), '_'), '_')}">
    <xsl:value-of select=".."/>
  </xsl:element>
</xsl:template>

</xsl:stylesheet>

Upvotes: 3

Related Questions