Dynamic grouping on element and attributes names

2020-04-27 19:05发布

I would like to categorize results from an XPath under headings by element name (and then by same attribute names). Note: XML data could be inconsistent and some elements with the same name could have different attributes, therefore they need different headings.

I can't seem to write out my problem in words, so it might be best to use an example..

XML:

<pets>  
    <dog name="Frank" cute="yes" color"brown" type="Lab"/>
    <cat name="Fluffy" cute="yes" color="brown"/>
    <cat name="Lucy" cute="no" color="brown"/>
    <dog name="Spot" cute="no" color="brown"/>
    <dog name="Rover" cute="yes" color="brown"/>
    <dog name="Rupert" cute="yes" color="beige" type="Pug"/>
    <cat name="Simba" cute="yes" color="grey"/>
    <cat name="Princess" color="brown"/>

</pets>

XPath:

//*[@color='brown']

What the output should sort of look like (with the different headings for different elements):

ElementName  Color   Cute     Name     Type   
Dog          Brown   Yes      Frank    Lab


ElementName  Color   Cute     Name       
Dog          Brown   No       Spot    
Dog          Brown   Yes      Rover


ElementName  Color   Cute     Name     
Cat          Brown   Yes      Fluffy    
Cat          Brown   No       Lucy



ElementName  Color   Name     
Cat          Brown   Princess  

The XSL I currently have (simplified!):

<xsl:apply-templates select="//*[@color='brown']" mode="result">
    <xsl:sort select="name()" order="ascending"/>
</xsl:apply-templates>


<xsl:template match="@*|node()" mode="result">
    <tr>
        <th align="left">Element</th>

        <xsl:for-each select="@*">
            <xsl:sort select="name()" order="ascending"/>
            <th align="left">
                <xsl:value-of select="name()"/>
            </th>
        </xsl:for-each>
    </tr>

    <tr>
        <td align="left">
            <xsl:value-of select="name()"/>
         </td>
         <xsl:for-each select="@*">
             <xsl:sort select="name()" order="ascending"/>
             <td align="left">
                 <xsl:value-of select="."/>
             </td>
         </xsl:for-each>
     </tr>
</xsl:template>

This above XSL sorts them correctly in the way I want.. but now I need some sort of check to see which elements have the same name, and then if they have the same name, do they have the same attributes. Once I complete this check, I can then put general "Headings" above sets of records with matching element name and attributes.

I figured I could use xsl:choose xsl:when and do some tests. I was thinking (after the correct ordering has been done):

If element name != previous element name
    create headings
Else if all attributes != all previous element's attributes
    create headings

I guess my biggest problem is, is that I don't know how to check what the previous returned data set was... Can someone please tell me how to do this?

Or if I am approaching this wrong.. lead me to a better solution?

Hope that all made sense! Let me know if you need clarification!

Thanks in advance for your patience and responses! :)

2条回答
Rolldiameter
2楼-- · 2020-04-27 19:43

This stylesheet:

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="ByName-AttNum" match="*/*[@color='brown']" use="concat(name(),'++',count(@*))"/>
    <xsl:template match="/">
        <html>
            <xsl:apply-templates/>
        </html>
    </xsl:template>
    <xsl:template match="*/*[generate-id(.) = generate-id(key('ByName-AttNum',concat(name(),'++',count(@*)))[1])]">
        <table>
            <tr>
                <th>ElementName</th>
                <xsl:apply-templates select="@*" mode="headers">
                    <xsl:sort select="name()"/>
                </xsl:apply-templates>
            </tr>
            <xsl:apply-templates select="key('ByName-AttNum',concat(name(),'++',count(@*)))" mode="list"/>
        </table>
    </xsl:template>
    <xsl:template match="*" mode="list">
        <tr>
            <td>
                <xsl:value-of select="name()"/>
            </td>
            <xsl:apply-templates select="@*" mode="list">
                <xsl:sort select="name()"/>
            </xsl:apply-templates>
        </tr>
    </xsl:template>
    <xsl:template match="@*" mode="headers">
        <th>
            <xsl:value-of select="name()"/>
        </th>
    </xsl:template>
    <xsl:template match="@*" mode="list">
        <td>
            <xsl:value-of select="."/>
        </td>
    </xsl:template>
</xsl:stylesheet>

Result:

<html>
    <table>
        <tr>
            <th>ElementName</th>
            <th>color</th>
            <th>cute</th>
            <th>name</th>
            <th>type</th>
        </tr>
        <tr>
            <td>dog</td>
            <td>brown</td>
            <td>yes</td>
            <td>Frank</td>
            <td>Lab</td>
        </tr>
    </table>
    <table>
        <tr>
            <th>ElementName</th>
            <th>color</th>
            <th>cute</th>
            <th>name</th>
        </tr>
        <tr>
            <td>cat</td>
            <td>brown</td>
            <td>yes</td>
            <td>Fluffy</td>
        </tr>
        <tr>
            <td>cat</td>
            <td>brown</td>
            <td>no</td>
            <td>Lucy</td>
        </tr>
    </table>
    <table>
        <tr>
            <th>ElementName</th>
            <th>color</th>
            <th>cute</th>
            <th>name</th>
        </tr>
        <tr>
            <td>dog</td>
            <td>brown</td>
            <td>no</td>
            <td>Spot</td>
        </tr>
        <tr>
            <td>dog</td>
            <td>brown</td>
            <td>yes</td>
            <td>Rover</td>
        </tr>
    </table>
    <table>
        <tr>
            <th>ElementName</th>
            <th>color</th>
            <th>name</th>
        </tr>
        <tr>
            <td>cat</td>
            <td>brown</td>
            <td>Princess</td>
        </tr>
    </table>
</html>

Note: This assumes that all elements having the same number of attributes have also the same attribute's name (like in your input sample).

EDIT: Better ouput markup.

EDIT 2: Another kind of solution: one header with all posible attribute (like CSV pattern) and order element by attribute count and name.

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
    <xsl:key name="attrByName" match="pets/*/@*" use="name()"/>
    <xsl:variable name="attr" select="/pets/*/@*[count(.|key('attrByName',name())[1])=1]"/>
    <xsl:template match="pets">
        <html>
            <table>
                <tr>
                    <th>ElementName</th>
                    <xsl:apply-templates select="$attr" mode="headers">
                        <xsl:sort select="name()"/>
                    </xsl:apply-templates>
                </tr>
                <xsl:apply-templates select="*[@color='brown']">
                    <xsl:sort select="count(@*)" order="descending"/>
                    <xsl:sort select="name()"/>
                </xsl:apply-templates>
            </table>
        </html>
    </xsl:template>
    <xsl:template match="pets/*">
        <tr>
            <td>
                <xsl:value-of select="name()"/>
            </td>
            <xsl:apply-templates select="$attr" mode="list">
                <xsl:sort select="name()"/>
                <xsl:with-param name="node" select="."/>
            </xsl:apply-templates>
        </tr>
    </xsl:template>
    <xsl:template match="@*" mode="headers">
        <th>
            <xsl:value-of select="name()"/>
        </th>
    </xsl:template>
    <xsl:template match="@*" mode="list">
        <xsl:param name="node"/>
        <td>
            <xsl:value-of select="$node/@*[name()=name(current())]"/>
        </td>
    </xsl:template>
</xsl:stylesheet>

Result:

<html>
    <table>
        <tr>
            <th>ElementName</th>
            <th>color</th>
            <th>cute</th>
            <th>name</th>
            <th>type</th>
        </tr>
        <tr>
            <td>dog</td>
            <td>brown</td>
            <td>yes</td>
            <td>Frank</td>
            <td>Lab</td>
        </tr>
        <tr>
            <td>cat</td>
            <td>brown</td>
            <td>yes</td>
            <td>Fluffy</td>
            <td></td>
        </tr>
        <tr>
            <td>cat</td>
            <td>brown</td>
            <td>no</td>
            <td>Lucy</td>
            <td></td>
        </tr>
        <tr>
            <td>dog</td>
            <td>brown</td>
            <td>no</td>
            <td>Spot</td>
            <td></td>
        </tr>
        <tr>
            <td>dog</td>
            <td>brown</td>
            <td>yes</td>
            <td>Rover</td>
            <td></td>
        </tr>
        <tr>
            <td>cat</td>
            <td>brown</td>
            <td></td>
            <td>Princess</td>
            <td></td>
        </tr>
    </table>
</html>

Note: This runs through the tree twice but without extension. Exact match for desired output without extensions would require to mimic key mechanism like this: run through the tree adding new keys (name of element plus attributes' names) to a param, then again for every key run through the tree filtering node by key (could be a little optimization keeping a node set for non matching elements...). Worst case (every node with distinc key) will pass trough a node: N (for key building) + (N + 1) * N / 2

查看更多
狗以群分
3楼-- · 2020-04-27 19:44

This transformation doesn't make any assumptions about the sets having the same number of attributes -- no assumptions at all.

<xsl:stylesheet version="1.0"
 xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
 xmlns:ext="http://exslt.org/common"
 exclude-result-prefixes="ext">
 <xsl:output omit-xml-declaration="yes" indent="yes"/>
 <xsl:key name="kAnimalByProperties" match="animal"
  use="concat(@atype, .)"/>

 <xsl:variable name="vrtfNewDoc">
  <xsl:apply-templates select="/pets/*">
    <xsl:sort select="name()"/>
  </xsl:apply-templates>
 </xsl:variable>

 <xsl:template match="pets/*">
   <animal atype="{name()}">
     <xsl:copy-of select="@*"/>
     <xsl:for-each select="@*">
       <xsl:sort select="name()"/>
         <attrib>|<xsl:value-of select="name()"/>|</attrib>
     </xsl:for-each>
   </animal>
 </xsl:template>

 <xsl:template match="/">
   <xsl:for-each select="ext:node-set($vrtfNewDoc)">
     <xsl:for-each select=
     "*[generate-id()
       =generate-id(key('kAnimalByProperties',
                        concat(@atype, .)
                        )[1]
                    )
       ]">
        <table border="1">
          <tr>
            <td>Element Name</td>
            <xsl:for-each select="*">
              <td><xsl:value-of select="translate(.,'|','')"/></td>
            </xsl:for-each>
          </tr>
          <xsl:for-each select=
          "key('kAnimalByProperties', concat(@atype, .))">
            <xsl:variable name="vcurAnimal" select="."/>
            <tr>
              <td><xsl:value-of select="@atype"/></td>
              <xsl:for-each select="*">
                <td>
                  <xsl:value-of select=
                   "$vcurAnimal/@*[name()=translate(current(),'|','')]"/>
                </td>
              </xsl:for-each>
            </tr>
          </xsl:for-each>
        </table>
        <p/>
     </xsl:for-each>
   </xsl:for-each>
 </xsl:template>
</xsl:stylesheet>

When applied on the provided XML document:

<pets>
    <dog name="Frank" cute="yes" color="brown" type="Lab"/>
    <cat name="Fluffy" cute="yes" color="brown"/>
    <cat name="Lucy" cute="no" color="brown"/>
    <dog name="Spot" cute="no" color="brown"/>
    <dog name="Rover" cute="yes" color="brown"/>
    <dog name="Rupert" cute="yes" color="beige" type="Pug"/>
    <cat name="Simba" cute="yes" color="grey"/>
    <cat name="Princess" color="brown"/>
</pets>

the wanted, correct result is produced:

<table border="1">
   <tr>
      <td>Element Name</td>
      <td>color</td>
      <td>cute</td>
      <td>name</td>
   </tr>
   <tr>
      <td>cat</td>
      <td>brown</td>
      <td>yes</td>
      <td>Fluffy</td>
   </tr>
   <tr>
      <td>cat</td>
      <td>brown</td>
      <td>no</td>
      <td>Lucy</td>
   </tr>
   <tr>
      <td>cat</td>
      <td>grey</td>
      <td>yes</td>
      <td>Simba</td>
   </tr>
</table>
<p/>
<table border="1">
   <tr>
      <td>Element Name</td>
      <td>color</td>
      <td>name</td>
   </tr>
   <tr>
      <td>cat</td>
      <td>brown</td>
      <td>Princess</td>
   </tr>
</table>
<p/>
<table border="1">
   <tr>
      <td>Element Name</td>
      <td>color</td>
      <td>cute</td>
      <td>name</td>
      <td>type</td>
   </tr>
   <tr>
      <td>dog</td>
      <td>brown</td>
      <td>yes</td>
      <td>Frank</td>
      <td>Lab</td>
   </tr>
   <tr>
      <td>dog</td>
      <td>beige</td>
      <td>yes</td>
      <td>Rupert</td>
      <td>Pug</td>
   </tr>
</table>
<p/>
<table border="1">
   <tr>
      <td>Element Name</td>
      <td>color</td>
      <td>cute</td>
      <td>name</td>
   </tr>
   <tr>
      <td>dog</td>
      <td>brown</td>
      <td>no</td>
      <td>Spot</td>
   </tr>
   <tr>
      <td>dog</td>
      <td>brown</td>
      <td>yes</td>
      <td>Rover</td>
   </tr>
</table>
<p/>
查看更多
登录 后发表回答