How to partition dates with XSLT - xslt

I have a group of dates and I'd like to create partitions with a criterion such as "exactly 7 days apart" For example this is my source xml:
<root>
<entry date="2019-05-12" />
<entry date="2019-05-19" />
<entry date="2019-05-26" />
<entry date="2019-06-16" />
<entry date="2019-06-23" />
</root>
The result should be like this:
<root>
<group>
<val>12.5.</val>
<val>19.5.</val>
<val>26.5.</val>
</group>
<group>
<val>16.6.</val>
<val>23.6.</val>
</group>
</root>
since the first three and the last two dates are all on a Sunday without a gap.
What I have so far is this:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:sd="urn:someprefix"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
>
<xsl:output indent="yes"/>
<xsl:template match="root">
<root>
<xsl:copy-of select="sd:partition(distinct-values(for $i in entry/#date return $i cast as xs:date))"/>
</root>
</xsl:template>
<xsl:function name="sd:partition">
<xsl:param name="dates" as="xs:date*"/>
<xsl:for-each-group select="$dates" group-adjacent="format-date(., '[F]')">
<group>
<xsl:for-each select="current-group()">
<val>
<xsl:value-of select="format-date(.,'[D].[M].')"/>
</val>
</xsl:for-each>
</group>
</xsl:for-each-group>
</xsl:function>
</xsl:stylesheet>
Which only generates one group.
How can I ask for the previous element to be 7 days apart? I know of duration (xs:dayTimeDuration('P1D')), but I don't know how to compare it to a previous value.
I use Saxon 9.8 HE.

I think you can also do it using group-adjacent:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes"
version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="root">
<xsl:copy>
<xsl:for-each-group select="entry/#date/xs:date(.)"
group-adjacent=". - (position() - 1) * xs:dayTimeDuration('P7D')">
<group>
<xsl:apply-templates select="current-group()"/>
</group>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match=".[. instance of xs:date]">
<val>{format-date(.,'[D].[M].')}</val>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/ncdD7mM

To do your grouping, you really need to know the difference in days with the previous element, then you can group starting with dates where the difference is not 7 days. So, you can declare a variable where you build up some new XML with the dates and differences, and then use that to group.
Try this function in your XSLT instead.
<xsl:function name="sd:partition">
<xsl:param name="dates" as="xs:date*"/>
<xsl:variable name="datesWithDiff" as="element()*">
<xsl:for-each select="$dates">
<xsl:variable name="pos" select="position()" />
<date diff="{(. - $dates[$pos - 1]) div xs:dayTimeDuration('P1D')}">
<xsl:value-of select="." />
</date>
</xsl:for-each>
</xsl:variable>
<xsl:for-each-group select="$datesWithDiff" group-starting-with="date[#diff = '' or xs:int(#diff) gt 7]">
<group>
<xsl:for-each select="current-group()">
<val>
<xsl:value-of select="format-date(.,'[D].[M].')"/>
</val>
</xsl:for-each>
</group>
</xsl:for-each-group>
</xsl:function>

Related

Using correct regex in xslt 2.0

I've been stuck using the correct regex. I need to tokenize each data per keys along with there values. In my example below,
Sample File:
<Rec>
<Data>/CHG1/EUR1000,00/EXCH/0,10/CPRP/Payment Code 1</Data>
<Data>/CHG1/EUR1000,00/EXCH/0,10/CPRP/Payment Code 1</Data>
<Data>/CHG3/EUR3000,00/PURP//CD/Payment Code 3</Data>
<Data>/CHG5/EUR5000,00/PURP//PRTRY/Payment Code 5</Data>
<Data>/ORIG//CSID/EUR7000,00/BENM//ID/Payment Code 7</Data>
</Rec>
The keys with '//' in the middle is considered as 1 key. I need to generate the output like this:
<Data>
<Group>
<Token>/CHG1/EUR1000,00</Token>
<Token>/EXCH/0,10</Token>
<Token>/CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token>/CHG3/EUR3000,00</Token>
<Token>/PURP//CD/Payment Code 3</Token>
</Group>
<Group>
<Token>/CHG5/EUR5000,00</Token>
<Token>/PURP//PRTRY/Payment Code 5</Token>
</Group>
<Group>
<Token>/ORIG//CSID/EUR7000,00</Token>
<Token>/BENM//ID/Payment Code 7</Token>
</Group>
</Data>
But, my generated output is like this:
<Data>
<Group>
<Token>/CHG1/</Token>
<Token>/EXCH/</Token>
<Token>/CPRP/</Token>
</Group>
<Group>
<Token>/CHG3/</Token>
<Token>/PURP//CD/</Token>
</Group>
<Group>
<Token>/CHG5/</Token>
<Token>/PURP//PRTRY/</Token>
</Group>
<Group>
<Token>/ORIG//CSID/</Token>
<Token>/BENM//ID/</Token>
</Group>
</Data>
Here is my XSLT that I've been using:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()[boolean(normalize-space())]|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Data">
<xsl:for-each select=".">
<Group>
<xsl:analyze-string select="." regex="\/[A-Z]+[0-9]?\/(\/(CD|PRTRY|MARF|ID|CSID|NAME|RID)\/)?">
<xsl:matching-substring>
<xsl:variable name="val" select="."/>
<Token>
<xsl:value-of select="$val"/>
</Token>
</xsl:matching-substring>
</xsl:analyze-string>
</Group>
</xsl:for-each>
</xsl:template>
There is something missing in my regex expression. Can anyone help me figure out? Thank you for your feedback.
I can't make heads or tails of your expected output. I suspect the correct output should be actually something like:
<?xml version="1.0" encoding="UTF-8"?>
<Rec>
<Group>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token>CHG3/EUR3000,00</Token>
<Token>PURP/</Token>
<Token>CD/Payment Code 3</Token>
</Group>
<Group>
<Token>CHG5/EUR5000,00</Token>
<Token>PURP/</Token>
<Token>PRTRY/Payment Code 5</Token>
</Group>
<Group>
<Token>ORIG/</Token>
<Token>CSID/EUR7000,00</Token>
<Token>BENM/</Token>
<Token>ID/Payment Code 7</Token>
</Group>
</Rec>
If so, I would suggest you change your approach and try:
XSLT 2.0
<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:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Data">
<xsl:variable name="t" select="tokenize(., '/')" />
<Group>
<xsl:for-each select="$t[position() mod 2 = 0]">
<xsl:variable name="i" select="index-of($t, .)"/>
<Token>
<xsl:value-of select="." />
<xsl:text>/</xsl:text>
<xsl:value-of select="$t[$i + 1]" />
</Token>
</xsl:for-each>
</Group>
</xsl:template>
</xsl:stylesheet>
Demo: http://xsltransform.net/93dEHGr
Or even simpler:
XSLT 2.0
<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:strip-space elements="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Data">
<Group>
<xsl:for-each-group select="tokenize(., '/')" group-by="position() idiv 2">
<Token>
<xsl:value-of select="current-group()" separator="/"/>
</Token>
</xsl:for-each-group>
</Group>
</xsl:template>
</xsl:stylesheet>
Demo: http://xsltransform.net/93dEHGr/2
Added:
Actually, the keys with 2 '//' in the middle is considered as 1 key.
Well, then let's take the // strings out of the game before doing the tokenizing, and restore them at the end:
<xsl:template match="Data">
<Group>
<xsl:for-each-group select="tokenize(replace(., '//', '§§§'), '/')" group-by="position() idiv 2">
<Token>
<xsl:value-of select="replace(string-join(current-group(), '/'), '§§§', '//')" />
</Token>
</xsl:for-each-group>
</Group>
</xsl:template>
Result
<?xml version="1.0" encoding="UTF-8"?>
<Rec>
<Group>
<Token/>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token/>
<Token>CHG1/EUR1000,00</Token>
<Token>EXCH/0,10</Token>
<Token>CPRP/Payment Code 1</Token>
</Group>
<Group>
<Token/>
<Token>CHG3/EUR3000,00</Token>
<Token>PURP//CD/Payment Code 3</Token>
</Group>
<Group>
<Token/>
<Token>CHG5/EUR5000,00</Token>
<Token>PURP//PRTRY/Payment Code 5</Token>
</Group>
<Group>
<Token/>
<Token>ORIG//CSID/EUR7000,00</Token>
<Token>BENM//ID/Payment Code 7</Token>
</Group>
</Rec>
http://xsltransform.net/93dEHGr/4
You can try this with tokenize in xslt 2.0
<xsl:template match="Rec">
<Data>
<xsl:apply-templates/>
</Data>
</xsl:template>
<xsl:template match="Data">
<Group>
<xsl:for-each select="tokenize(., '0/')">
<Token>
<xsl:if test="position() ne 1">
<xsl:text>/</xsl:text>
</xsl:if>
<xsl:value-of select="."/>
<xsl:if test="position() ne last()">
<xsl:text>0</xsl:text>
</xsl:if>
</Token>
</xsl:for-each>
</Group>
</xsl:template>

Merge and overwrite repeating xml nodes using XSLT 1.0

I have got a source XML
<Records>
<Data>
<RecordType>New</RecordType>
<Number>4734122946</Number>
<DateOfBirth>20160506</DateOfBirth>
<Title>Mr</Title>
<ChangeTimeStamp>20160101010001</ChangeTimeStamp>
<SerialChangeNumber>01</SerialChangeNumber>
</Data>
<Data>
<RecordType>New</RecordType>
<Number>4734122946</Number>
<LastName>Potter</LastName>
<DateOfBirth>20160506</DateOfBirth>
<ChangeTimeStamp>20160101010002</ChangeTimeStamp>
<SerialChangeNumber>01</SerialChangeNumber>
</Data>
</Records>
I want to use XSLT 1.0 to produce the below output
<Contact>
<Number>4734122946</Number>
<Title>Mr</Title>
<LastName>Potter</LastName>
<BirthDate>20160506</BirthDate>
<ChangeTimeStamp>20160101010002</ChangeTimeStamp>
</Contact>
The XSLT has to group and merge the child nodes of Data records into one based on the Number field. Also if there are same elements present, then it should use the ChangeTimeStamp element to figure out the latest change and use that element.
I tried the below XSLT. But I am nowhere close to the output.
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt" exclude-result-prefixes="msxsl">
<xsl:output method="xml" indent="yes" />
<xsl:key name="groups" match="Data" use="Number"/>
<xsl:key name="sortGroup" match="Data" use ="ChangeTimeStamp"/>
<xsl:template match="#* | node()">
<xsl:copy>
<xsl:apply-templates select="#* | node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="Records">
<xsl:for-each select="Data[generate-id() = generate-id(key('groups',Number))]">
<Contact>
<Number>
<xsl:value-of select="Number"/>
</Number>
<xsl:for-each select="key('groups',Number)">
<xsl:for-each select="key('sortGroup',ChangeTimeStamp)">
<xsl:sort select="sortGroup" order="ascending"/>
<xsl:if test="Title">
<Title>
<xsl:value-of select ="Title"/>
</Title>
</xsl:if>
<xsl:if test="LastName">
<LastName>
<xsl:value-of select="LastName"/>
</LastName>
</xsl:if>
<BirthDate>
<xsl:value-of select="DateOfBirth"/>
</BirthDate>
</xsl:for-each>
</xsl:for-each>
</Contact>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Appreciate your help.
The XSLT has to group and merge the child nodes of Data records into
one based on the Number field. Also if there are same elements
present, then it should use the ChangeTimeStamp element to figure out
the latest change and use that element.
For that, I believe you would want to do something like:
<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:key name="group" match="Data" use="Number"/>
<xsl:key name="item" match="Data/*" use="concat(../Number, '|', name())"/>
<xsl:template match="/Records">
<root>
<xsl:for-each select="Data[generate-id() = generate-id(key('group', Number)[1])]">
<Contact>
<xsl:for-each select="key('group', Number)/*[generate-id() = generate-id(key('item', concat(../Number, '|', name()))[1])]">
<xsl:for-each select="key('item', concat(../Number, '|', name()))">
<xsl:sort select="../ChangeTimeStamp" data-type="number" order="descending"/>
<xsl:if test="position()=1">
<xsl:copy-of select="."/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</Contact>
</xsl:for-each>
</root>
</xsl:template>
</xsl:stylesheet>
You will have to make some adjustments if you want to include only some data items and/or if you want them to appear in particular order. If you have a list of all possible data item names (e.g. Title, LastName, DateOfBirth, etc.) then this could be simpler.

Grouping of grouped data

Input:
<persons>
<person name="John" role="Writer"/>
<person name="John" role="Poet"/>
<person name="Jacob" role="Writer"/>
<person name="Jacob" role="Poet"/>
<person name="Joe" role="Poet"/>
</persons>
Expected Output:
<groups>
<group roles="Wriet, Poet" persons="John, Jacob"/>
<group roles="Poet" persons="Joe"/>
</groups>
As in the above example, I first need to group on person names and find everyone's roles. If more than one person is found to have the same set of roles (e.g. both John and Jacob are both Writer and Poet), then I need to group on each set of roles and list the person names.
I can do this for the first level of grouping using Muenchian method or EXSLT set:distinct etc.
<groups>
<group roles="Wriet, Poet" persons="John"/>
<group roles="Wriet, Poet" persons="Jacob"/>
<group roles="Poet" persons="Joe"/>
</groups>
The above was transformed using XSLT 1.0 and EXSLT:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:sets="http://exslt.org/sets" extension-element-prefixes="sets">
<xsl:output method="xml" indent="yes" omit-xml-declaration="yes"/>
<xsl:key name="persons-by-name" match="person" use="#name"/>
<xsl:template match="persons">
<groups>
<xsl:for-each select="sets:distinct(person/#name)">
<group>
<xsl:attribute name="persons"><xsl:value-of select="."/></xsl:attribute>
<xsl:attribute name="roles">
<xsl:for-each select="key('persons-by-name', .)">
<xsl:value-of select="#role"/>
<xsl:if test="position()!=last()"><xsl:text>, </xsl:text></xsl:if>
</xsl:for-each>
</xsl:attribute>
</group>
</xsl:for-each>
</groups>
</xsl:template>
</xsl:stylesheet>
However, I need help to understand how to group on the grouped roles.
If XSLT 1.0 solution is not available, please feel free to recommend XSLT 2.0 approach.
Try it this way?
XSLT 1.0
(using EXSLT node-set() and distinct() functions)
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
xmlns:set="http://exslt.org/sets"
extension-element-prefixes="exsl set">
<xsl:output method="xml" encoding="UTF-8" indent="yes" />
<xsl:key name="person-by-name" match="person" use="#name" />
<xsl:key name="person-by-roles" match="person" use="#roles" />
<xsl:variable name="distinct-persons">
<xsl:for-each select="set:distinct(/persons/person/#name)">
<person name="{.}">
<xsl:attribute name="roles">
<xsl:for-each select="key('person-by-name', .)/#role">
<xsl:sort/>
<xsl:value-of select="." />
<xsl:if test="position()!=last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:attribute>
</person>
</xsl:for-each>
</xsl:variable>
<xsl:template match="/">
<groups>
<xsl:for-each select="set:distinct(exsl:node-set($distinct-persons)/person/#roles)">
<group roles="{.}">
<xsl:attribute name="names">
<xsl:for-each select="key('person-by-roles', .)/#name">
<xsl:value-of select="." />
<xsl:if test="position()!=last()">
<xsl:text>, </xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:attribute>
</group>
</xsl:for-each>
</groups>
</xsl:template>
</xsl:stylesheet>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<groups>
<group roles="Poet, Writer" names="John, Jacob"/>
<group roles="Poet" names="Joe"/>
</groups>
I did exactly the same thing as you already did and then went a step further and grouped again. Now I get the following output with your input:
<?xml version="1.0" encoding="UTF-8"?>
<groups>
<group roles="Writer,Poet" persons="John,Jacob"/>
<group roles="Poet" persons="Joe"/>
</groups>
This is the XSLT 2.0
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns="" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:avintis="http://www.avintis.com/esb" exclude-result-prefixes="#all" version="2.0">
<xsl:output method="xml" encoding="UTF-8" indent="yes"/>
<xsl:template match="/persons">
<groups>
<xsl:variable name="persons" select="."/>
<!-- create a temporary variable containing all roles of a person -->
<xsl:variable name="roles">
<xsl:for-each select="distinct-values(person/#name)">
<xsl:sort select="."/>
<xsl:variable name="name" select="."/>
<xsl:element name="group">
<xsl:attribute name="roles">
<!-- sort the roles of each person -->
<xsl:variable name="rolesSorted">
<xsl:for-each select="$persons/person[#name=$name]">
<xsl:sort select="#role"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="string-join($rolesSorted/person/#role,',')"/>
</xsl:attribute>
<xsl:attribute name="persons" select="."/>
</xsl:element>
</xsl:for-each>
</xsl:variable>
<!-- now loop again over all roles of the persons and group persons having the same roles -->
<xsl:for-each select="distinct-values($roles/group/#roles)">
<xsl:element name="group">
<xsl:variable name="name" select="."/>
<xsl:attribute name="roles" select="$name"/>
<xsl:attribute name="persons">
<xsl:value-of select="string-join($roles/group[#roles=$name]/#persons,',')"/>
</xsl:attribute>
</xsl:element>
</xsl:for-each>
</groups>
</xsl:template>
<xsl:template match="*|text()|#*">
<xsl:copy>
<xsl:apply-templates select="*|text()|#*"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
The roles get also sorted - so independent from the input order of the roles and persons.

xslt aggregation sum

I want do some sum of the values and return it as a row or column with the data.
taking the below xml as example:
<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<root>
<default0>
<Group>
<groupEntry>
<Day>Mon</Day>
<ID>111</ID>
<Number>-3</Number>
</groupEntry>
</Group>
<Group>
<groupEntry>
<Day>Tue</Day>
<ID>222</ID>
<Number>4</Number>
</groupEntry>
</Group>
<Group>
<groupEntry>
<Day>Tue</Day>
<ID>444</ID>
<Number>5</Number>
</groupEntry>
<Breakdown>
<Details>
<Day>Tue</Day>
<ID>444</ID>
<Number>-3</Number>
</Details>
<Details>
<Day>Tue</Day>
<ID>444</ID>
<Number>8</Number>
</Details>
</Breakdown>
</Group>
<Group>
<groupEntry>
<Day>Fri</Day>
<ID>333</ID>
<Number>-3</Number>
</groupEntry>
</Group>
</default0>
</root>
My below xslt :
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:text>ID,Day,Number</xsl:text>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="groupEntry|Details">
<xsl:text>
</xsl:text>
<xsl:value-of select="ID"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Day"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Number"/>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
returns this result:
ID,Day,Number
111,Mon,-3
222,Tue,4
444,Tue,5
444,Tue,-3
444,Tue,8
333,Fri,-3
However I want to get the total number by Day and report it in the result as either of the below 2 options
create 1 summary row like:
ID,Day,Number
Mon,Mon,-3
111,Mon,-3
Tue,Tue,9
222,Tue,4
444,Tue,5
444,Tue,-3
444,Tue,8
Fri,Fri,-3
333,Fri,-3
create an extra column:
ID,Day,Number,TotalNumber
111,Mon,-3,-3
222,Tue,4,9
444,Tue,5,9
444,Tue,-3,9
444,Tue,8,9
333,Fri,-3,-3
Does anyone know if this is possible?
In either option, you would probably need to define a key to group the elements by Day
<xsl:key name="days" match="groupEntry|Details" use="Day"/>
Then you can just add your extra column like so
<xsl:value-of select="sum(key('days', Day)/Number)"/>
Here is the full XSLT for the first option
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="days" match="groupEntry|Details" use="Day"/>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:text>ID,Day,Number,TotalNumber</xsl:text>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="groupEntry|Details">
<xsl:text>
</xsl:text>
<xsl:value-of select="ID"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Day"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Number"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="sum(key('days', Day)/Number)"/>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
This should output the following results
ID,Day,Number,TotalNumber
111,Mon,-3,-3
222,Tue,4,14
444,Tue,5,14
444,Tue,-3,14
444,Tue,8,14
333,Fri,-3,-3
In the second option, you would want to add a total line for the first occurrence of a particular Day. You can do this by checking if the current element is the first element in the key for that day
<xsl:if test="generate-id() = generate-id(key('days', Day)[1])">
Here is the XSLT for the second case
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:key name="days" match="groupEntry|Details" use="Day"/>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:text>ID,Day,Number</xsl:text>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="groupEntry|Details">
<xsl:if test="generate-id() = generate-id(key('days', Day)[1])">
<xsl:text>
</xsl:text>
<xsl:value-of select="Day"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Day"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="sum(key('days', Day)/Number)"/>
</xsl:if>
<xsl:text>
</xsl:text>
<xsl:value-of select="ID"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Day"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Number"/>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
This should output the following results
ID,Day,Number
Mon,Mon,-3
111,Mon,-3
Tue,Tue,14
222,Tue,4
444,Tue,5
444,Tue,-3
444,Tue,8
Fri,Fri,-3
333,Fri,-3

How do I combine or merge grouped nodes?

Using the XSL:
<?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:output method="xml"/>
<xsl:template match="/">
<records>
<record>
<!-- Group record by bigID, for further processing -->
<xsl:for-each-group select="records/record" group-by="bigID">
<xsl:sort select="bigID"/>
<xsl:for-each select="current-group()">
<!-- Create new combined record -->
<bigID>
<!-- <xsl:value-of select="."/> -->
<xsl:for-each select=".">
<xsl:value-of select="bigID"/>
</xsl:for-each>
</bigID>
<text>
<xsl:value-of select="text"/>
</text>
</xsl:for-each>
</xsl:for-each-group>
</record>
</records>
</xsl:template>
</xsl:stylesheet>
I'm trying to change:
<?xml version="1.0" encoding="UTF-8"?>
<records>
<record>
<bigID>123</bigID>
<text>Contains text for 123</text>
<bigID>456</bigID>
<text>Some 456 text</text>
<bigID>123</bigID>
<text>More 123 text</text>
<bigID>123</bigID>
<text>Yet more 123 text</text>
</record>
</records>
into:
<?xml version="1.0" encoding="UTF-8"?>
<records>
<record>
<bigID>123
<text>Contains text for 123</text>
<text>More 123 text</text>
<text>Yet more 123 text</text>
</bigID>
<bigID>456
<text>Some 456 text</text>
</bigID>
</record>
</records>
Right now, I'm just listing the grouped <bigID>s, individually. I'm missing the step after grouping, where I combine the grouped <bigID> nodes. My suspicion is that I need to use the "key" function somehow, but I'm not sure.
Thanks for any help.
Here is the wanted XSLT 2.0 transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:key name="kTextforId" match="text"
use="preceding-sibling::bigID[1]"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="record">
<record>
<xsl:for-each-group select="bigID" group-by=".">
<bigID>
<xsl:sequence select="current-grouping-key()"/>
<xsl:copy-of select=
"key('kTextforId', current-grouping-key())"/>
</bigID>
</xsl:for-each-group>
</record>
</xsl:template>
</xsl:stylesheet>
When performed on the provided XML document, the wanted result is produced.
In XSLT 2.0 you don't need keys for grouping.
Since you are just copying the text elements in the group, the inner for-each can be removed.
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes" />
<xsl:template match="/">
<records>
<record>
<xsl:for-each-group select="records/record/bigID" group-by=".">
<xsl:sort select="." data-type="number" />
<bigID>
<xsl:value-of select="." />
<xsl:copy-of select="current-group()/following-sibling::text[1]" />
</bigID>
</xsl:for-each-group>
</record>
</records>
</xsl:template>
</xsl:stylesheet>
If you instead wanted to output the bigID elements followed by their text elements, then my loop would be replaced by the following.
<xsl:for-each-group select="records/record/bigID" group-by=".">
<xsl:sort select="." data-type="number" />
<xsl:copy-of select="." />
<xsl:copy-of select="current-group()/following-sibling::text[1]" />
</xsl:for-each-group>