Listing elements returned by group by xslt - xslt-grouping

Hi I have the following xml
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item>
<name>john</name>
<year>2010</year>
</item>
<item>
<name>sam</name>
<year>2000</year>
</item>
<item>
<name>jack</name>
<year>2007</year>
</item>
<item>
<name>smith</name>
<year>2010</year>
</item>
</root>
I use the following xslt to group by year
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://www.w3.org/1999/xhtml">
<xsl:template match="/">
<xsl:for-each-group select="r//*[(name=*)]" group-by="year">
<xsl:sort select="year" order="descending"/>
<xsl:variable name="total" select="count(/r//*[(name=*)]) + 1" />
<xsl:value-of select="year"/><br />
<xsl:for-each select="current-group()/name">
<xsl:variable name="i" select="position()"/>
<xsl:call-template name="row">
<xsl:with-param name="name" select="."/>
<xsl:with-param name="number" select="$total - $i"/>
</xsl:call-template>
</xsl:for-each>
<br />
</xsl:for-each-group>
</xsl:template>
<xsl:template name="row">
<xsl:param name="name"/>
<xsl:param name="number"/>
<xsl:value-of select="concat($number, '. ')"/>
<xsl:value-of select="concat($name, ' ')"/><br />
</xsl:template>
</xsl:stylesheet>
This is outputing, it's quite close to the output i want.
2010
4. john
3. smith
2007
4. jack
2000
4. sam
What i want is simply numbering all names (descending order from total number of names to 1) e.g
2010
4. john
3. smith
2007
2. jack
2000
1. sam
It would be simple if we can reassign a varible to a new value, but i thinks it's not possible, so i have to find another solution. Can anyone help me to find out how to resolve this problem.
thanks

Here is an XSLT-1.0 solution:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output type="text" omit-xml-declaration="yes"/>
<xsl:key name="byYear" match="item" use="year"/>
<xsl:template match="/">
<!-- process first item for each distinct year number (ordered) -->
<xsl:apply-templates select="//item[count(.|key('byYear',year)[1])=1]">
<xsl:sort select="year" order="descending"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="item">
<!-- output year number, surrounded by newlines -->
<xsl:text>
</xsl:text>
<xsl:value-of select="year"/>
<xsl:text>
</xsl:text>
<!-- now process all items for the current year number -->
<xsl:for-each select="key('byYear',year)">
<!-- output reversed index of current item for current year number
plus total items for lower year numbers -->
<xsl:number value="count(//item[year < current()/year])+last()-position()+1"
format="1. "/>
<!-- and finally also the name of the current item and again a newline -->
<xsl:value-of select="name"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Related

Combining XML Nodes into a single node with an XSLT

I'm trying to edit some XML with a transform but I'm struggling to achieve my desired results.
I have some XML:
<FX>
<Order ATTRIBUTE1="ACTIVE" ATTRIBUTE2="CCY" />
<Attribute NAME="N1" VALUE="V1" />
<Attribute NAME="N2" VALUE="V2" />
<Attribute NAME="N3" VALUE="V3" />
</FX>
And I want to transform it to look like:
<FX>
<Order ATTRIBUTE1="ACTIVE" ATTRIBUTE2="CCY" />
<Attribute NAME="N1, N2, N3" VALUE="V1,V2,V3" />
</FX>
Is this possible? Can anyone offer any suggestions on how to do this with a transform?
You can use the following, Asp.NET compatable, XSLT-1.0 stylesheet to perform an XSLT transformation from your source XML to your destination XML:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/FX">
<xsl:copy>
<xsl:copy-of select="Order" />
<Attribute>
<xsl:attribute name="NAME">
<xsl:for-each select="Attribute">
<xsl:value-of select="#NAME" />
<xsl:if test="position() != last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:attribute>
<xsl:attribute name="VALUE">
<xsl:for-each select="Attribute">
<xsl:value-of select="#VALUE" />
<xsl:if test="position() != last()">
<xsl:text>,</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:attribute>
</Attribute>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Its output is:
<FX>
<Order ATTRIBUTE1="ACTIVE" ATTRIBUTE2="CCY"/>
<Attribute NAME="N1, N2, N3" VALUE="V1,V2,V3"/>
</FX>
In general, if you want to transform some nodes but keep the rest you use the identity transformation template as the starting point and then add templates that change those nodes you want to change:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="FX/Attribute[1]">
<xsl:copy>
<xsl:apply-templates select="#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="FX/Attribute[position() > 1]"/>
<xsl:template match="FX/Attribute[1]/#*">
<xsl:attribute name="{name()}">
<xsl:for-each select=". | ../following-sibling::Attribute/#*[name() = name(current())]">
<xsl:if test="position() > 1">,</xsl:if>
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/jyH9rNk

XML Rename node name and convert datetime to date

I have an xml file I want to both rename the element name and return the date part of the date only to produce
<!-- reference the stylesheet -->
<?xml-stylesheet type="text/xsl" href="Dates.xsl"?>
<user>
<dob>1992-02-22T00:00:00.0000000</dob>
</user>
I want to both rename the element name and return the date part of the date only to produce
<!-- reference the stylesheet -->
<?xml-stylesheet type="text/xsl" href="Dates.xsl"?>
<user>
<USER_DOB>1992-02-22</USER_DOB>
</user>
In my XSL file
To change the element name this works
<xsl:template match="dob">
<USER_DOB><xsl:apply-templates select="node()"/></USER_DOB>
</xsl:template>
To change the date this works
<xsl:template match="dob">
<xsl:copy>
<xsl:call-template name="FormatDate">
<xsl:with-param name="DateTime" select="."/>
</xsl:call-template>
</xsl:copy>
</xsl:template>
<xsl:template name="FormatDate">
<xsl:param name="DateTime" />
<xsl:variable name="date">
<xsl:value-of select="substring-before($DateTime,'T')" />
</xsl:variable>
<xsl:if test="string-length($date) != 10">
<xsl:value-of select="$DateTime"/>
</xsl:if>
<xsl:if test="string-length($date) = 10">
<xsl:value-of select="$date"/>
</xsl:if>
</xsl:template>
I need to know how to combine both changes to produce the single output element with the renamed node and the formatted date
Thanks,
Brevan
Simply have one template matching dob that does this...
<xsl:template match="dob">
<USER_DOB>
<xsl:call-template name="FormatDate">
<xsl:with-param name="DateTime" select="."/>
</xsl:call-template>
</USER_DOB>
</xsl:template>

Issues with publishing an RSS feed

I'm having trouble publishing an RSS feed from my Umbraco site. I found this Umbraco.TV video and tried to follow the instructions there using an XSLT selector to select all nodes of a give type, like so:
umbraco.library.GetXmlAll()/node [#nodeTypeAlias='Alias]/node
As sugested here but that didn't work. Apparently the schema has changed or something. When this didn't work I looked for a plugin to do this kind of stuff and was amazed to find just 2 plugins, both of them with little-to-no documentation and neither seemed to work (first plugin, second plugin).
So once and for all, I'd like to have a definite answer - how does one publish an RSS feed in Umbraco?
Here's an XSLT that we use for News Items RSS (News Items are under a News Page). Let me know if that helps. I also have versions for Blogs.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:rssdatehelper="urn:rssdatehelper"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:msxml="urn:schemas-microsoft-com:xslt"
xmlns:umbraco.library="urn:umbraco.library" xmlns:Exslt.ExsltCommon="urn:Exslt.ExsltCommon" xmlns:Exslt.ExsltDatesAndTimes="urn:Exslt.ExsltDatesAndTimes" xmlns:Exslt.ExsltMath="urn:Exslt.ExsltMath" xmlns:Exslt.ExsltRegularExpressions="urn:Exslt.ExsltRegularExpressions" xmlns:Exslt.ExsltStrings="urn:Exslt.ExsltStrings" xmlns:Exslt.ExsltSets="urn:Exslt.ExsltSets"
exclude-result-prefixes="msxml umbraco.library Exslt.ExsltCommon Exslt.ExsltDatesAndTimes Exslt.ExsltMath Exslt.ExsltRegularExpressions Exslt.ExsltStrings Exslt.ExsltSets ">
<xsl:output method="xml" omit-xml-declaration="yes"/>
<xsl:param name="currentPage"/>
<!-- Update these variables to modify the feed -->
<xsl:variable name="RSSNoItems" select="/macro/RSSNoItems"/>
<xsl:variable name="RSSTitle" select="/macro/RSSTitle"/>
<xsl:variable name="SiteURL" select="concat('http://',umbraco.library:RequestServerVariables('HTTP_HOST'))"/>
<xsl:variable name="RSSDescription" select="/macro/RSSDescription"/>
<xsl:variable name="source" select="/macro/source"/>
<!-- This gets all news and events and orders by updateDate to use for the pubDate in RSS feed -->
<xsl:variable name="pubDate">
<xsl:for-each select="umbraco.library:GetXmlNodeById($source)/* [#isDoc and string(umbracoNaviHide) != '1']">
<xsl:sort select="./newsDate" order="descending" />
<xsl:if test="position() = 1">
<xsl:value-of select="./newsDate" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<!-- change the mimetype for the current page to xml -->
<xsl:value-of select="umbraco.library:ChangeContentType('text/xml')"/>
<xsl:text disable-output-escaping="yes"><?xml version="1.0" encoding="UTF-8"?></xsl:text>
<rss version="2.0"
xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:wfw="http://wellformedweb.org/CommentAPI/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
>
<channel>
<title>
<xsl:value-of select="$RSSTitle"/>
</title>
<link>
<xsl:value-of select="$SiteURL"/>
</link>
<pubDate>
<xsl:value-of select="$pubDate"/>
</pubDate>
<generator>umbraco v4</generator>
<description>
<xsl:value-of select="$RSSDescription"/>
</description>
<language>en</language>
<xsl:for-each select="umbraco.library:GetXmlNodeById($source)/* [#isDoc and string(umbracoNaviHide) != '1']">
<xsl:sort select="./newsDate" order="descending" />
<xsl:if test="position() <= $RSSNoItems">
<xsl:call-template name="RSSitem">
<xsl:with-param name="node" select="current()"/>
</xsl:call-template>
</xsl:if>
</xsl:for-each>
</channel>
</rss>
</xsl:template>
<xsl:template match="node">
<xsl:if test="position() <= $RSSNoItems">
<item>
<title>
<xsl:value-of select="#nodeName"/>
</title>
<link>
<xsl:value-of select="$SiteURL"/>
<xsl:value-of select="umbraco.library:NiceUrl(#id)"/>
</link>
<pubDate>
<xsl:value-of select="umbraco.library:FormatDateTime(./newsDate,'r')" />
</pubDate>
<guid>
<xsl:value-of select="$SiteURL"/>
<xsl:value-of select="umbraco.library:NiceUrl(#id)"/>
</guid>
<content:encoded>
<xsl:value-of select="concat('<![CDATA[ ', ./bodyText,']]>')" disable-output-escaping="yes"/>
</content:encoded>
</item>
</xsl:if>
</xsl:template>
<xsl:template name="RSSitem">
<xsl:param name="node"/>
<item>
<title>
<xsl:value-of select="$node/#nodeName"/>
</title>
<link>
<xsl:value-of select="$SiteURL"/><xsl:value-of select="umbraco.library:NiceUrl($node/#id)"/>
</link>
<pubDate>
<xsl:value-of select="umbraco.library:FormatDateTime(./newsDate,'r')"/>
</pubDate>
<dc:creator><xsl:value-of select="#writerName"/></dc:creator>
<xsl:for-each select="umbraco.library:Split($node/categories, ',')/value">
<xsl:sort data-type="text" order="ascending"/>
<category>
<xsl:value-of select="current()"/>
</category>
</xsl:for-each>
<guid>
<xsl:value-of select="$SiteURL"/><xsl:value-of select="umbraco.library:NiceUrl($node/#id)"/>
</guid>
<description>
<xsl:value-of select="concat('<![CDATA[ ', $node/summary,']]>')" disable-output-escaping="yes"/>
</description>
<content:encoded>
<xsl:value-of select="concat('<![CDATA[ ', $node/bodyText,']]>')" disable-output-escaping="yes"/>
</content:encoded>
</item>
</xsl:template>
</xsl:stylesheet>

xslt transform list and take max

I want to:
select all attributes "foo", which are string values, and store the values in a list.
transform each value of attribute "foo" in this list using some map in my xslt to a number.
select the max value of the list and output that.
So given the following xml:
<t>
<tag foo="A">apples</tag>
<tag foo="C">oranges</tag>
<tag foo="B">trees</tag>
</t>
And the following mapping:
<xsl:variable name="myMap">
<entry key="A">1</entry>
<entry key="B">2</entry>
<entry key="C">3</entry>
</xsl:variable>
The output would be:
<max>3</max>
Another question, why can't I indent my code? I'm putting spaces but it's not working.
I This standard XSLT 1.0 transformation (most resembling your approach):
<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:variable name="vrtfMap">
<entry key="A" value="1"/>
<entry key="B" value="2"/>
<entry key="C" value="3"/>
<entry key="X" value="8"/>
</xsl:variable>
<xsl:variable name="vMap" select=
"document('')/*/xsl:variable[#name = 'vrtfMap']/*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#foo">
<xsl:attribute name="foo">
<xsl:value-of select="$vMap[#key = current()]/#value"/>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
when applied on the following XML document (as you didn't provide any):
<t foo="X">
<a foo="A">
<b foo="B"/>
</a>
<c foo="C"/>
</t>
produces the wanted, correct result:
<t foo="8">
<a foo="1">
<b foo="2"/>
</a>
<c foo="3"/>
</t>
Explanation: Appropriate use of the XSLT current() function.
II. XSLT 1.0 solution using keys for speed
<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="kValFromKey" match="entry/#value" use="../#key"/>
<xsl:variable name="vrtfMap">
<entry key="A" value="1"/>
<entry key="B" value="2"/>
<entry key="C" value="3"/>
<entry key="X" value="8"/>
</xsl:variable>
<xsl:variable name="vMap" select=
"document('')/*/xsl:variable[#name = 'vrtfMap']/*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="#foo">
<xsl:variable name="vCur" select="."/>
<xsl:attribute name="foo">
<xsl:for-each select="document('')">
<xsl:value-of select="key('kValFromKey', $vCur)"/>
</xsl:for-each>
</xsl:attribute>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), the same correct result is produced.
Explanation:
Use of <xsl:for-each select="document('')"> to set the current document to the stylesheet, so that the key() function will use the key index built for this document.
Saving the node matched by the template in a variable so that we can use it inside the xsl:for-each -- current() cannot be correctly used here, because it gets the current node on which xsl:for-each operates.
UPDATE: The OP has now clarified in a comment that his biggest problem is finding the maximum.
<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:variable name="vrtfMap">
<entry key="A" value="1"/>
<entry key="B" value="2"/>
<entry key="C" value="3"/>
</xsl:variable>
<xsl:variable name="vMap" select=
"document('')/*/xsl:variable[#name = 'vrtfMap']/*"/>
<xsl:template match="/">
<xsl:variable name="vDoc" select="."/>
<xsl:variable name="vFoosMapped"
select="$vMap[#key = $vDoc/*/*/#foo]"/>
<max>
<xsl:value-of select=
"$vFoosMapped
[not($vFoosMapped/#value > #value)]
/#value
"/>
</max>
</xsl:template>
</xsl:stylesheet>
When given this XML document (the one provided by the OP lacks a singlr top element):
<t>
<tag foo="A">apples</tag>
<tag foo="C">oranges</tag>
<tag foo="B">trees</tag>
</t>
the wanted, correct result is produced:
<max>3</max>
Remark: A more efficient way of calculating maximum (or minimum -- in a similar way) in XSLT 1.0 is to do 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:variable name="vrtfMap">
<entry key="A" value="1"/>
<entry key="B" value="2"/>
<entry key="C" value="3"/>
</xsl:variable>
<xsl:variable name="vMap" select=
"document('')/*/xsl:variable[#name = 'vrtfMap']/*"/>
<xsl:template match="/">
<xsl:variable name="vDoc" select="."/>
<xsl:variable name="vFoosMapped"
select="$vMap[#key = $vDoc/*/*/#foo]"/>
<max>
<xsl:for-each select="$vFoosMapped">
<xsl:sort select="#value" data-type="number" order="descending"/>
<xsl:if test="position() = 1">
<xsl:value-of select="#value"/>
</xsl:if>
</xsl:for-each>
</max>
</xsl:template>
</xsl:stylesheet>
Another question, why can't I indent my code? I'm putting spaces but
it's not working.
This is a SO bug that they failed to fix for many months.
Most probably you are using IE. If your version is 9, then do the following:
Press F12.
In the window that pops up click on the right-most menu and select: "Document mode: IE9 Standards"
Now you should be able to see the code with indentation.

Trim first 10 characters off the title of RSS feed

I'm trying to write some xsl to style an RSS feed. I need to trim the first 10 characters off the title of each item.
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/rss">
<ul>
<xsl:for-each select="channel/item">
<li><strong><xsl:value-of select="title"/>
</strong>
More</li>
</xsl:for-each>
</ul>
</xsl:template>
<xsl:template name="trimtitle">
<xsl:param name="string" select="." />
<xsl:if test="$string">
<xsl:text>Foo</xsl:text>
<xsl:call-template name="trimtitle">
<xsl:with-param name="string" select="substring($string, 10)" />
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template match="title">
<xsl:call-template name="title" />
<xsl:value-of select="." />
</xsl:template>
</xsl:stylesheet>
I think you should write your substring function as this:
substring($string,1, 10)
Look at here
http://www.zvon.org/xxl/XSLTreference/Output/function_substring.html
What are you doing in your trimtitle template?
Why are you calling trimtitle recursive..?
The easiest way to show a trimmed string is with:
<xsl:value-of select="substring(title,0,10)"/>

Resources