2
votes

I am trying to solve this for someone else and have run into an issue myself.

I have the XML:

<Process>
  <name>Pro1</name>
  <duration>Dur1</duration>
  <time>Time1</time>
  <name>Pro2</name>
  <duration>Dur2</duration>
  <time>Time2</time>
  <name>Pro3</name>
  <duration>Dur3</duration>
  <time>Time3</time>
  <name>Pro4</name>
  <duration>Dur4</duration>
  <time>Time4</time>
  <name>Pro5</name>
  <duration>Dur5</duration>
  <time>Time5</time>
</Process>

Output:

<Process>
  <Process_Info>
    <name>Pro1</name>
    <duration>Dur1</duration>
    <time>Time1</time>
  </Process_Info>
  <Process_Info>
    <name>Pro2</name>
    <duration>Dur2</duration>
    <time>Time2</time>
  </Process_Info>
  <Process_Info>
    <name>Pro3</name>
    <duration>Dur3</duration>
    <time>Time3</time>
  </Process_Info>
  <Process_Info>
    <name>Pro4</name>
    <duration>Dur4</duration>
    <time>Time4</time>
  </Process_Info>
  <Process_Info>
    <name>Pro5</name>
    <duration>Dur5</duration>
    <time>Time5</time>
  </Process_Info>
</Process>

Using XSLT:

<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

  <xsl:variable name ="varProcess" select ="Process"/>

  <xsl:template match="Process">
    <xsl:element name="Process">
      <xsl:for-each select ="name">
        <xsl:variable name ="posName" select ="position()"/>
        <xsl:element name ="Process_Info">
          <xsl:copy-of select ="."/>
          <xsl:copy-of select="$varProcess/duration[$posName]"/>
          <xsl:copy-of select="$varProcess/time[$posName]"/>
        </xsl:element>
      </xsl:for-each>
    </xsl:element>
  </xsl:template>
</xsl:stylesheet>

However, the <duration> and <time> nodes will not always be present and the <name> is the only guarenteed node. Therefore if one is missing my position() selecting fails.

How can I change the XSLT to allow for it to work even when <duration> and/or <time> does not exist.

My theory would be that you select the two nodes below the current name node and if they are <duration> or <time> they are copied? But not sure how that would implement either.

Example of current output causing issue.

Input:

<Process>
  <name>Pro1</name>
  <duration>Dur1</duration>
  <time>Time1</time>
  <name>Pro2</name>
  <duration>Dur2</duration>
  <time>Time2</time>
  <name>Pro3</name>
  <duration>Dur3</duration>
  <time>Time3</time>
  <name>Pro4</name>
  <time>Time4</time>
  <name>Pro5</name>
  <duration>Dur5</duration>
</Process>

Output:

<Process>
  <Process_Info>
    <name>Pro1</name>
    <duration>Dur1</duration>
    <time>Time1</time>
  </Process_Info>
  <Process_Info>
    <name>Pro2</name>
    <duration>Dur2</duration>
    <time>Time2</time>
  </Process_Info>
  <Process_Info>
    <name>Pro3</name>
    <duration>Dur3</duration>
    <time>Time3</time>
  </Process_Info>
  <Process_Info>
    <name>Pro4</name>
    <duration>Dur5</duration> <!-- Should be in the below process_info -->
    <time>Time4</time>
  </Process_Info>
  <Process_Info>
    <name>Pro5</name>
  </Process_Info>
</Process>
3

3 Answers

2
votes

This does the trick, albeit a bit 'manual' in approach. There may be more elegant ways to achieve this

<xsl:template match="Process">
    <xsl:element name="Process">
        <xsl:for-each select ="name">
            <xsl:element name ="Process_Info">
                <xsl:copy-of select ="."/>
                <xsl:variable name="firstSib" select="local-name(following-sibling::*[1])" />
                <xsl:variable name="secondSib" select="local-name(following-sibling::*[2])" />
                <xsl:choose>
                    <xsl:when test="$firstSib='duration'">
                        <xsl:copy-of select="following-sibling::*[1]"/>
                        <xsl:choose>
                            <xsl:when test="$secondSib='time'">
                                <xsl:copy-of select="following-sibling::*[2]"/>
                            </xsl:when>
                            <xsl:otherwise>
                                <time>SomeDefaultValueForMissingTime</time>
                            </xsl:otherwise>
                        </xsl:choose>
                    </xsl:when>
                    <xsl:when test="$firstSib='time'">
                        <duration>SomeDefaultValueForMissingDuration</duration>
                        <xsl:copy-of select="following-sibling::*[1]"/>
                    </xsl:when>
                    <xsl:otherwise>
                        <duration>SomeDefaultValueForMissingDuration</duration>
                        <time>SomeDefaultValueForMissingTime</time>
                    </xsl:otherwise>
                </xsl:choose>
            </xsl:element>
        </xsl:for-each>
    </xsl:element>
</xsl:template>

Input :

<Process>
    <name>Pro1</name>
    <duration>Dur1</duration>
    <time>Time1</time>
    <name>NameMissingDuration</name>
    <time>TimeMissingDuration</time>
    <name>NameMissingTime</name>
    <duration>DurMissingTime</duration>
    <name>NameMissingBoth</name>
    <name>NormalName</name>
    <duration>NormalDuration</duration>
    <time>NormalTime</time>
</Process>

Output

<Process>
  <Process_Info>
    <name>Pro1</name>
    <duration>Dur1</duration>
    <time>Time1</time>
  </Process_Info>
  <Process_Info>
    <name>NameMissingDuration</name>
    <duration>SomeDefaultValueForMissingDuration</duration>
    <time>TimeMissingDuration</time>
  </Process_Info>
  <Process_Info>
    <name>NameMissingTime</name>
    <duration>DurMissingTime</duration>
    <time>SomeDefaultValueForMissingTime</time>
  </Process_Info>
  <Process_Info>
    <name>NameMissingBoth</name>
    <duration>SomeDefaultValueForMissingDuration</duration>
    <time>SomeDefaultValueForMissingTime</time>
  </Process_Info>
  <Process_Info>
    <name>NormalName</name>
    <duration>NormalDuration</duration>
    <time>NormalTime</time>
  </Process_Info>
</Process>
2
votes

Here's an alternative solution without xsl:if.

This demonstrates using the generate-id() function for testing for node equality in selecting the first time (or duration) element which follows the current name element and whose first preceding name element is the current element.

<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

  <xsl:template match="Process">
    <xsl:copy>
      <xsl:apply-templates select="name"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="name">
    <xsl:variable name="this" select="generate-id()"/>
    <Process_Info>
      <xsl:copy-of select="."/>
      <xsl:copy-of select="following-sibling::duration
                           [generate-id(preceding-sibling::name[1]) = $this]
                           [1]"/>
      <xsl:copy-of select="following-sibling::time
                           [generate-id(preceding-sibling::name[1]) = $this]
                           [1]"/>
    </Process_Info>
  </xsl:template>

</xsl:stylesheet>
1
votes

Based off nonnb's response so all credit goes to him, the final XSLT being used. My mistake for not including in the question that I don't need a filler node.

<xsl:stylesheet version="1.0" 
                xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>

  <xsl:template match="Process">
    <xsl:element name="Process">
      <xsl:for-each select ="name">
        <xsl:element name ="Process_Info">
          <xsl:copy-of select ="."/>
          <xsl:variable name="firstSib" select="local-name(following-sibling::*[1])" />
          <xsl:variable name="secondSib" select="local-name(following-sibling::*[2])" />
          <xsl:if test ="($firstSib='duration') or ($firstSib='time')">
            <xsl:copy-of select="following-sibling::*[1]"/>
          </xsl:if>
          <xsl:if test ="($secondSib='duration') or ($secondSib='time')">
            <xsl:copy-of select="following-sibling::*[2]"/>
          </xsl:if>
        </xsl:element>
      </xsl:for-each>
    </xsl:element>
  </xsl:template>
</xsl:stylesheet>