tags:

views:

37

answers:

2

I'm wondering if/how the following can be accomplished in XSLT. If not, what would you use? (I used OmniMark, but I would like to know if this is possible in XSLT.)

Here's an example of the input XML:

<?xml version="1.0" encoding="UTF-8"?>
<fragment>
    <firstElem>
        <secondElem>D12</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J7-10</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C2-4</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP20-25</secondElem>
    </firstElem>
</fragment>

What I needed to do was take the text content of the secondElem element and create as many new firstElem elements as required. The '-' was a "through", so 'A3J7-10' was really 'A3J7' through 'A3J10' (A3J7, A3J8, A3J9, A3J10). (Sometimes the "through" would be fairly large, like A1B2C1-150 (A1B2C1 through A1B2C150).)

If there was no dash, nothing needed to be done.

Here's an example of the output XML:

<?xml version="1.0" encoding="UTF-8"?>
<fragment>
    <firstElem>
        <secondElem>D12</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J7</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J8</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J9</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J10</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C2</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C3</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C4</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP20</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP21</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP22</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP23</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP24</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP25</secondElem>
    </firstElem>
</fragment>

Is there a way to do this in XSLT?

Thanks!

+2  A: 

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" exclude-result-prefixes="xsl">
    <xsl:template match="@*|node()">
        <xsl:copy>
            <xsl:apply-templates select="@*|node()"/>
        </xsl:copy>
    </xsl:template>
    <xsl:template match="firstElem[contains(.,'-')]" name="firstElem">
        <xsl:param name="from">
            <xsl:call-template name="getfrom"/>
        </xsl:param>
        <xsl:param name="base" select="normalize-space(substring-before(.,concat($from,'-')))"/>
        <xsl:param name="to" select="substring-after(.,'-')"/>
        <xsl:if test="$to >= $from">
            <firstElem>
                <secondElem>
                    <xsl:value-of select="concat($base,$from)"/>
                </secondElem>
            </firstElem>
            <xsl:call-template name="firstElem">
                <xsl:with-param name="from" select="$from + 1"/>
                <xsl:with-param name="base" select="$base"/>
                <xsl:with-param name="to" select="$to"/>
            </xsl:call-template>
        </xsl:if>
    </xsl:template>
    <xsl:template name="getfrom">
        <xsl:param name="string" select="substring-before(.,'-')"/>
        <xsl:variable name="last" select="substring($string,string-length($string))"/>
        <xsl:if test="contains('0123456789',$last)">
            <xsl:call-template name="getfrom">
                <xsl:with-param name="string" select="substring($string,1,string-length($string)-1)"/>
            </xsl:call-template>
            <xsl:value-of select="$last"/>
        </xsl:if>
    </xsl:template>
</xsl:stylesheet>

Output:

<fragment>
    <firstElem>
        <secondElem>D12</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J7</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J8</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J9</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J10</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C2</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C3</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C4</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP20</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP21</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP22</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP23</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP24</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP25</secondElem>
    </firstElem>
</fragment>
Alejandro
@Alejandro: Thank you very much for the 1.0 answer!
DevNull
+2  A: 

Here is an XSLT 2.0 solution:

<xsl:stylesheet version="2.0"
    xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
  exclude-result-prefixes="xs">
    <xsl:output omit-xml-declaration="yes" indent="yes"/>

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

 <xsl:template match="firstElem[contains(secondElem, '-')]">
   <xsl:analyze-string select="secondElem"
    regex="(^.*[^\d])([\d]*)-([\d]*)">

     <xsl:matching-substring>
        <xsl:variable name="vbaseName" select="regex-group(1)"/>
        <xsl:variable name="vstart" select="regex-group(2)"/>
        <xsl:variable name="vend" select="regex-group(3)"/>

        <xsl:for-each select=
          "xs:integer($vstart) to xs:integer($vend)">
                <firstElem>
                    <secondElem>
                      <xsl:value-of select="concat($vbaseName, .)"/>
                    </secondElem>
                </firstElem>
        </xsl:for-each>
     </xsl:matching-substring>
   </xsl:analyze-string>
 </xsl:template>
</xsl:stylesheet>

When the above transformation is applied on the provided XML document:

<fragment>
    <firstElem>
        <secondElem>D12</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>A3J7-10</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>C2-4</secondElem>
    </firstElem>
    <firstElem>
        <secondElem>QW9R7NP20-25</secondElem>
    </firstElem>
</fragment>

the wanted, correct result is produced:

<fragment>
    <firstElem>
        <secondElem>D12</secondElem>
    </firstElem>
    <firstElem>
      <secondElem>A3J7</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>A3J8</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>A3J9</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>A3J10</secondElem>
   </firstElem>
    <firstElem>
      <secondElem>C2</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>C3</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>C4</secondElem>
   </firstElem>
    <firstElem>
      <secondElem>QW9R7NP20</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>QW9R7NP21</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>QW9R7NP22</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>QW9R7NP23</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>QW9R7NP24</secondElem>
   </firstElem>
   <firstElem>
      <secondElem>QW9R7NP25</secondElem>
   </firstElem>
</fragment>

Do note:

  1. The use of regular expressions.

  2. Sub-expression capturing and the regex-group() function.

  3. The use of <xsl:analyze-string> and <xsl:matching-substring> .

  4. The use of the to operator to create the sequence to be used in <xsl:for-each>

  5. The use of <xsl:for-each> on a sequence of non-nodes (integers).

Dimitre Novatchev
@Dimitre: +1 for XSLT 2.0 solution. I want to be an XSLT2 dude, ja!
Alejandro
@Dimitre: Thank you very much. That is the first I've seen `<xsl:analyze-string>` used. That will come in very handy.
DevNull