I've the following xml & want to regroup the elements as per specific date ranges.
Input xml:
`
<cust_CD>
<user>
<userId>50298867</userId>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-05-31T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>20</cust_percentage>
<cust_costCenter>N3020050</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-02-28T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>40</cust_percentage>
<cust_costCenter>NT7000003</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-01-31T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>40</cust_percentage>
<cust_costCenter>N2030020</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
</user>
</cust_CD>
Desired Output:
<group>
<date>
<effectiveDate>2023-01-01T00:00:00.000</effectiveDate>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-05-31T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>20</cust_percentage>
<cust_costCenter>N3020050</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-02-28T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>40</cust_percentage>
<cust_costCenter>NT7000003</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-01-31T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>40</cust_percentage>
<cust_costCenter>N2030020</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
</date>
<date>
<effectiveDate>2023-02-01T00:00:00.000</effectiveDate>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-05-31T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>20</cust_percentage>
<cust_costCenter>N3020050</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-02-28T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>40</cust_percentage>
<cust_costCenter>NT7000003</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
</date>
<date>
<effectiveDate>2023-03-01T00:00:00.000</effectiveDate>
<cust_MDFCostDistribution_child>
<cust_EndDate>2023-05-31T00:00:00.000</cust_EndDate>
<cust_startdate>2023-01-01T00:00:00.000</cust_startdate>
<cust_percentage>20</cust_percentage>
<cust_costCenter>N3020050</cust_costCenter>
<cust_MDFCostDistribution_Parent_usersSysId>50298867</cust_MDFCostDistribution_Parent_usersSysId>
</cust_MDFCostDistribution_child>
</date>
<date>
<effectiveDate>2023-06-01T00:00:00.000</effectiveDate>
</date>
</group>
The logic behind is: Each child element has a start/end date range (cust_startdate/cust_EndDate). Only the elements falls between the date-range will be grouped together. When, for any specific element(s) end-date is over, the rest elements excluding them, need to be re-grouped from next day onwards. So that, effective dates can be maintained.
'**'
In output xml, the 'effectiveDate' comes from cust_EndDate + 1Day ' + xs:dayTimeDuration('P1D')'. This is because, we need to determine which elements are effective on particular dates. For Ex: all 3 are valid from 2023-01-01(1st effective date/Initial one) till 2023-01-31. 2 valid between 2023-02-01(2nd effective date) to 2023-02-28. Only 1 valid between 2023-03-01(3rd effective date) to 2023-05-31. And none after 2023-06-01 onwards. Hope I clarifies.
'**'
Will welcome any help/suggestion regarding the matter.
Thank You
Not really able to find an way. Tried with group-starting-with & group-ending-with functions, but that didn't resolved the issue.
If I follow the required logic correctly (which is not at all certain), you want to do something like:
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="/cust_CD">
<group>
<xsl:call-template name="find-overlaps">
<xsl:with-param name="x-date" select="min(user/cust_MDFCostDistribution_child/xs:dateTime(cust_startdate))"/>
</xsl:call-template>
</group>
</xsl:template>
<xsl:template name="find-overlaps">
<xsl:param name="x-date"/>
<xsl:variable name="overlaps" select="user/cust_MDFCostDistribution_child[xs:dateTime(cust_startdate) le $x-date and $x-date le xs:dateTime(cust_EndDate)]" />
<date>
<effectiveDate>
<xsl:value-of select="$x-date"/>
</effectiveDate>
<xsl:copy-of select="$overlaps"/>
</date>
<xsl:if test="$overlaps">
<xsl:call-template name="find-overlaps">
<xsl:with-param name="x-date" select="min($overlaps/xs:dateTime(cust_EndDate)) + xs:dayTimeDuration('P1D')"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Note that this assumes that every cust_MDFCostDistribution_child node overlaps another to some extent, or is at least adjacent to another. If there can be two or more completely separate, discontiguous ranges, then this needs more work.
Related
I am looking to shorten my XSLT codebase by seeing if XSLT can increase a text number for each match. The text number exists in both the attribute value "label-period0" and the "xls:value-of" value.
The code works, no errors so this is more a question of how to shorten the code and make use of some sort of iteration on a specific character in a string.
I added 2 similar code structures for "period0" and "period1" to better see what exactly are the needed changes in terms of the digit in the text strings.
Source XML file:
<data>
<periods>
<period0><from>2016-01-01</from><to>2016-12-01</to></period0>
<period1><from>2015-01-01</from><to>2015-12-01</to></period1>
<period2><from>2014-01-01</from><to>2014-12-01</to></period2>
<period3><from>2013-01-01</from><to>2013-12-01</to></period3>
</periods>
<balances>
<balance0><instant>2016-12-31</instant></balance0>
<balance1><instant>2015-12-31</instant></balance1>
<balance2><instant>2014-12-31</instant></balance2>
<balance3><instant>2013-12-31</instant></balance3>
</balances>
</data>
XSL file:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:transform
version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="xml" indent="yes"/>
<!-- Block all data that has no user defined template -->
<xsl:mode on-no-match="shallow-skip"/>
<xsl:template match="data">
<results>
<periods>
<periods label="period0">
<xsl:value-of
select =
"concat(periods/period0/from, '--', periods/period0/to)"
/>
</periods>
<periods label="period1">
<xsl:value-of
select =
"concat(periods/period1/from, '--', periods/period1/to)"
/>
</periods>
<!-- Etc for period [2 and 3]-->
</periods>
<balances>
<balance label="balance0">
<xsl:value-of select ="balances/balance0/instant"/>
</balance>
<!-- Etc for balance [1,2 and 3] -->
</balances>
</results>
</xsl:template>
</xsl:transform>
Result:
<?xml version="1.0" encoding="UTF-8"?>
<results>
<periods>
<periods label="period0">2016-01-01--2016-12-01</periods>
<periods label="period1">2015-01-01--2015-12-01</periods>
</periods>
<balances>
<balance label="balance0">2016-12-31</balance>
</balances>
</results>
Wanted result:
(with an XSL that steps the digit in the text string, or any other logics in XSL that could cater for manipulating the digit in text string)
<?xml version="1.0" encoding="UTF-8"?>
<results>
<periods>
<periods label="period0">2016-01-01--2016-12-01</periods>
<periods label="period1">2015-01-01--2015-12-01</periods>
<periods label="period2">2014-01-01--2015-12-01</periods>
<periods label="period3">2013-01-01--2015-12-01</periods>
</periods>
<balances>
<balance label="balance0">2016-12-31</balance>
<balance label="balance1">2015-12-31</balance>
<balance label="balance2">2014-12-31</balance>
<balance label="balance3">2013-12-31</balance>
</balances>
</results>
Couldn't you do simply something like:
<xsl:template match="/data">
<results>
<periods>
<xsl:for-each select="periods/*">
<periods label="{name()}">
<xsl:value-of select="from"/>
<xsl:text>--</xsl:text>
<xsl:value-of select="to"/>
</periods>
</xsl:for-each>
</periods>
<balances>
<xsl:for-each select="balances/*">
<balance label="{name()}">
<xsl:value-of select="instant"/>
</balance>
</xsl:for-each>
</balances>
</results>
</xsl:template>
If you want to do your own numbering, you can change:
<periods label="{name()}">
to:
<periods label="period{position() - 1}">
I have two large xml files, one of which has the following format:
<Persons>
<Person>
<ID>1</ID>
<LAST_NAME>London</LAST_NAME>
</Person>
<Person>
<ID>2</ID>
<LAST_NAME>Twain</LAST_NAME>
</Person>
<Person>
<ID>3</ID>
<LAST_NAME>Dikkens</LAST_NAME>
</Person>
</Persons>
The second file has the following format:
<SalesPersons>
<SalesPerson>
<ID>2</ID>
<LAST_NAME>London</LAST_NAME>
</SalesPerson>
<SalesPerson>
<ID>3</ID>
<LAST_NAME>Dikkens</LAST_NAME>
</SalesPerson>
</SalesPersons>
I need to find those records from file 1, which does not exist in file 2. Although I have it done using for-each loop, such an approach is taking a substantial amount of time. Is it possible to somehow make it run faster using a different approach?
Using a key can help to improve performance on lookups:
<xsl:key name="sales-person" match="SalesPerson" use="concat(ID, '|', LAST_NAME)"/>
<xsl:template match="/">
<xsl:for-each select="Persons/Person">
<xsl:variable name="person" select="."/>
<!-- need to change context document for key function use -->
<xsl:for-each select="$doc2">
<xsl:if test="not(key('sales-person', concat($person/ID, '|', $person/LAST_NAME)))">
<xsl:copy-of select="$person"/>
</xsl:if>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
That assumes you have bound doc2 as a variable or parameter with e.g. <xsl:param name="doc2" select="document('sales-persons.xml')"/>.
I have to determine the input value having date format of dd-mmm-yyyy. If I can find will set some attribute based on the attribute I can do the format in C# report processing class.
<td>
<xsl:if test="To write expression to match the value">
<r>
<xyz:value-of select="'Set Value'" />
</r>
</xsl:if>
</td>
Input value is "30-Jun-2019". If it matches I want to set .
Basically I have set of columns in the report. I have to identify the the values in the report if the value matches with the Date format of dd-mmm-yyy setting some attribute in the xslt and applying the same format in report parser code which is written in c#
As I said in comments, there is no regex support in XSLT 1.0, so this can get quite tedious.
Consider the following example:
XML
<input>
<item>21-Jan-1987</item>
<item>921-Jan-1987</item>
<item>15-Jul-2009</item>
<item>15-Jux-2009</item>
<item>03-Dec-2014</item>
<item>03-Dec-999</item>
</input>
XSLT 1.0
<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:strip-space elements="*"/>
<xsl:template match="/input">
<output>
<xsl:for-each select="item">
<item value="{.}">
<xsl:variable name="dd" select="substring-before(., '-')" />
<xsl:variable name="mmm" select="substring-before(substring-after(., '-'), '-')" />
<xsl:variable name="yyyy" select="substring-after(substring-after(., '-'), '-')" />
<xsl:if test="translate($dd, '123456789', '000000000') = '00' and translate($yyyy, '123456789', '000000000') = '0000' and ($mmm='Jan' or $mmm='Feb' or $mmm='Mar' or $mmm='Apr' or $mmm='May' or $mmm='Jun' or $mmm='Jul' or $mmm='Aug' or $mmm='Sep' or $mmm='Oct' or $mmm='Nov' or $mmm='Dec')">
<xsl:text>Is Date</xsl:text>
</xsl:if>
</item>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<item value="21-Jan-1987">Is Date</item>
<item value="921-Jan-1987"/>
<item value="15-Jul-2009">Is Date</item>
<item value="15-Jux-2009"/>
<item value="03-Dec-2014">Is Date</item>
<item value="03-Dec-999"/>
</output>
Note that this checks only that the input conforms to the pattern, not that the date itself is valid. Also keep in mind that XML is case-sensititve.
Added:
If you prefer, you could simplify the test to:
<xsl:if test="translate(translate(translate(., '123456789', '000000000'), 'JFMASOND', '########'), 'anebpryulgctov', '%%%%%%%%%%%%%%') = '00-#%%-0000'">
but then a value like 15-Jpt-2009 will pass as date.
In XSLT 2.0 this is fairly trivial: matches(., '[0-9]{2}-[A-Z][a-z]{2}-[0-9]
{4}')
In 1.0 it's considerably harder, and it depends a little bit how precise you want to be. But you could get close with translate(translate($input, 'ABC...abc...', 'AAAAAAAA....'), '0123456789', '9999999999') = '99-AAA-9999') where the '...' means you have to write out the rest of the alphabet.
If I run the following XSLT code:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kValueByVal" match="variable_name/#value"
use="."/>
<xsl:template match="assessment">
<xsl:for-each select="
/*/*/variable/attributes/variable_name/#value
[generate-id()
=
generate-id(key('kValueByVal', .)[1])
]
">
<xsl:value-of select=
"concat(., ' ', count(key('kValueByVal', .)), '
')"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
on the following XML:
<assessment>
<variables>
<variable>
<attributes>
<variable_name value="FRED"/>
</attributes>
</variable>
</variables>
<variables>
<variable>
<attributes>
<variable_name value="MORTIMER"/>
</attributes>
</variable>
</variables>
<variables>
<variable>
<attributes>
<variable_name value="FRED"/>
</attributes>
</variable>
</variables>
</assessment>
I get the desired output:
FRED 2
MORTIMER 1
(See my original question for more info, if you wish.)
However, if I run it on this input:
<ExamStore>
<assessment>
<variables>
<variable>
<attributes>
<variable_name value="FRED"/>
</attributes>
</variable>
</variables>
<variables>
<variable>
<attributes>
<variable_name value="MORTIMER"/>
</attributes>
</variable>
</variables>
<variables>
<variable>
<attributes>
<variable_name value="FRED"/>
</attributes>
</variable>
</variables>
</assessment>
</ExamStore>
I get nothing. (Note that I just wrapped the original input in an ExamStore tag.) I was expecting and hoping to get the same output.
Why don't I? How can I change the original XSLT code to get the same output?
Well your select xpath /*/*/variable/attributes/variable_name/... is no longer correct because you added another node higher in the node-tree.
If you want to have true independence you need to use something like:
//variable/attributes/variable_name/...
...(not the double slash at the start) but this is fairly dangerous because it will catch all occurences of that structure - be really sure that's what you mean.
Otherwise, just prepend your xpath with another /*
When you introduced yet another level in the XML document, this screwed up the absolute XPath expression used in the original solution (taylored exactly after your original XML file).
Therefore, in order to make the XPath expression work in the new situation, just do the following:
Replace:
/*/*/variable/attributes/variable_name/#value
with
/*/*/*/variable/attributes/variable_name/#value
and now you again get the wanted neat result:
FRED 2
MORTIMER 1
I would never give you an "independent" solution, because you haven't provided any properties/guarantees/constraints about the set of possible XML documents on which you want to apply the transformation.
In your original question you used:
.//variables/variable/attributes/variable_name
off assessment,
and this is why I used the absolute XPath expression in my solution. There was no guarantee that in another XML document some variable_name elements wouldn't exist such that their chain of ancestors is not variables/variable/attributes, If this were the case, this would mean that you probably were not interested in the values of such "irregular" variable_name elements.
The lesson is that one should not be too specific in defining a question and then want general solutions. :)
For real structure independence you should use:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kValueByVal" match="variable_name/#value"
use="."/>
<xsl:template match="variable_name[#value
[generate-id()
=
generate-id(key('kValueByVal', .)[1])
]]">
<xsl:value-of select=
"concat(#value, ' ', count(key('kValueByVal', #value)), '
')"/>
</xsl:template>
</xsl:stylesheet>
Result with first input:
FRED 2
MORTIMER 1
Result with second input:
FRED 2
MORTIMER 1
Note: Never use // as fisrt XPath operator.
Given the following xml:
<container>
<val>2</val>
<id>1</id>
</container>
<container>
<val>2</val>
<id>2</id>
</container>
<container>
<val>2</val>
<id>3</id>
</container>
<container>
<val>4</val>
<id>1</id>
</container>
<container>
<val>4</val>
<id>2</id>
</container>
<container>
<val>4</val>
<id>3</id>
</container>
I'd like to return something like
2 - 1
2 - 3
4 - 1
4 - 3
Using a nodeset I've been able to get the last occurrence via:
exsl:node-set($list)/container[not(val = following::val)]
but I can't figure out how to get the first one.
To get the first and the last occurrence (document order) in each "<val>" group, you can use an <xsl:key> like this:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:key name="ContainerGroupByVal" match="container" use="val" />
<xsl:variable name="ContainerGroupFirstLast" select="//container[
generate-id() = generate-id(key('ContainerGroupByVal', val)[1])
or
generate-id() = generate-id(key('ContainerGroupByVal', val)[last()])
]" />
<xsl:template match="/">
<xsl:for-each select="$ContainerGroupFirstLast">
<xsl:value-of select="val" />
<xsl:text> - </xsl:text>
<xsl:value-of select="id" />
<xsl:value-of select="'
'" /><!-- LF -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
EDIT #1: A bit of an explanation since this might not be obvious right away:
The <xsl:key> returns all <container> nodes having a given <val>. You use the key() function to query it.
The <xsl:variable> is where it all happens. It reads as:
for each of the <container> nodes in the document ("//container") check…
…if it has the same unique id (generate-id()) as the first node returned by key() or the last node returned by key()
where key('ContainerGroupByVal', val) returns the set of <container> nodes matching the current <val>
if the unique ids match, include the node in the selection
the <xsl:for-each> does the output. It could just as well be a <xsl:apply-templates>.
EDIT #2: As Dimitre Novatchev rightfully points out in the comments, you should be wary of using the "//" XPath shorthand. If you can avoid it, by all means, do so — partly because it potentially selects nodes you don't want, and mainly because it is slower than a more specific XPath expression. For example, if your document looks like:
<containers>
<container><!-- ... --></container>
<container><!-- ... --></container>
<container><!-- ... --></container>
</containers>
then you should use "/containers/container" or "/*/container" instead of "//container".
EDIT #3: An alternative syntax of the above would be:
<xsl:stylesheet
version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
>
<xsl:output method="text" />
<xsl:key name="ContainerGroupByVal" match="container" use="val" />
<xsl:variable name="ContainerGroupFirstLast" select="//container[
count(
.
| key('ContainerGroupByVal', val)[1]
| key('ContainerGroupByVal', val)[last()]
) = 2
]" />
<xsl:template match="/">
<xsl:for-each select="$ContainerGroupFirstLast">
<xsl:value-of select="val" />
<xsl:text> - </xsl:text>
<xsl:value-of select="id" />
<xsl:value-of select="'
'" /><!-- LF -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Explanation: The XPath union operator "|" combines it's arguments into a node-set. By definition, a node-set cannot contain duplicate nodes — for example: ". | . | ." will create a node-set containing exactly one node (the current node).
This means, if we create a union node-set from the current node ("."), the "key(…)[1]" node and the "key(…)[last()]" node, it's node count will be 2 if (and only if) the current node equals one of the two other nodes, in all other cases the count will be 3.
Basic XPath:
//container[position() = 1] <- this is the first one
//container[position() = last()] <- this is the last one
Here's a set of XPath functions in more detail.
I. XSLT 1.0
Basically the same solution as the one by Tomalak, but more understandable Also it is complete, so you only need to copy and paste the XML document and the transformation and then just press the "Transform" button of your favourite XSLT IDE:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:key name="kContByVal" match="container"
use="val"/>
<xsl:template match="/*">
<xsl:for-each select=
"container[generate-id()
=
generate-id(key('kContByVal',val)[1])
]
">
<xsl:variable name="vthisvalGroup"
select="key('kContByVal', val)"/>
<xsl:value-of select=
"concat($vthisvalGroup[1]/val,
'-',
$vthisvalGroup[1]/id,
'
'
)
"/>
<xsl:value-of select=
"concat($vthisvalGroup[last()]/val,
'-',
$vthisvalGroup[last()]/id,
'
'
)
"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the originally-provided XML document (edited to be well-formed):
<t>
<container>
<val>2</val>
<id>1</id>
</container>
<container>
<val>2</val>
<id>2</id>
</container>
<container>
<val>2</val>
<id>3</id>
</container>
<container>
<val>4</val>
<id>1</id>
</container>
<container>
<val>4</val>
<id>2</id>
</container>
<container>
<val>4</val>
<id>3</id>
</container>
</t>
the wanted result is produced:
2-1
2-3
4-1
4-3
Do note:
We use the Muenchian method for grouping to find one container element for each set of such elements that have the same value for val.
From the whole node-list of container elements with the same val value, we output the required data for the first container element in the group and for the last container element in the group.
II. XSLT 2.0
This transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:for-each-group select="container"
group-by="val">
<xsl:for-each select="current-group()[1], current-group()[last()]">
<xsl:value-of select=
"concat(val, '-', id, '
')"/>
</xsl:for-each>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
when applied on the same XML document as above, prodices the wanted result:
2-1
2-3
4-1
4-3
Do note:
The use of the <xsl:for-each-group> XSLT instruction.
The use of the current-group() function.