Search code examples
xsltxslt-1.0xslt-grouping

Groups two xml files like a sql group-by


With the following file as input of xsltprocessor :

mylibrary.xml :

<library>  
   <book isbn="1"/>    
   <book isbn="3"/>
   <book isbn="5"/>
</library>  

and this one that can be used : bookreference.xml :

<reference>  
    <book isbn="1">  
        <category>SF</category>  
    </book>  
    <book isbn="2">  
        <category>SF</category>  
    </book>  
    <book isbn="3">  
        <category>SF</category>  
    </book>  
    <book isbn="4">  
        <category>Comedy</category>  
    </book>  
    <book isbn="5">  
        <category>Comedy</category>  
    </book>
</reference>  

i want to get the numbers of book i got in mylibrary, groupby category, using xslt 1-0.

output wanted:

SF : 2 book(s) 
Comedy : 1 book(s) 

here is the xsl i write using Martin Honnen method explains in 'Grouping when using 2 different XML files as sources?' , i think that solve the problem but i do not validate yet and perhaps someone have a better solution.

<xsl:stylesheet
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:exsl="http://exslt.org/common"
  exclude-result-prefixes="exsl"
  version="1.0">

<xsl:output method="xml" indent="yes"/>
<xsl:key name="book-by-category" match="book" use="category"/>

<xsl:param name="bookref" select="'bookreference.xml'"/>
<xsl:variable name="doc" select="document($bookref)/reference"/>

<xsl:variable name="rtf">
    <xsl:apply-templates select="//library" mode="merge"/>
</xsl:variable>

<xsl:template match="library" mode="merge">
    <xsl:element name="mybookcat">
    <xsl:for-each select="book">
        <xsl:variable name="isbn" select="@isbn"/>
        <xsl:element name="book">
        <xsl:attribute name="isbn"><xsl:value-of select="$isbn"/></xsl:attribute>
        <xsl:for-each select="$doc/book[@isbn=$isbn]">
            <xsl:element name="category">
                <xsl:value-of select="./category"/>
            </xsl:element>
        </xsl:for-each>
        </xsl:element>
    </xsl:for-each>
    </xsl:element>
</xsl:template>     

<xsl:template match="/">
    <xsl:apply-templates select="exsl:node-set($rtf)/mybookcat"/>
</xsl:template>

<xsl:template match="mybookcat">
<xsl:for-each select="book[count(. | key('book-by-category',category)[1]) = 1]"> 
    <xsl:sort select="category"/> 
    <xsl:value-of select="category" /><xsl:text> : </xsl:text>
    <xsl:variable name="current-cat" select="key('book-by-category', category)"/>
    <xsl:value-of select="count($current-cat)"/><xsl:text> book(s)
</xsl:text>
    <xsl:for-each select="key('book-by-category', category)">
        <xsl:sort select="@isbn" data-type="number" />
        <xsl:value-of select="@isbn" /><xsl:text>
</xsl:text>            
    </xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Solution

  • here is the xsl i write using Martin Honnen method explains in 'Grouping when using 2 different XML files as sources?' , i think that solve the problem but i do not validate yet and perhaps someone have a better solution

    .

    Here is a simpler XSLT 1.0 solution that doesn't use any extension functions or xsl:for-each:

    <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:key name="kBookByCat" match="book"
              use="category"/>
    
         <xsl:variable name="vRef" select=
         "document('file:///c:/temp/delete/reference.xml')"/>
    
         <xsl:variable name="vMyIsbns" select="/*/*/@isbn"/>
    
         <xsl:variable name="vResult">
          <xsl:apply-templates select="$vRef/*"/>
         </xsl:variable>
    
         <xsl:template match="/">
          <xsl:copy-of select="$vResult"/>
         </xsl:template>
    
         <xsl:template match=
          "book[generate-id()
               =
                generate-id(key('kBookByCat', category)[1])
                ]
          ">
             <xsl:variable name="vBooksinCat" select=
                  "key('kBookByCat', category)"/>
    
             <xsl:value-of select="category"/> : <xsl:text/>
             <xsl:value-of select="count($vBooksinCat[@isbn=$vMyIsbns])"/>
             <xsl:text> book(s)&#xA;</xsl:text>
         </xsl:template>
         <xsl:template match="text()"/>
    </xsl:stylesheet>
    

    When applied on the provided XML document contained in MyLibrary.xml:

    <library>
        <book isbn="1"/>
        <book isbn="3"/>
        <book isbn="5"/>
    </library>
    

    and having this provided XML document contained in the file C:\temp\delete\reference.xml:

    <reference>
        <book isbn="1">
            <category>SF</category>
        </book>
        <book isbn="2">
            <category>SF</category>
        </book>
        <book isbn="3">
            <category>SF</category>
        </book>
        <book isbn="4">
            <category>Comedy</category>
        </book>
        <book isbn="5">
            <category>Comedy</category>
        </book>
    </reference>
    

    the wanted, correct result is produced:

    SF : 2 book(s)
    Comedy : 1 book(s)
    

    II. An XSLT 2.0 solution:

    This is slightly simpler, as we can define the key to be dependent on the second document.

    <xsl:stylesheet version="2.0"
         xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
         <xsl:output omit-xml-declaration="yes" indent="yes"/>
         <xsl:strip-space elements="*"/>
    
         <xsl:key name="kBookByCat" match="book[@isbn = $vMyIsbns]"
              use="category"/>
    
         <xsl:variable name="vRef" select=
         "document('file:///c:/temp/delete/reference.xml')"/>
    
         <xsl:variable name="vMyIsbns" select="/*/*/@isbn"/>
    
         <xsl:variable name="vResult">
          <xsl:apply-templates select="$vRef/*"/>
         </xsl:variable>
    
         <xsl:template match="/">
          <xsl:copy-of select="$vResult"/>
         </xsl:template>
    
         <xsl:template match=
          "book[generate-id()
               =
                generate-id(key('kBookByCat', category)[1])
                ]
          ">
             <xsl:variable name="vBooksinCat" select=
                  "key('kBookByCat', category)"/>
    
             <xsl:value-of select="category"/> : <xsl:text/>
             <xsl:value-of select="count($vBooksinCat)"/>
             <xsl:text> book(s)&#xA;</xsl:text>
         </xsl:template>
         <xsl:template match="text()"/>
    </xsl:stylesheet>