grouping with complex "selection" - grouping

This is the source XML:
<root>
<!-- a and b have the same date entries, c is different -->
<variant name="a">
<booking>
<date from="2017-01-01" to="2017-01-02" />
<date from="2017-01-04" to="2017-01-06" />
</booking>
</variant>
<variant name="b">
<booking>
<date from="2017-01-01" to="2017-01-02" />
<date from="2017-01-04" to="2017-01-06" />
</booking>
</variant>
<variant name="c">
<booking>
<date from="2017-04-06" to="2017-04-07" />
<date from="2017-04-07" to="2017-04-09" />
</booking>
</variant>
</root>
I'd like to group the three variants so that each variants with same #from and #to in each date should be grouped together.
My attempt is:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"></xsl:output>
<xsl:template match="root">
<variants>
<xsl:for-each-group select="for $i in variant return $i" group-by="booking/date/#from">
<group>
<xsl:attribute name="cgk" select="current-grouping-key()"/>
<xsl:copy-of select="current-group()"></xsl:copy-of>
</group>
</xsl:for-each-group>
</variants>
</xsl:template>
</xsl:stylesheet>
But this gives too many groups. (How) is this possible to achieve?

Using a composite key and XSLT 3.0 you could use
<xsl:template match="root">
<variants>
<xsl:for-each-group select="variant" group-by="booking/date/(#from, #to)" composite="yes">
<group key="{current-grouping-key()}">
<xsl:copy-of select="current-group()"/>
</group>
</xsl:for-each-group>
</variants>
</xsl:template>
which should group any variant elements together which have the same descendant date element sequence.
XSLT 3.0 is supported by Saxon 9.8 (any edition) or 9.7 (PE and EE) or a 2017 release of Altova XMLSpy/Raptor.
Using XSLT 2.0 you could concatenate all those date values with string-join():
<xsl:template match="root">
<variants>
<xsl:for-each-group select="variant" group-by="string-join(booking/date/(#from, #to), '|')">
<group key="{current-grouping-key()}">
<xsl:copy-of select="current-group()"/>
</group>
</xsl:for-each-group>
</variants>
</xsl:template>
Like the XSLT 3.0 solution, it only groups variant with the same sequence of date descendants, I am not sure whether that suffices or whether you might want to sort any date descendants first before computing the grouping key. In the XSLT 3 case you could do that easily with
<xsl:for-each-group select="variant" group-by="sort(booking/date, (), function($d) { xs:date($d/#from), xs:date($d/#to) })!(#from, #to)" composite="yes">
inline (although that leaves 9.8 HE behind as it does not support function expressions/higher order functions, so there you would need to move the sorting to your own user-defined xsl:function and in there use xsl:perform-sort).

Related

Sort data in the xml alphabetical order

Input XML :
<?xml version="1.0" encoding="utf-8" ?>
<infoset>
<info>
<title>Bill</title>
<group>
<code>state</code>
</group>
</info>
<info>
<title>Auto</title>
<group>
<code>state</code>
</group>
</info>
<info>
<title>Auto2</title>
</info>
<info>
<title>Auto3</title>
</info>
<info>
<title>Auto5</title>
</info>
<info>
<title>Certificate</title>
<group>
<code>Auto4</code>
</group>
</info>
</infoset>
Expected output :
A
Auto2
Auto3
Auto4
Certificate
Auto5
S
state
Auto
Bill
I need to arrange the title and code in alphabetical order.If the info has group the tile should come under the group. I am using visual studio2010 , xslt1.0 Processor and xml editor.
Sorting in XSLT is straight-forward. What you are actually really needing to know is how to 'group' items. As Michael Kay commented, this is much easier in XSLT 2.0 than in XSLT 1.0. In XSLT 1.0 you tend to use the Muenchian Grouping method, which appears confusing when you first see it, but is generally the most efficient way of doing it.
From the looks of your output, you are doing two lots of grouping. Firstly, by the first letter, then by either group/code (if it exists), or title.
Muenchian Grouping works by defining a key, to enable quick look up of all items in a 'group'. For the first letter of eithe group/code or title, you would define it like so
<xsl:key name="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
(Note: This is case sensitive, so you may need to use the 'translate' function if you can have a mix of lower and upper case start letters).
If group/code exists, it will use the first letter of that, otherwise it will pick up the first letter of the title.
For the group/code or title itself, the key would be as follows
<xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
So, it only uses "title" elements where there is no "group" element present.
To get the distinct first letters for your first grouping, you select all the info elements and check whether they are the first element in the key for their given letter. This is done like so
<xsl:apply-templates
select="info
[generate-id()
= generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]"
mode="letter">
<xsl:sort select="substring(concat(group/code, title), 1, 1)" />
</xsl:apply-templates>
The 'mode' is used here because the final XSLT will have to templates matching info.
Within the matching template, to group by either code/group or title you can then do this
<xsl:apply-templates
select="key('letter', $letter)
[generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
<xsl:sort select="title[not(../group)]|group/code" />
</xsl:apply-templates>
And finally, to output all the elements within the final group, you would just use the key again
<xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
Here is the full XSLT.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:key name="letter" match="info" use="substring(concat(group/code, title), 1, 1)"/>
<xsl:key name="info" match="info" use="title[not(../group)]|group/code"/>
<xsl:template match="/*">
<xsl:apply-templates select="info[generate-id() = generate-id(key('letter', substring(concat(group/code, title), 1, 1))[1])]" mode="letter">
<xsl:sort select="substring(concat(group/code, title), 1, 1)" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="info" mode="letter">
<xsl:variable name="letter" select="substring(concat(group/code, title), 1, 1)" />
<xsl:value-of select="concat($letter, '
')" />
<xsl:apply-templates select="key('letter', $letter)[generate-id() = generate-id(key('info', title[not(../group)]|group/code)[1])]">
<xsl:sort select="title[not(../group)]|group/code" />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="info">
<xsl:variable name="value" select="title[not(../group)]|group/code" />
<xsl:value-of select="concat($value, '
')" />
<xsl:apply-templates select="key('info', $value)[group/code=$value]/title">
<xsl:sort select="." />
</xsl:apply-templates>
</xsl:template>
<xsl:template match="title">
<xsl:value-of select="concat(' ', ., '
')" />
</xsl:template>
</xsl:stylesheet>
When applied to your XML, the following is output
A
Auto2
Auto3
Auto4
Certificate
Auto5
s
state
Auto
Bill

Format date in xslt

I have following xml
<Report>
<Items>
<Item>
<Id>1</Id>
<TotalSent>251</TotalSent>
<Opened>48</Opened>
<LastSend>01/07/2013 16:38:18</LastSend>
<Bounced>1</Bounced>
<Unopened>202</Unopened>
</Item>
</Items>
</Report>
i want to transform it to another xml using xslt , my desired o/p is like below
<chart subcaption ="Last sent on Monday 01 July 2013 at 16:38">
<set label="Opened" value="48"/>
<set label="Bounced" value="1"/>
</chart>
I am not able to get date as i want for subcaption attribute.
I tried below xslt code but it is not working
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:ms="urn:schemas-microsoft-com:xslt">
<xsl:output method="xml" indent="yes" omit-xml-declaration="no"/>
<xsl:template match="/">
<chart>
<xsl:variable name='lastSend' select='Report/Items/Item/LastSend' />
<xsl:attribute name="subcaption">
<xsl:value-of select="ms:format-date($lastSend, ' Last sent on MMM dd, yyyy at')"/>
<xsl:value-of select="ms:format-time($lastSend, ' hh:mm')"/>
</xsl:attribute>
<xsl:for-each select="Report/Items/Item">
<set>
<xsl:attribute name="label">Opened</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="Opened" />
</xsl:attribute>
</set>
<set>
<xsl:attribute name="label">Bounced</xsl:attribute>
<xsl:attribute name="value">
<xsl:value-of select="Bounced" />
</xsl:attribute>
</set>
</xsl:for-each>
</chart>
</xsl:template>
</xsl:stylesheet>
when i am passing hard coded value in ms:format-date() & ms:format-time() functions, like 01/07/2013 16:38:18 it was working fine , but when i am passing variable value $lastSend it is not working.
Note: I can use any version of xsl.
If you want to use XSLT 2.0 then you need to convert your custom date respectively dateTime format into an xs:dateTime and then you can use the format-dateTime function that XSLT 2.0 provides (see http://www.w3.org/TR/xslt20/#format-date):
<xsl:template match="LastSend">
<!-- 01/07/2013 16:38:18 -->
<xsl:variable name="dt" as="xs:dateTime" select="xs:dateTime(concat(substring(., 7, 4), '-', substring(., 4, 2), '-', substring(., 1, 2), 'T', substring(., 12)))"/>
<xsl:attribute name="subcaption" select="format-dateTime($dt, 'Last sent on [F] [D01] [MNn] [Y0001] at [H01]:[m01]')"/>
</xsl:template>
Take the above second argument "picture string" as an example on how to format a dateTime, you might need to adjust it for your needs, based on the picture string arguments documented in the XSLT 2.0 specification.

XSLT - how to apply a template to every node of the type?

I am quite new to xsl and functional programming, so I'll be grateful for help on this one:
I have a template that transforms some xml and provides an output. The problem is that there are many elements of type xs:date, all in different contexts, that must be localized. I use a concatenation of substrings of these xs:dates to produce a localized date pattern strings.
As you can guess this causes a lot of copy-paste "substring-this and substring-that". How can I write a template that will automatically transform all the elements of type xs:date to localized strings preserving all the context-aware transformations?
My xsl is something like this:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html" encoding="utf-8"/>
<xsl:template match="/">
...
<input value="{substring(/select/a/date 9,2)}.{substring(/select/a/date, 6,2)}.{substring(/select/a/date 1,4)}">
...
<!-- assume that following examples are also with substrings -->
<div><xsl:value-of select="different-path/to_date"/></div>
...
<table>
<tr><td><xsl:value-of select="path/to/another/date"/></td></tr>
</table>
<apply-templates/>
</xsl:template>
<xsl:template match="something else">
<!-- more dates here -->
</xsl:template>
</xsl:stylesheet>
I hope I managed to make my question clear =)
UPD: Here is an example of xml:
<REQUEST>
<header>
<... />
<ref>
<ref_date type="xs:date">1970-01-01</ref_date>
</ref>
</header>
<general>
<.../>
<info>
<.../>
<date type="xs:date">1970-01-01</date>
<ExpireDate type="xs:date">1970-01-01</ExpireDate>
<RealDate type="xs:date">1970-01-01</RealDate>
<templateDetails>template details</templateDetails>
<effectiveDate type="xs:date">1970-01-01</effectiveDate>
</info>
<party>
<.../>
<date type="xs:date">1970-01-01</date>
</party>
<!-- many other parts of such kind -->
</general>
</REQUEST>
As for the output, there are many different options. The main thing is that these values must be set as a value of different html objects, such as tables, input fields and so on. You can see an example in the xsl listing.
P.S. I'm using xsl 1.0.
If you did a schema-aware XSLT 2.0 transformation, you wouldn't need all those type='xs:date' attributes: defining it in the schema as a date would be enough. You could then match the attributes with
<xsl:template match="attribute(*, xs:date)">
What you could do is add a template to match any element which has an #type attribute of 'xs:date', and do you substring manipulation in there
<xsl:template match="*[#type='xs:date']">
<xsl:value-of select="translate(., '-', '/')" />
</xsl:template>
In this case I am just replacing the hyphens by slashes as an example.
Then, instead of using xsl:value-of....
<div><xsl:value-of select="different-path/to_date"/></div>
You could use xsl:apply-templates
<div><xsl:apply-templates select="different-path/to_date"/></div>
Consider this XSLT as an example
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="*[#type='xs:date']">
<xsl:copy>
<xsl:value-of select="translate(., '-', '/')" />
</xsl:copy>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
In this case, all this XSLT is doing is copying the XML document as-is, but changing the date elements.
If you wanted to use the date template for other elements, or values, you could also make it a named-template, like so
<xsl:template match="*[#type='xs:date']" name="date">
<xsl:param name="date" select="." />
<xsl:value-of select="translate($date, '-', '/')" />
</xsl:template>
This would allow you to also call it much like a function. For example, to format a data and add as an attribute you could do the following:
<input>
<xsl:attribute name="value">
<xsl:call-template name="date">
<xsl:with-param name="date" select="/select/a/date" />
</xsl:call-template>
</xsl:attribute>
</input>

XPath/XSLT nested predicates: how to get the context of outer predicate?

It seems that this question was not discussed on stackoverflow before, save for Working With Nested XPath Predicates ... Refined where the solution not involving nested predicates was offered.
So I tried to write the oversimplified sample of what I'd like to get:
Input:
<root>
<shortOfSupply>
<food animal="doggie"/>
<food animal="horse"/>
</shortOfSupply>
<animalsDictionary>
<cage name="A" animal="kittie"/>
<cage name="B" animal="dog"/>
<cage name="C" animal="cow"/>
<cage name="D" animal="zebra"/>
</animals>
</root>
Output:
<root>
<hungryAnimals>
<cage name="B"/>
<cage name="D"/>
</hungryAnimals>
</root>
or, alternatively, if there is no intersections,
<root>
<everythingIsFine/>
</root>
And i want to get it using a nested predicates:
<xsl:template match="cage">
<cage>
<xsl:attribute name="name">
<xsl:value-of select="#name"/>
</xsl:attribute>
</cage>
</xsl:template>
<xsl:template match="/root/animalsDictionary">
<xsl:choose>
<!-- in <food> in <cage> -->
<xsl:when test="cage[/root/shortOfSupply/food[ext:isEqualAnimals(./#animal, ?????/#animal)]]">
<hungryAnimals>
<xsl:apply-templates select="cage[/root/shortOfSupply/food[ext:isEqualAnimals(#animal, ?????/#animal)]]"/>
</hungryAnimals>
</xsl:when>
<xsl:otherwise>
<everythingIsFine/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
So what should i write in place of that ??????
I know i could rewrite the entire stylesheet using one more template and extensive usage of variables/params, but it makes even this stylesheet significantly more complex, let alone the real stylesheet i have for real problem.
It is written in XPath reference that the dot . sign means the current context node, but it doesn't tell whether there is any possibility to get the node of context before that; and i just can't believe XPath is missing this obvious feature.
XPath 2.0 one-liner:
for $a in /*/animalsDictionary/cage
return
if(/*/shortOfSupply/*[my:isA($a/#animal, #animal)])
then $a
else ()
When applied on the provided XML document selects:
<cage name="B"/>
<cage name="D"/>
One cannot use a single XPath 1.0 expression to find that a given cage contains a hungry animal.
Here is an XSLT solution (XSLT 2.0 is used only to avoid using an extension function for the comparison -- in an XSLT 1.0 solution one will use an extension function for the comparison and the xxx:node-set() extension to test if the RTF produced by applying templates in the body of the variable contains any child element):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:my="my:my" exclude-result-prefixes="xs my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<my:Dict>
<a genName="doggie">
<name>dog</name>
<name>bulldog</name>
<name>puppy</name>
</a>
<a genName="horse">
<name>horse</name>
<name>zebra</name>
<name>pony</name>
</a>
<a genName="cat">
<name>kittie</name>
<name>kitten</name>
</a>
</my:Dict>
<xsl:variable name="vDict" select=
"document('')/*/my:Dict/a"/>
<xsl:template match="/">
<root>
<xsl:variable name="vhungryCages">
<xsl:apply-templates select=
"/*/animalsDictionary/cage"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="$vhungryCages/*">
<hungryAnimals>
<xsl:copy-of select="$vhungryCages"/>
</hungryAnimals>
</xsl:when>
<xsl:otherwise>
<everythingIsFine/>
</xsl:otherwise>
</xsl:choose>
</root>
</xsl:template>
<xsl:template match="cage">
<xsl:if test="
/*/shortOfSupply/*[my:isA(current()/#animal,#animal)]">
<cage name="{#name}"/>
</xsl:if>
</xsl:template>
<xsl:function name="my:isA" as="xs:boolean">
<xsl:param name="pSpecName" as="xs:string"/>
<xsl:param name="pGenName" as="xs:string"/>
<xsl:sequence select=
"$pSpecName = $vDict[#genName = $pGenName]/name"/>
</xsl:function>
</xsl:stylesheet>
When this transformation is applied on the provided XML document (corrected to be well-formed):
<root>
<shortOfSupply>
<food animal="doggie"/>
<food animal="horse"/>
</shortOfSupply>
<animalsDictionary>
<cage name="A" animal="kittie"/>
<cage name="B" animal="dogs"/>
<cage name="C" animal="cow"/>
<cage name="D" animal="zebras"/>
</animalsDictionary>
</root>
the wanted, correct result is produced:
<root>
<hungryAnimals>
<cage name="B"/>
<cage name="D"/>
</hungryAnimals>
</root>
Explanation: Do note the use of the XSLT current() function.
XPath 1.0 is not "relationally complete" - it can't do arbitrary joins. If you're in XSLT, you can always get round the limitations by binding variables to intermediate nodesets, or (sometimes) by using the current() function.
XPath 2.0 introduces range variables, which makes it relationally complete, so this limitation has gone.
Doesn't <xsl:when test="cage[#animal = /root/shortOfSupply/food/#animal]"> suffice to express your test condition?
Notice The dot operator in XPath is related to the current context. In XSLT the current template context_ is given by the function current(), which most of the time (not always) coincides with the ..
You can perform the test (and the apply templates as well), using the parent axis abbreviation (../):
cage[#animal=../../shortOfSupply/food/#animal]
Moreover the match pattern in the the first template is wrong, it should be relative to the root:
/root/animalsDictionary
#Martin suggestion is also obviously correct.
Your final template slightly modified:
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="root/animalsDictionary">
<xsl:choose>
<xsl:when test="cage[#animal=../../shortOfSupply/food/#animal]">
<hungryAnimals>
<xsl:apply-templates select="cage[#animal
=../../shortOfSupply/food/#animal]"/>
</hungryAnimals>
</xsl:when>
<xsl:otherwise>
<everythingIsFine/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="cage">
<cage name="{#name}"/>
</xsl:template>
</xsl:stylesheet>

How to use group by in xslt

I have a xml that has so many elements and most of that contain attributes.. for some of the attributes values are same so I need to group them and generate diff xml.
I/p Ex:
<TestNode>
<ABC1 value="10.7" format="$" />
<ABC2 value="10.5" format="$" />
<ABC3 value="20" format="Rs" />
<ABC4 value="50" format="Rs" />
<ABC5 value="10.5" format="$" />
</TestNode>
I need to group the rows by format. Note: Format is not fixed... it may grow ...
O/P Ex:
is it possible to get ? Thanks in advance...
In XSLT 1.0 you would use Muenchian grouping.
Define a key "format", from which we can easily select all elements given a format name. Than apply Muenchian grouping to find the unique formats in the input.
Then it gets simple. The "*" template will be applied once per format, and uses the key() to fetch all entries for that format.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:key name="format" match="TestNode/*" use="#format" />
<xsl:template match="TestNode">
<body>
<xsl:apply-templates select="*[generate-id(.)=generate-id(key('format',#format)[1])]"/>
</body>
</xsl:template>
<xsl:template match="*">
<format format="{#format}">
<xsl:copy-of select="key('format', #format)" />
</format>
</xsl:template>
</xsl:stylesheet>
In XSLT 2.0 you should be able to do it with <xsl:for-each-group>, current-grouping-key() and current-group()
Example:
<xsl:for-each-group
select="TestNode/*"
group-by="#format"
>
<group format="{current-grouping-key()}">
<xsl:for-each select="current-group()">
<xsl:copy-of select="."/>
</xsl:for-each>
</group>
</xsl:for-each-group>
See: http://www.w3.org/TR/xslt20/#grouping