LadyCygnus
LadyCygnus

Reputation: 693

XSLT sort across nodes and text

I'm trying to find, sort, and output a string of copyright years. I've got a working bit of code, but I just found that some of my years are not in the same tags as others.

Initially I thought all my years were in the following tag: <copyright-year>2020</copyright-year>, see below for a working bit of code to find, sort, and output those.

I just found that some of my copyright years look like this: <copyright-statement>&#xa9; 2017 Company. All rights reserved.</copyright-statement>.

I can find the years in these statements using //copyright-statement/substring(.,3,4). However, when I tried to search for both types like this: <xsl:for-each-group select="//copyright-year|copyright-statement/substring(., 3, 4)" group-by="text()">, it gives the following warning:

Required item type of document-order sorter is node(); supplied expression ((./copyright-statement)/(fn:substring(...))) has item type xs:string. The expression can succeed only if the supplied value is an empty sequence.

And obviously doesn't work. Any idea how to merge these two sets of years to get: <output>2020, 2019, 2017</output>?


Sample XML

<?xml version="1.0" encoding="UTF-8"?>
<book>
<book-meta>
    <copyright-year>2020</copyright-year>
</book-meta>
<body>
    <book-part>
        <book-part-meta>
            <copyright-year>2019</copyright-year>
        </book-part-meta>
    </book-part>
</body>
<back>
    <copyright-statement>&#xa9; 2017 Company. All rights reserved.</copyright-statement>
</back>
</book>

Sample XSLT

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">

<xsl:template match="book">
    <xsl:variable name="years">
        <xsl:for-each-group select="//copyright-year" group-by="text()">
            <xsl:sort select="." order="descending"/>
            <xsl:value-of select="."/><xsl:if test="position() != last()"><xsl:text>, </xsl:text></xsl:if>
        </xsl:for-each-group>
    </xsl:variable>

    <output><xsl:value-of select="$years"/></output>

</xsl:template>
</xsl:stylesheet>

Upvotes: 2

Views: 50

Answers (2)

michael.hor257k
michael.hor257k

Reputation: 117140

Here's one way to get the specify output;

XSLT 2.0

<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>

<xsl:template match="/book">
    <xsl:variable name="years" as="xs:string*">
        <xsl:perform-sort>
            <xsl:sort select="." data-type="number" order="descending"/>
            <xsl:apply-templates select="//(copyright-year | copyright-statement)"/>
        </xsl:perform-sort>     
     </xsl:variable>
    <output>
        <xsl:value-of select="$years" separator=","/>
    </output>
</xsl:template>

<xsl:template match="copyright-statement">
    <xsl:value-of select="substring-before(substring-after(., '&#xa9; '), ' ')"/>       
</xsl:template>

</xsl:stylesheet>

Demo: https://xsltfiddle.liberty-development.net/bwdwsd/3

Upvotes: 2

Martin Honnen
Martin Honnen

Reputation: 167716

Which version of which XSLT processor do you use? XSLT 3 has a sort function

  <xsl:value-of select="reverse(sort(distinct-values((//copyright-year/xs:integer(.), //copyright-statement/xs:integer(substring(.,3,4))))))" separator=", "/>

https://xsltfiddle.liberty-development.net/bwdwsd

It might be easier to read that with the new => arrow operator:

  <xsl:value-of 
    select="(//copyright-year/xs:integer(.), //copyright-statement/xs:integer(substring(.,3,4)))
            => distinct-values()
            => sort()
            => reverse()" 
    separator=", "/>

https://xsltfiddle.liberty-development.net/bwdwsd/2

But in general the step you need is to simply ensure you work with atomic values e.g. xs:integers seems the right value for years. I think in XSLT 2 I would wrap perform-sort into a function:

  <xsl:function name="mf:sort" as="item()*">
      <xsl:param name="input" as="item()*"/>
      <xsl:perform-sort select="$input">
          <xsl:sort order="descending"/>
      </xsl:perform-sort>
  </xsl:function>

  <xsl:template match="book">
      <xsl:value-of select="mf:sort(distinct-values((//copyright-year/xs:integer(.), //copyright-statement/xs:integer(substring(.,3,4)))))" separator=", "/>
  </xsl:template>

https://xsltfiddle.liberty-development.net/bwdwsd/1

Upvotes: 3

Related Questions