Groups two xml files like a sql group-by - xslt

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>

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)
</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)
</xsl:text>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>

Related

Output only Duplicates in the final Text Output

I am trying to output only one line per unique value in my final text output after running an XML through an XSL stylesheet. In my research, I came upon the distinct-values function, but I'm unable to execute it the way that I want.
Here is my XML:
<Library>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>1</Code>
<Title>MANAGEMENT</Title>
</Book>
<Book>
<Code>10</Code>
<Title>MECHANICAL</Title>
</Book>
<Book>
<Code>106</Code>
<Title>TRANSPORTATION</Title>
</Book>
</Library>
And here is my current XSL (incorrect):
<xsl:template match="Book">
<xsl:value-of select="this:fixedOutput(Code)" />
<xsl:value-of select="this:fixedOutput(Title)" />
<xsl:value-of select="$linefeed" />
</xsl:template>
My output right now is:
1|MANAGEMENT| 1|MANAGEMENT| 1|MANAGEMENT| 1|MANAGEMENT| 1|MANAGEMENT|
10|MECHANICAL| 106|TRANSPORTATION|
But I want it to be this:
1|MANAGEMENT| 10|MECHANICAL| 106|TRANSPORTATION|
I'm not sure how to use the syntax of distinct values to get to where I need.
An XSLT 1.0 solution that uses key and the generate-id() function (Muenchian grouping) to get distinct values :
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="bookCode" match="/Library/Book/Code" use="." />
<xsl:template match="/">
<xsl:for-each select="/Library/Book/Code[generate-id()
= generate-id(key('bookCode',.)[1])]">
<xsl:value-of select="this:fixedOutput(.)" />
<xsl:value-of select="this:fixedOutput(../Title)" />
<xsl:value-of select="$linefeed" />
</xsl:for-each>
</xsl:template>
An XSLT 2.0 solution which uses xsl:for-each-group as #michael.hor257k said :
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*" />
</xsl:copy>
</xsl:template>
<xsl:template match="Library">
<xsl:for-each-group select="Book" group-by="concat(Code,Title)">
<xsl:apply-templates select="." />
</xsl:for-each-group>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="this:fixedOutput(Code)" />
<xsl:value-of select="this:fixedOutput(Title)" />
<xsl:value-of select="$linefeed" />
</xsl:template>
Note: As this:fixedOutput in your code doen't refer any namespace it has been used as it is.
Refer this: http://xsltransform.net/3MP2uBE

How to use contains in xslt?

I'm trying use contains in xslt for return any values.
for example:
I've this xml:
<BOOKS>
<BOOK>
<TITLE>title1</TITLE>
<ISSN>12313213</ISSN>
</BOOK>
<BOOK>
<TITLE>title2</TITLE>
<ISSN>67895776</ISSN>
</BOOK>
<BOOK>
<TITLE>title3</TITLE>
<ISSN>54363645</ISSN>
</BOOK>
</BOOKS>
and this static xml(issn.xml):
<ISSNS>
<ISSN>
<VALUE>12313213, 67895776</VALUE>
<PRICE>1235,23</PRICE>
</ISSN>
<ISSN>
<VALUE>5463432, 54363645</VALUE>
<PRICE>345,23</PRICE>
</ISSN>
</ISSNS>
I need to verify if have book issn in issn xml.
My code:
<xsl:template match="/">
<xsl:variable name="issn" select="document('file:///E:/issn.xml')"/>
<BOOKS>
<xsl:for-each select="$issn/ISSNS/ISSN">
<xsl:variable name="value" select="VALUE"/>
<xsl:variable name="price" select="ESTRATO"/>
<xsl:for-each select="//BOOKS">
<xsl:for-each select="BOOK">
<xsl:if test="contains($value, #ISSN)">
<BOOK>
<TITLE><xsl:value-of select="#TITLE"/></TITLE>
<ISSN><xsl:value-of select="$value"/></ISSN>
<PRICE><xsl:value-of select="$price"/></PRICE>
</BOOK>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</BOOKS>
</xsl:template>
I wanna return when the book issn contains in issn value. Can anyone Help me? Thanks
If I understand this correctly, you want to do:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="issns" select="document('file:///E:/issn.xml')"/>
<xsl:template match="/BOOKS">
<BOOKS>
<xsl:for-each select="BOOK">
<xsl:variable name="issn" select="$issns/ISSNS/ISSN[contains(VALUE, current()/ISSN)] "/>
<xsl:if test="$issn">
<BOOK>
<xsl:copy-of select="*"/>
<xsl:copy-of select="$issn/PRICE"/>
</BOOK>
</xsl:if>
</xsl:for-each>
</BOOKS>
</xsl:template>
</xsl:stylesheet>
Note that this assumes one ISSN value cannot contain another (which could possibly generate a false match).

search keyword and replace the text in xml file based on external xml file

I have a xml file main.xml with following markup and data.
main.xml
<xml>
<content>
<para>
This is a para.
</para>
<sub para>
This is para.
</sub para>
</content>
</xml>
I have another xml file keyword.xml with list of keywords that we need to find any where in above xml and replace the keyword value.
keyword.xml
<xml>
<keywordList>
<keyword>
<value>para</value>
<replace> paragraph </replace>
</keyword>
<keyword>
<value>is</value>
<replace>IS</replace>
</keyword>
</xml>
Can we do it in xslt so that the output should be
output
<xml>
<content>
<para>
This IS a paragraph.
</para>
<sub para>
This IS paragraph.
</sub para>
</content>
</xml>
Try the following
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:variable name="keywords" select="document('keyword.xml')"/>
<xsl:template match="/">
<xsl:copy>
<xsl:apply-templates/>
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()">
<xsl:analyze-string select="." regex="[A-Za-z]+">
<xsl:matching-substring>
<xsl:variable name="repl" select="$keywords//keyword[value = current()]"/>
<xsl:choose>
<xsl:when test="$repl">
<xsl:value-of select="$repl/replace"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="current()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:matching-substring>
<xsl:non-matching-substring>
<xsl:value-of select="current()"/>
</xsl:non-matching-substring>
</xsl:analyze-string>
</xsl:template>
</xsl:stylesheet>
Note that the replace value for para includes spaces around the new word, hence the additional spaces:
<?xml version="1.0" encoding="UTF-8"?>
<xml>
<content>
<para>
This IS a paragraph .
</para>
<subpara>
This IS paragraph .
</subpara>
</content>
</xml>
This is an XSLT 1.0 solution (of course, can be used with XSLT 2.0, too):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<my:params xml:space="preserve">
<pattern>
<old>para</old>
<new> paragraph </new>
</pattern>
<pattern>
<old> is </old>
<new> IS </new>
</pattern>
</my:params>
<xsl:variable name="vrtfPats">
<xsl:for-each select="document('')/*/my:params/*">
<xsl:sort select="string-length(old)"
data-type="number" order="descending"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="vPats" select=
"ext:node-set($vrtfPats)/*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="text()" name="multiReplace" priority="2">
<xsl:param name="pText" select="."/>
<xsl:param name="pPatterns" select="$vPats"/>
<xsl:if test= "string-length($pText) >0">
<xsl:variable name="vPat" select=
"$vPats[starts-with($pText, old)][1]"/>
<xsl:choose>
<xsl:when test="not($vPat)">
<xsl:copy-of select="substring($pText,1,1)"/>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$vPat/new/node()"/>
</xsl:otherwise>
</xsl:choose>
<xsl:call-template name="multiReplace">
<xsl:with-param name="pText" select=
"substring($pText,
1 + not($vPat) + string-length($vPat/old/node())
)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the provided XML document (corrected to be well-formed):
<xml>
<content>
<para>
This is a para.
</para>
<sub_para>
This is para.
</sub_para>
</content>
</xml>
the wanted, correct result is produced:
<xml>
<content>
<para>
This IS a paragraph .
</para>
<sub_para>
This IS paragraph .
</sub_para>
</content>
</xml>
Explanation: The text is scanned character by character and the longest possible target string starting at that position in the text is replaced with its specified replacement.

Groups two xml files like a sql group-by [2]

This is an evolution of my ask here :
Groups two xml files like a sql group-by
The Example given and Dimitre Solution was counting distinct isbn value.
Now modify library xml to have
mylibrary.xml :
<library>
<book id="1" isbn="1"/>
<book id="2" isbn="1"/>
<book id="3" isbn="2"/>
<book id="4" isbn="4"/>
<book id="5" isbn="5"/>
<book id="6" isbn="4"/>
<book id="7" isbn="4"/>
</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 'even if some have same isbn', groupby category, using xslt 1-0.
output wanted:
SF : 3 book(s)
Comedy : 4 book(s)
my xslt propose here : Groups two xml files like a sql group-by works fine but of course use 'for-each' loop and extension functions.
Surely there is a better solution.
Again a very good question! (+1)
This transformation, using two keys for achieving full efficiency:
<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:key name="kBookByIsbn" match="book"
use="#isbn"/>
<xsl:variable name="vDoc" select="/"/>
<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:for-each select="$vDoc">
<xsl:value-of select="count(key('kBookByIsbn',$vBooksinCat/#isbn))"/>
</xsl:for-each>
<xsl:text> book(s)
</xsl:text>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
when applied on the provided XML document contained in the file mylibrary.xml:
<library>
<book id="1" isbn="1"/>
<book id="2" isbn="1"/>
<book id="3" isbn="2"/>
<book id="4" isbn="4"/>
<book id="5" isbn="5"/>
<book id="6" isbn="4"/>
<book id="7" isbn="4"/>
</library>
and having this provided XML document in 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>
produces the wanted, correct output:
SF : 3 book(s)
Comedy : 4 book(s)
modified Dimitri version to work for this
<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="meh" select="*"/>
<xsl:template match="/">
<xsl:apply-templates select="$vRef/reference/book[generate-id()=generate-id(key('kBookByCat', category)[1])]" />
</xsl:template>
<xsl:template match="book">
<xsl:variable name="cat" select="category"/>
<xsl:value-of select="category"/> : <xsl:text/>
<xsl:variable name="isbns" select="$vRef/reference/book[category=$cat]/#isbn"/>
<xsl:value-of select="count($meh/book[#isbn=$isbns])"/>
<xsl:text> book(s)
</xsl:text>
</xsl:template>
in add to the great dimitri response, i propose the following to not print book category that have 0 book set in mylibrary :
<xsl:variable name="catname" select="category"/>
<xsl:for-each select="$vDoc">
<xsl:variable name="cnt" select="count(key('kBookByIsbn',$vBooksinCat/#isbn))"/>
<xsl:if test="$cnt > 0">
<xsl:value-of select="$catname"/> :
<xsl:text/>
<xsl:value-of select="$cnt"/>
<xsl:text> book(s)
</xsl:text>
</xsl:if>
</xsl:for-each>

Grouping when using 2 different XML files as sources?

what i want to do is a typical grouping that can usualy be done using xsl:key, but it become more complicated as data to groups are in 2 differents files. How process ?
Here is an example of what i want to do, can i request your help ? must be xslt-1.0 compliant.
bookreference.xml :
<t>
<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>
</t>
mylibrary.xml :
<t>
<book isbn="1">
<price>10</price>
</book>
<book isbn="2">
<price>10</price>
</book>
<book isbn="3">
<price>20</price>
</book>
<book isbn="4">
<price>5</price>
</book>
</t>
output wanted:
SF : 3 book(s) - Total : 40$
Comedy : 2 book(s) - Total : 5$
As already suggested in my comment, you can first merge two documents into a result tree fragment, then use exsl:node-set to get a node-set on which you can then apply Muenchian grouping:
<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:param name="price-url" select="'test2011113002.xml'"/>
<xsl:variable name="doc2" select="document($price-url)"/>
<xsl:output method="text"/>
<xsl:variable name="rtf">
<xsl:apply-templates select="//book" mode="merge"/>
</xsl:variable>
<xsl:template match="book" mode="merge">
<xsl:copy>
<xsl:variable name="isbn" select="#isbn"/>
<xsl:copy-of select="#* | node()"/>
<xsl:for-each select="$doc2">
<xsl:copy-of select="key('k1', $isbn)/price"/>
</xsl:for-each>
</xsl:copy>
</xsl:template>
<xsl:key name="k1" match="book" use="#isbn"/>
<xsl:key name="k2" match="book" use="category"/>
<xsl:template match="/">
<xsl:apply-templates select="exsl:node-set($rtf)/book[generate-id() = generate-id(key('k2', category)[1])]"/>
</xsl:template>
<xsl:template match="book">
<xsl:variable name="current-group" select="key('k2', category)"/>
<xsl:value-of select="concat($current-group/category, ': ', count($current-group), ' - Total : ', sum($current-group/price), '
')"/>
</xsl:template>
</xsl:stylesheet>
Good question, +1.
There is no need for complicated (more than necessary) grouping or for any extension functions:
<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:key name="kPriceByIsbn" match="price"
use="../#isbn"/>
<xsl:variable name="vMyLib" select=
"document('file:///c:/temp/delete/mylibrary.xml')"/>
<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) - Total : $</xsl:text>
<xsl:for-each select="$vMyLib">
<xsl:value-of select="sum(key('kPriceByIsbn', $vBooksinCat/#isbn))"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
When this transformation is applied on the first of the two provided data fragments (corrected to well-formed XML document):
<t>
<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>
</t>
and having the second data fragment (corrected to well-formed XML document) saved as C:\Temp\Delete\mylibrary.xml,
The wanted, correct result is produced:
SF : 3 book(s) - Total : $40
Comedy : 2 book(s) - Total : $5