Sort parent nodes XML depends on child nodes value

2019-02-25 07:55发布

问题:

I have a following XML

<Root> 
  <Element A/>
  <Element B/>
  <Data1> 
    <DataElement/> 
    <Values>
       <Value>2222</Value> 
       <Name>field1</Name>
    </Values> 
    <Values>
       <Value>ABC</Value> 
       <Name>field2</Name>
    </Values> 
  </Data1> 
  <Data2> 
    <DataElement/> 
    <Values>
       <Value>1111</Value> 
       <Name>field1</Name>
    </Values> 
    <Values>
       <Value>XYZ</Value> 
       <Name>field2</Name>
    </Values> 
  </Data2>
  <DataN> 
    ... 
  </DataN> 
</Root> 

I need to get the same XML with Data section sorted by "Value" of specified field name, for exmple: sort by "field1" will return

    <Root> 
  <Element A/>
  <Element B/>
  <Data2> 
    <DataElement/> 
    <Values>
       <Value>1111</Value> 
       <Name>field1</Name>
    </Values> 
    <Values>
       <Value>XYZ</Value> 
       <Name>field2</Name>
    </Values> 
  </Data2>
  <Data1> 
    <DataElement/> 
    <Values>
       <Value>2222</Value> 
       <Name>field1</Name>
    </Values> 
    <Values>
       <Value>ABC</Value> 
       <Name>field2</Name>
    </Values> 
  </Data1>   
  <DataN> 
    ... 
  </DataN> 
</Root> 

Also, I have to send name of sort field as parameter...

回答1:

This transformation implements exactly the stated requirements. It takes special care to preserve the exact order of the elements that are not to be sorted. No other answer at present does this:

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

 <xsl:param name="vField" select="'field1'"/>
 <xsl:param name="pSortType" select="'number'"/>

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

   <xsl:template match="*[starts-with(name(),'Data')]">
    <xsl:variable name="vPos" select=
      "count(preceding-sibling::*[starts-with(name(),'Data')])+1"/>

  <xsl:for-each select="/*/*[starts-with(name(),'Data')]">
   <xsl:sort select="Values[Name=$vField]/Value"
             data-type="{$pSortType}"/>
   <xsl:if test="position() = $vPos">
    <xsl:copy-of select="."/>
   </xsl:if>
  </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

When applied on the following XML document (the same as the provided one, but with an additional <Element C=""/> inserted between Data1 and Data2 so that we can verify the preservation of ordering of the non-sorted elements):

<Root>
    <Element A=""/>
    <Element B=""/>
    <Data1>
        <DataElement/>
        <Values>
            <Value>2222</Value>
            <Name>field1</Name>
        </Values>
        <Values>
            <Value>ABC</Value>
            <Name>field2</Name>
        </Values>
    </Data1>
        <Element C=""/>
    <Data2>
        <DataElement/>
        <Values>
            <Value>1111</Value>
            <Name>field1</Name>
        </Values>
        <Values>
            <Value>XYZ</Value>
            <Name>field2</Name>
        </Values>
    </Data2>
</Root>

produces the wanted, correct result -- note that the position of <Element C=""/> is preserved:

<Root>
   <Element A=""/>
   <Element B=""/>
   <Data2>
      <DataElement/>
      <Values>
         <Value>1111</Value>
         <Name>field1</Name>
      </Values>
      <Values>
         <Value>XYZ</Value>
         <Name>field2</Name>
      </Values>
   </Data2>
   <Element C=""/>
   <Data1>
      <DataElement/>
      <Values>
         <Value>2222</Value>
         <Name>field1</Name>
      </Values>
      <Values>
         <Value>ABC</Value>
         <Name>field2</Name>
      </Values>
   </Data1>
</Root>


回答2:

I think you can go with this simple transform:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:output indent="yes"/>
    <xsl:strip-space elements="*"/>

    <xsl:param name="field" select="'field1'"/>

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

    <xsl:template match="Root">
        <xsl:copy>
            <xsl:apply-templates select="@*|*">
                <xsl:sort select="Values[Name=$field]/
                    Value[string(number(.))!='NaN']" 
                    data-type="number"/>
                <xsl:sort select="Values[Name=$field]/
                    Value[string(number(.))='NaN']"/>
            </xsl:apply-templates>
        </xsl:copy>
    </xsl:template>

</xsl:stylesheet>

When applied to your input (corrected to make it well-formed):

<Root> 
    <Element A=""/>
    <Element B=""/>
    <Data1> 
        <DataElement/> 
        <Values>
            <Value>2222</Value> 
            <Name>field1</Name>
        </Values> 
        <Values>
            <Value>ABC</Value> 
            <Name>field2</Name>
        </Values> 
    </Data1> 
    <Data2> 
        <DataElement/> 
        <Values>
            <Value>1111</Value> 
            <Name>field1</Name>
        </Values> 
        <Values>
            <Value>XYZ</Value> 
            <Name>field2</Name>
        </Values> 
    </Data2> 
</Root>

produces:

<Root>
    <Element A=""></Element>
    <Element B=""></Element>
    <Data2>
        <DataElement></DataElement>
        <Values>
            <Value>1111</Value>
            <Name>field1</Name>
        </Values>
        <Values>
            <Value>XYZ</Value>
            <Name>field2</Name>
        </Values>
    </Data2>
    <Data1>
        <DataElement></DataElement>
        <Values>
            <Value>2222</Value>
            <Name>field1</Name>
        </Values>
        <Values>
            <Value>ABC</Value>
            <Name>field2</Name>
        </Values>
    </Data1>
</Root>

Explanation:

  • Use of identity rule to copy every thing as is.
  • override the required DataN elements and simple application of sort condition based on $field input parameter.
  • data-type for sorting dynamically changes according to element field type