Home > Net >  Using XSLT, is there a way to make the sort order of a nodeset match the order of a second nodeset?
Using XSLT, is there a way to make the sort order of a nodeset match the order of a second nodeset?

Time:03-04

I am acting on a set of documents that have a <DataTypes> area, which defines the structure of groups of primative datatypes and other structures, and a <Tags> area, which defines the values of instances of these datatypes.

Original XML

<?xml version="1.0" encoding="utf-8" ?>

<Program>
  <DataTypes>
    <DataType Name="String20">
      <Member Name="LEN" DataType="INTEGER" Dimension="0" />
      <Member Name="DATA" DataType="BYTE" Dimension="20" />
    </DataType>
    <DataType Name="UDT_Params">
      <Member Name="InAlarm" DataType="BIT" Dimension="0" />
      <Member Name="SetPoint" DataType="FLOAT" Dimension="0" />
      <Member Name="DwellTime" DataType="INTEGER" Dimension="0" />
      <Member Name="UserName" DataType="String20" Dimension="0" />
    </DataType>
  </DataTypes>
  <Tags>
    <Tag Name="MyParameters" DataType="UDT_Params">
      <Data Name="InAlarm" DataType="BIT" Value="0" />
      <Data Name="SetPoint" DataType="FLOAT" Value="4.5" />
      <Data Name="DwellTime" DataType="INTEGER" Value="10" />
      <Data Name="UserName" DataType="String20">
        <Data Name="LEN" DataType="INTEGER" Value="3" />
        <Data Name="DATA" DataType="String20" >         <!--The system I'm working in shows strings as arrays of BYTES in DataType, -->
          Bob                                           <!--but calls them out as Strings when they are used as tags.  I cannot change it.-->
        </Data>
      </Data>
    </Tag>
  </Tags>
</Program>

Stylesheet

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
>
    <xsl:output method="xml" indent="yes"/>

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

<!--Packing algorithm.  Works fine on datatypes, but not on Tags.-->
<xsl:template name="pack-nodes">
    <xsl:param name="nodes" />
    <!--Omitted for brevity-->
</xsl:template>

  <!--Pack DataTypes-->
  <xsl:variable name="datatypes-packed">
    <xsl:call-template name="pack-nodes">
      <xsl:with-param name="nodes" select="/Program/DataTypes/DataType" />
    </xsl:call-template>
  </xsl:variable>

  <!--Write DataTypes to output.-->
  <xsl:template match="/Program/DataTypes">
    <xsl:copy>
      <xsl:for-each select="msxsl:node-set($datatypes-packed)">
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
  
  <!--Pack tags-->
  <xsl:variable name="tags-packed">
    <xsl:call-template name="pack-nodes">
      <xsl:with-param name="nodes" select="/Program/Tags/Tag" />
    </xsl:call-template>
  </xsl:variable>
  
  <!--Write Tags to output.-->
  <xsl:template match="/Program/Tags">
    <xsl:copy>
      <xsl:for-each select="msxsl:node-set($tags-packed)">
        <xsl:copy-of select="."/>
      </xsl:for-each>
    </xsl:copy>
  </xsl:template>
</xsl:stylesheet>

Result

<?xml version="1.0" encoding="utf-8"?>
<Program>
    <DataTypes>
        <DataType Name="String20">
            <Member Name="LEN" DataType="INTEGER" Dimension="0"/>
            <Member Name="DATA" DataType="BYTE" Dimension="20"/>
        </DataType>
        <DataType Name="Parameters" DataType="UDT_Params">
            <Member Name="UserName" DataType="String20" Dimension="0"/>
            <Member Name="SetPoint" DataType="FLOAT" Dimension="0"/>
            <Member Name="DwellTime" DataType="INTEGER" Dimension="0"/>
            <Member Name="InAlarm" DataType="BIT" Dimension="0"/>
        </DataType>
    </DataTypes>
    <Tags>
        <Tag Name="MyParameters" DataType="UDT_Params">
            <Data Name="UserName" DataType="String20">
                <Data Name="DATA" DataType="String20">      <!--Note that DATA comes before LEN -->
                    Bob                                     
                </Data>
                <Data Name="LEN" DataType="INTEGER" Value="3"/>
            </Data>
            <Data Name="SetPoint" DataType="FLOAT" Value="4.5"/>
            <Data Name="DwellTime" DataType="INTEGER" Value="10"/>
            <Data Name="InAlarm" DataType="BIT" Value="0"/>
        </Tag>
    </Tags>
</Program>

My operations on the DataTypes section adds nodes and changes the node order. For the section to work correctly, the tag elements must match the contents and order of their respective datatypes, exactly.

If I keep a variable in memory of the final state of the DataSet nodes, is there a simple way to have the tag nodes look up their dataset (via the Structure and StructureMember @DataSet attributes, and sort their members accordingly?

I'm having trouble figuring out where to start.

NOTE: Transformation must be in XSLT 1.0. I'm using .Net, and don't want to introduce a lot of dependencies on external libraries.

CodePudding user response:

It's a bit tricky in XSLT 1.0 (isn't everything?) but a technique that sometimes works is to construct a variable $tokens containing the list of tokens in the required order, for example "|Description|Name|ProcessEntityIndex|Severity|...", and then sort on select="string-length(substring-before($tokens, concat('|',@Name)))".

CodePudding user response:

Using Michael Kay's suggesting for sorting, I ended up with the following:

Stylesheet

        <?xml version="1.0" encoding="utf-8"?>
        <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
            xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl"
        >
            <xsl:output method="xml" indent="yes"/>
        
    <!--Skipped to new part -->
        
          <xsl:template name="sort-by-datatype">
            <xsl:param name="tags" />
            <xsl:param name="datatypes" />
            <xsl:for-each select="msxsl:node-set($tags)">
              <!--First, do an edge-check.-->
              <xsl:variable name="edge">
                <xsl:call-template name="edge-check">
                  <xsl:with-param name="n1" select="." />
                </xsl:call-template>
              </xsl:variable>
              <xsl:choose>
                <!--No children, nothing to sort.  Just do a deep copy.-->
                <xsl:when test="$edge = 'true'">
                  <xsl:copy-of select="."/>
                </xsl:when>
                <xsl:otherwise>
                  <!--Search for datatype in the DataTypes nodeset, and use it to create a list of members in order.-->
                  <xsl:variable name="tag-datatype" select="./@DataType" />
                  <xsl:variable name="tokens-untrimmed">
                    <xsl:for-each select="msxsl:node-set($datatypes)/DataType[@Name = $tag-datatype]/Member">
                      <xsl:value-of select="concat(' | ', @Name)"/>
                    </xsl:for-each>
                  </xsl:variable>
                  <xsl:variable name="tokens" select="substring-after($tokens-untrimmed, '|')" />
                  <xsl:choose>
                    <!--If tokens string is empty (maybe because we couldn't find the datatype?), just copy the tag as it is, then recurse.-->
                    <xsl:when test="string-length($tokens) = 0">
                      <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <xsl:call-template name="sort-by-datatype">
                          <xsl:with-param name="tags" select="." />
                          <xsl:with-param name="datatypes" select="$datatypes" />
                        </xsl:call-template>
                      </xsl:copy>
                    </xsl:when>
                    <!--Otherwise, sort members in the same order as datatype-->
                    <xsl:otherwise>
                      <!--Build variable with sorted members.-->
                      <xsl:variable name="tag-members-sorted">
                        <xsl:for-each select="*">
                          <xsl:sort data-type="number" order="ascending" select="string-length(substring-before($tokens, concat(' | ', @Name)))" />   <!--Magic Sort Algorithm-->-->
                          <xsl:copy-of select="."/>
                        </xsl:for-each>
                      </xsl:variable>
                      <!--Copy the parent node node.-->
                      <xsl:copy>
                        <xsl:copy-of select="@*"/>
                        <!--Now sort and copy the children.-->
                        <xsl:for-each select="msxsl:node-set($tag-members-sorted)/*">
                          <!--Recurse.  This copies the child node.-->
                          <xsl:call-template name="sort-by-datatype">
                            <xsl:with-param name="tags" select="." />
                            <xsl:with-param name="datatypes" select="$datatypes" />
                          </xsl:call-template>
                        </xsl:for-each>
                      </xsl:copy>
                    </xsl:otherwise>
                  </xsl:choose>
                </xsl:otherwise>
              </xsl:choose>
            </xsl:for-each>
          </xsl:template>
          
          <!--Pack tags-->
          <xsl:variable name="tags-packed">
            <xsl:call-template name="sort-by-datatype">
              <xsl:with-param name="tags" select="/Program/Tags/Tag" />
              <xsl:with-param name="datatypes" select="$datatypes-packed" />
            </xsl:call-template>
          </xsl:variable>
          
<!--Skipped for brevity-->

        </xsl:stylesheet>
  • Related