Sorting xslt nodes based on sibling - sorting

I am having some trouble getting this to work and I could really need some help.
I want to store the smallest variable that has a sibling which is not null.
I have a XML which looks like this:
<Root>
<Item>
<ValueOne>5</ValueOne>
<Item>bababa</Item>
</Item>
<Item>
<ValueOne>3</ValueOne>
<Item>ababa</Item>
</Item>
<Item>
<ValueOne>1</ValueOne>
<Item/>
</Item>
</Root>
I want the smallest ValueOne who's sibling Item has a value.
I was thinking something like:
<xsl:variable name="var">
<xsl:for-each select="Root/Item">
<xsl:if test="not(/Item = '')">
<xsl:sort data-type="text"/>
<xsl:if test="position()=1">
<xsl:value-of select="/Item"/>
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:variable>
But this doesn't work and I am not sure why. As you might tell I'm not the best at xslt. I cannot rename anything in the xml or change the structure of it.

Assuming those spaces and returns are non-essential, you could use:
<xsl:variable name="var">
<xsl:for-each select="Root/Item[normalize-space(Item)]">
<xsl:sort select="ValueOne" data-type="number" order="ascending"/>
<xsl:if test="position()=1">
<xsl:value-of select="normalize-space(ValueOne)"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
With all the redundant white space removed, this can be simplified to:
<xsl:variable name="var">
<xsl:for-each select="Root/Item[Item/text()]">
<xsl:sort select="ValueOne" data-type="number" order="ascending"/>
<xsl:if test="position()=1">
<xsl:value-of select="ValueOne"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>

Related

XSLT: sort and limit nested nodes

XML:
<node>
<node date="01-01-2002">Node</node>
<node date="01-01-2005">Node</node>
<node date="01-01-2001">Node</node>
<node date="01-01-2003">Node</node>
<node date="01-01-2006">Node</node>
<node>
<node date="01-01-2000">Node</node>
<node date="01-01-2007">Node</node>
</node>
<node date="01-01-2004">Node</node>
</node>
Problem:
I need to sort by date AND take a limited number of sorted nodes. Need to be able to traverse any number of levels.
Required result:
<p>01-01-2000</p>
<p>01-01-2001</p>
<p>01-01-2002</p>
<p>01-01-2003</p>
<p>01-01-2004</p>
Assumptions:
For sorting by date I use c# extension method that returns time stamp:
<xsl:sort select="cs:formatDate(#date)" order="ascending" data-type="number" />
Limit to 5 oldest nodes.
Order: ascending
XSLT 1.0
EDIT:
As requested this is where i got so far:
I can do sorting and limiting for not nested nodes:
<xsl:template match="node">
<xsl:apply-templates select="node">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
<xsl:with-param name="limit" select="5"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node[#date]">
<xsl:param name="limit" />
<xsl:if test="position() < $limit+1">
<h5><xsl:value-of select="#date"/></h5>
</xsl:if>
</xsl:template>
Or when I try to apply for nested as below, I get nested nodes sorted in isolation, and I cannot limit them in same way anymore:
<xsl:template match="*">
<xsl:apply-templates select="node[#date]">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
</xsl:apply-templates>
<xsl:apply-templates select="node[not(#date)]">
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node[#date]">
<h5><xsl:value-of select="#date"/></h5>
</xsl:template>
<xsl:template match="node[not(#date)]">
<xsl:apply-templates select="node[#date]">
<xsl:sort select="cs:formatDate(#date,'dd-MM-yyyy','timestamp')" order="ascending" data-type="number" />
</xsl:apply-templates>
<xsl:apply-templates select="node[not(#date)]">
</xsl:apply-templates>
</xsl:template>
EDIT:
I thought it is obvious, but probably not: I need sort to be applied before the limit. E.g: "get oldest five" and NOT:"get first five nodes from xml and then sort them"
<xsl:template match="/">
<xsl:apply-templates select="//node[#date]">
<xsl:sort select="concat(substring-after(substring-after(#date,'-'),'-'),substring-before(substring-after(#date,'-'),'-'),substring-before(#date,'-'))" order="ascending" data-type="number" />
<xsl:with-param name="start" select="1"/>
<xsl:with-param name="end" select="5"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="node">
<xsl:param name="start" />
<xsl:param name="end" />
<xsl:if test="position() >= $start and position() <= $end">
<p>
<xsl:value-of select="#date"/>
</p>
</xsl:if>
</xsl:template>

XSLT concatenate input from several nodes in a single output

I'm trying to work out a transformation that will process an input with several Flights with Departure and Arrival into a single output with the complete route for the flights.
Input is as follows:
<FlightTrip>
<flights>
<departureAirport>
<airportCode>LocB</airportCode>
</departureAirport>
<departureTime>2013-03-28T10:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocC</airportCode>
</arrivalAirport>
</flights>
<flights>
<departureAirport>
<airportCode>LocA</airportCode>
</departureAirport>
<departureTime>2013-03-27T15:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocB</airportCode>
</arrivalAirport>
</flights>
<flights>
<departureAirport>
<airportCode>LocC</airportCode>
</departureAirport>
<departureTime>2013-03-30T14:00:00.000</departureTime>
<arrivalAirport>
<airportCode>LocD</airportCode>
</arrivalAirport>
</flights>
</FlightTrip>
The desired output would be this:
<FullTrip>LocA LocB LocC LocD</FullTrip>
I've tried to use foreach inside the output variable but I can't get it right. I also need to sort the input based on the departure date as the Flights can be in a different order (as per the sample input).
Any ideas of how to achieve this?
Thanks a lot!
Bruno
<?xml version="1.0" encoding="UTF-8" ?>
<xsl:transform xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output indent="yes"/>
<xsl:template match="FlightTrip">
<FullTrip>
<xsl:apply-templates select="flights">
<xsl:sort select="departureTime"/>
</xsl:apply-templates>
</FullTrip>
</xsl:template>
<xsl:template match="flights">
<xsl:value-of select="departureAirport/airportCode"/><xsl:text> </xsl:text>
<xsl:if test="position()=last()">
<xsl:value-of select="arrivalAirport/airportCode"/>
</xsl:if>
</xsl:template>
</xsl:transform>
Will produce:
<FullTrip>LocA LocB LocC LocD</FullTrip>
Working example
Thanks to Joepie for the enlightenment. I had to modify it a bit to get it to work in my environment, ended up using foreach as below:
<xsl:template match="/">
<xsl:variable name="locations">
<xsl:for-each select="/FlightTrip/flights">
<xsl:sort select="departureTime" order="ascending" data-type="text"/>
<xsl:value-of select="concat(departureAirport/airportCode,' - ')"/>
<xsl:if test="position() = last()">
<xsl:value-of select="arrivalAirport/airportCode"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<FullTrip>
<xsl:value-of select="$locations"/>
</FullTrip>
</xsl:template>
When applied to the example produces the output below:
<FullTrip>LocA - LocB - LocC - LocD</FullTrip>
Thanks again!

is there a way to emulate correctly replace function on XPATH 1?

I have this function which tries to replace dots and/or - with _
I'm limited to use xpath 1 so replace function is NOT an option. The template works not to much fine because if I use something like this:
FOO-BAR.THING-MADRID.html
it gives me out on screen this thing:
FOO-BAR.THING-MADRID.html
the middle dot is not replaced.
Someone could help me?
<xsl:template name="replaceDots">
<xsl:param name="outputString"/>
<xsl:variable name="target">.</xsl:variable>
<xsl:variable name="source">-</xsl:variable>
<xsl:variable name="replacement">_</xsl:variable>
<xsl:choose>
<xsl:when test="contains($outputString,$source)">
<xsl:value-of select="concat(substring-before($outputString,$source),$replacement)" disable-output-escaping="yes"/>
<xsl:call-template name="replaceDots">
<xsl:with-param name="outputString" select="substring-after($outputString,$source)"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="contains($outputString,$target)">
<xsl:value-of select="concat(substring-before($outputString,$target),$replacement)" disable-output-escaping="yes"/>
<xsl:call-template name="replaceDots">
<xsl:with-param name="outputString" select="substring-after($outputString,$target)"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$outputString" disable-output-escaping="yes"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
To replace all dots or dashes with underscores, you do not need an <xsl:template>. You can use:
<xsl:value-of select="translate(., '-.', '__')" />
If you want to keep the ".html", you can extend this like so:
<xsl:value-of select="
concat(
translate(substring-before(., '.html'), '-.', '__'),
'.hmtl'
)
" />
For a generic "string replace" template in XSLT, look at this question, for example.

Removing the last characters in an XSLT string

In my CMS it is possible to create a new article, and choose an image to be shown on that article. When an image is chosen, a thumbnail of the image will automatically be created as well.
If the uploaded image is called image.jpg, then the corresponding thumbnail will automatically be named image_thumbnail.jpg.
I would now like to use the thumbnail image, everywhere on the website where the article is mentioned, except in the article itself (where the original big image should be shown).
But how can I do that?
I imagine if I could get the original name of the image, and then split it up before the suffix (.jpg, .png, .jpeg etc.) and hardcode _thumbnail after the name, then that would be sufficient.
In other words, I want to take the complete filename, and cut it into two parts, so that I can insert the string _thumbnail between the two parts.
Maybe that would work, but what if an image called image.2horses.jpg (a file with more than one dot in the filename) is uploaded? A naive cut before the '.' wouldn't work here.
Is there a way to get around this? Perhaps by cutting the filename up before the last 4 (.jpg, .png) or 5 (.jpeg) characters?
Off the top of my head:
<xsl:template name="substring-before-last">
<xsl:param name="string1" select="''" />
<xsl:param name="string2" select="''" />
<xsl:if test="$string1 != '' and $string2 != ''">
<xsl:variable name="head" select="substring-before($string1, $string2)" />
<xsl:variable name="tail" select="substring-after($string1, $string2)" />
<xsl:value-of select="$head" />
<xsl:if test="contains($tail, $string2)">
<xsl:value-of select="$string2" />
<xsl:call-template name="substring-before-last">
<xsl:with-param name="string1" select="$tail" />
<xsl:with-param name="string2" select="$string2" />
</xsl:call-template>
</xsl:if>
</xsl:if>
</xsl:template>
Called as:
<xsl:template match="/">
<xsl:variable name="filename" select="'image.2horses.jpg'" />
<xsl:variable name="basename">
<xsl:call-template name="substring-before-last">
<xsl:with-param name="string1" select="$filename" />
<xsl:with-param name="string2" select="'.'" />
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="$basename" />
</xsl:template>
Yields:
image.2horses
Given the image's filename in $filename,
If you can assume that all images will end in ".jpg" and won't have ".jpg" elsewhere in the filename, then this should work:
<img src="{substring-before($filename, '.jpg')}_thumbnail.jpg" ... />
If you don't know the image type (like, you want to handle gif and png as well), or if you think the extension may occur multiple times in the filename ("image.jpg.jpg"), then you will want a template to help you:
<?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" indent="yes"/>
<xsl:template match="/">
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.09.07.11.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.gif'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image with spaces.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image with irregular spaces.jpg'"/>
</xsl:call-template>
</p>
<p>
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="'image.jpg.again.jpg'"/>
</xsl:call-template>
</p>
</xsl:template>
<xsl:template name="image_thumbnail">
<xsl:param name="filename"/>
<xsl:choose>
<xsl:when test="contains($filename, '.')">
<xsl:variable name="before" select="substring-before($filename, '.')"/>
<xsl:variable name="after" select="substring-after($filename, '.')"/>
<xsl:choose>
<xsl:when test="contains($after, '.')">
<xsl:variable name="recursive">
<xsl:call-template name="image_thumbnail">
<xsl:with-param name="filename" select="$after"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat($before, '.', $recursive)"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="concat($before, '_thumbnail.', $after)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$filename"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
A general solution involving only standard XSLT is somewhat hard since you have to search the string from the end. You can split your filename usings two functions, substring-before-last and substring-after-last. Unfortunately, these functions are not part of XSLT. You can Google and try to find implementations. Assuming you have these two functions implemented as XSLT templates you can then use the following template to generate thumbnail names:
<xsl:template name="thumbnail-name">
<xsl:param name="file-name"/>
<xsl:call-template name="substring-before-last">
<xsl:with-param name="text" select="$file-name"/>
<xsl:with-param name="chars" select="'.'"/>
</xsl:call-template>
<xsl:text>_thumbnail.</xsl:text>
<xsl:call-template name="substring-after-last">
<xsl:with-param name="text" select="$file-name"/>
<xsl:with-param name="chars" select="'.'"/>
</xsl:call-template>
</xsl:template>
You can use the template like this (assuming the variable $file-name contains the name of the image):
<img>
<xsl:attribute name="src">
<xsl:call-template name="thumbnail-name">
<xsl:with-param name="file-name" select="$file-name"/>
</xsl:call-template>
</xsl:attribute>
</img>
Have a look at the XPath functions overview at W3Schools, specifically the substring-before method.
I believe XPath functions operating on string might help you. I would try with some simple replace or translate.
XSLT 2 solution using regexp:
replace($filename, '(\.[^\.]*)$', concat('_thumbnail', '$1'))
Original answer (also XSLT 2):
This removes all after the last separator (including the separator). So below the $separatorRegexp could be '\.jpg' or just '\.' and the $separator '.jpg' or '.' in the other case.
string-join(reverse(remove(reverse(tokenize($filename, $separatorRegexp)),1)),$separator)
Eventually the '_thumbnail.jpg' can be appended with concat.

Modify an XML node using [MS] XSLT Script

I would like to select a node and modify its attributes and child-nodes using an
xsl:script function. In addition, templates matching child-nodes of that node should
STILL perform their job (after script is done processing the node).
Can it be done using XSLT?
Can you please provide an example / skeleton for such a transformation?
Yes, it can be done. I don't seem to see what the problem is because the XML (or whatever output) of an XSL script is buffered independently from its input.
This is illustrated in the following example whereby a simple XSL script copies an input XML document mostly as-is, changing a few things:
the root element name and attribute
flattening by removing the element from the hierarchy
dropping the results/date element
rename the item's 'source' attribute 'origin'
change the item's 'level' attribute value
rename the FirstName and LastName elements of the item elements
Sample input
<?xml version="1.0" encoding="ISO-8859-1"?>
<MyRoot version="1.2">
<results>
<info>Alpha Bravo</info>
<author>Employee No 321</author>
<date/>
<item source="www" level="6" cost="33">
<FirstName>Jack</FirstName>
<LastName>Frost</LastName>
<Date>1998-10-30</Date>
<Organization>Lemon growers association</Organization>
</item>
<item source="db-11" level="1" cost="65" qry="routine 21">
<FirstName>Mike</FirstName>
<LastName>Black</LastName>
<Date>2006-10-30</Date>
<Organization>Ford Motor Company</Organization>
</item>
</results>
</MyRoot>
Output produced
<?xml version="1.0" encoding="utf-16"?>
<MyNewRoot version="0.1">
<author>Employee No 321</author>
<info>Alpha Bravo</info>
<item cost="33" origin="www" level="77">
<GivenName>Jack</GivenName>
<FamilyName>Frost</FamilyName>
<Date>1998-10-30</Date>
<Organization>Lemon growers association</Organization>
</item>
<item cost="65" qry="routine 21" origin="db-11" level="77">
<GivenName>Mike</GivenName>
<FamilyName>Black</FamilyName>
<Date>2006-10-30</Date>
<Organization>Ford Motor Company</Organization>
</item>
</MyNewRoot>
XSL script
<?xml version='1.0'?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
exclude-result-prefixes="#default">
<xsl:template match="MyRoot">
<xsl:call-template name="MainTemplate">
</xsl:call-template>
</xsl:template>
<xsl:template name="MainTemplate">
<MyNewRoot version="0.1">
<xsl:copy-of select="results/author" />
<xsl:copy-of select="results/info" />
<xsl:for-each select="results/item">
<xsl:call-template name="FixItemElement"/>
</xsl:for-each>
</MyNewRoot>
</xsl:template>
<xsl:template name="FixItemElement">
<xsl:copy>
<xsl:copy-of select="#*[not(name()='source' or name()='level')]" />
<xsl:attribute name="origin">
<xsl:value-of select="#source"/>
</xsl:attribute>
<xsl:attribute name="level">
<xsl:value-of select="77"/>
</xsl:attribute>
<xsl:for-each select="descendant::*">
<xsl:choose>
<xsl:when test="local-name(.) = 'FirstName'">
<GivenName>
<xsl:value-of select="."/>
</GivenName>
</xsl:when>
<xsl:when test="local-name(.) = 'LastName'">
<FamilyName>
<xsl:value-of select="."/>
</FamilyName>
</xsl:when>
<xsl:otherwise>
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:copy>
</xsl:template>

Resources