I am looking for some help building an XSLT stylesheet. I have provided the input XML. I need it transformed to the output XML shown.
I am thinking ..
start from the root node
traverse the tree to check if attribute or element.
If element , then string concat and format in
<businessElements>
<key>requestId</key>
<type>String</type>
<value>TV00001001</value>
</businessElements>
Input XML
<entity1>
<requestID>TV00001001</requestID> //nested entities
<entity2>
<effectiveDt>2001-12-31T12:00:00</effectiveDt> // attribute with value
<companyCd>companyCd</companyCd>
< entity3>
<vo1>
<att1>true</ att1>
< att2>vehicleId</att2>
<att3>true</att3>
</vo1>
</ entity3>
</ entity2>
</ entity1>
Output XML
< entity1>
<businessElements>
<key>requestId</key>
<type>String</type>
<value>TV00001001</value>
</businessElements>
< entity2>
<businessElements>
<key>effectiveDt</key>
<type>Date</type>
<value>12/11/2016</value>
</businessElements>
<businessElements>
<key>companyCd</key>
<type>String</type>
<value>0001</value>
</businessElements>
< entity3>
< vo1>
<businessElements>
<key>vehicleId</key>
<type>String</type>
<value>5</value>
</businessElements>
</ vo1>
</ entity3>
</ entity2>
</ entity1>
I see no discernible logic in your output - especially as it contains values that are not in the input.
Try this as your starting point:
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="*"/>
<!-- identity transform -->
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[not(*)]">
<businessElements>
<key>
<xsl:value-of select="name()" />
</key>
<type>???</type>
<value>
<xsl:value-of select="." />
</value>
</businessElements>
</xsl:template>
</xsl:stylesheet>
This takes all the leaf nodes (elements that do not have other elements as their children) and transform them to a businessElements node, where key is the name of the original element and value is its string-value.
I am not sure where the type value should come from.
Applied to the following well-formed (!) input example:
XML
<entity1>
<requestID>TV00001001</requestID>
<entity2>
<effectiveDt>2001-12-31T12:00:00</effectiveDt>
<companyCd>companyCd</companyCd>
<entity3>
<vo1>
<att1>true</att1>
<att2>vehicleId</att2>
<att3>true</att3>
</vo1>
</entity3>
</entity2>
</entity1>
the result will be:
<?xml version="1.0" encoding="UTF-8"?>
<entity1>
<businessElements>
<key>requestID</key>
<type>???</type>
<value>TV00001001</value>
</businessElements>
<entity2>
<businessElements>
<key>effectiveDt</key>
<type>???</type>
<value>2001-12-31T12:00:00</value>
</businessElements>
<businessElements>
<key>companyCd</key>
<type>???</type>
<value>companyCd</value>
</businessElements>
<entity3>
<vo1>
<businessElements>
<key>att1</key>
<type>???</type>
<value>true</value>
</businessElements>
<businessElements>
<key>att2</key>
<type>???</type>
<value>vehicleId</value>
</businessElements>
<businessElements>
<key>att3</key>
<type>???</type>
<value>true</value>
</businessElements>
</vo1>
</entity3>
</entity2>
</entity1>
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}">
Newbie to this site and using xslt but running into a roadblock transforming a SSRS 2008v2 rendered xml file into another XSL raw format for a 3rd Party EDI transfer. I've been searching this site and others for a while now, but struggling putting it all together.I'm starting with the following raw xml data;
<Invoices xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.spscommerce.com/RSX" xsi:schemaLocation="http://www.spscommerce.com/RSX http://tfl- sql01/ReportServer_SQL2012? %2FTesting%2FINTest&rs%3ACommand=Render&rs%3AFormat=XML&rs%3ASessionID= jn5ugdirg4m02nmodnm0hynq&rc%3ASchema=True" Name="INTest">
<Invoices1> ***need to remove***
<ivhID_Collection> ***need to remove***
<Invoices>...</Invoices>
<Invoices>...</Invoices>
<Invoices>
<Invoice>
<Header1>
<InvoiceHeader>...</InvoiceHeader>
<PaymentTerms>...</PaymentTerms>
<Dates>...</Dates>
<Address>...</Address>
<References>...</References>
<ChargesAllowances>...</ChargesAllowances>
<LineItem_Collection> ***need to remove and replace with </Header>***
<LineItem>
<InvoiceLine>...</InvoiceLine>
<ProductOrItemDescription>...</ProductOrItemDescription>
</LineItem>
<LineItem>
<InvoiceLine>...</InvoiceLine>
<ProductOrItemDescription>...</ProductOrItemDescription>
</LineItem>
</LineItem_Collection> ***need to remove***
<Summary>...</Summary>
</Header1> ***need to remove***
</Invoice>
</Invoices>
<Invoices>...</Invoices>
<Invoices>...</Invoices>
<Invoices>...</Invoices>
/ivhID_Collection> ***need to remove***
</Invoices1> ***need to remove***
</Invoices>
Trying to get it in this structure instead;
<Invoices xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.spscommerce.com/RSX" xsi:schemaLocation="http://www.spscommerce.com/RSX http://tfl-sql01/ReportServer_SQL2012?%2FTesting%2FINDoItBest%20v7&rs%3ACommand=Render&rs%3AFormat=XML&rs%3ASessionID=jn5ugdirg4m02nmodnm0hynq&rc%3ASchema=True" Name="INDoItBest v7">
<Invoices>...</Invoices>
<Invoices>...</Invoices>
<Invoices>
<Invoice>
<Header>
<InvoiceHeader>...</InvoiceHeader>
<PaymentTerms>...</PaymentTerms>
<Dates>...</Dates>
<Address>...</Address>
<References>...</References>
<ChargesAllowances>...</ChargesAllowances>
</Header>
<LineItem>
<InvoiceLine>...</InvoiceLine>
<ProductOrItemDescription>...</ProductOrItemDescription>
</LineItem>
<LineItem>
<InvoiceLine>...</InvoiceLine>
<ProductOrItemDescription>...</ProductOrItemDescription>
</LineItem>
<Summary>...</Summary>
</Invoice>
</Invoices>
<Invoices>...</Invoices>
<Invoices>...</Invoices>
<Invoices>...</Invoices>
</Invoices>
I made some progress using this style sheet, but am stuck on the regrouping of the Header tag and the display of the element namespace.
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:t="http://www.spscommerce.com/RSX"
exclude-result-prefixes="t">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!--rule to suppress the undesired nodes-->
<xsl:template match="t:Invoices1|t:ivhID_Collection">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="t:LineItem_Collection">
<xsl:apply-templates/>
</xsl:template>
<!--<xsl:template match="t:Invoice/t:Header1">
<xsl:apply-templates/>
</xsl:template>-->
<!-- Identity Transform -->
<xsl:template match="t:Header1">
<xsl:copy>
<xsl:element name="Header">
<xsl:apply-templates select="#*|t:InvoiceHeader|t:PaymentTerms|t:Dates|t:Address|t:References|t:ChargesAllowances"/>
</xsl:element>
<xsl:apply-templates select="#*|t:LineItem_Collection|t:Summary"/>
</xsl:copy>
</xsl:template>
<!-- Had to comment out -->
<!--<xsl:template match="t:Invoice/t:Header1">
<xsl:apply-templates/>
</xsl:template>-->
The stylesheet produced most of what I needed, but failed when I tried to remove the Header1 tag (code commented out). Also, struggling to understand why "exclude-result-prefixes" isn't working to remove the namespace from the new xml file.
<Invoices xmlns="http://www.spscommerce.com/RSX" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.spscommerce.com/RSX http://tfl-sql01/ReportServer_SQL2012?%2FTesting%2FINDoItBest%20v7&rs%3ACommand=Render&rs%3AFormat=XML&rs%3ASessionID=jn5ugdirg4m02nmodnm0hynq&rc%3ASchema=True" Name="INDoItBest v7">
<Invoices>...</Invoices>
<Invoices>...</Invoices>
<Invoices>
<Invoice>
<Header1>
<Header xmlns="">
<InvoiceHeader xmlns="http://www.spscommerce.com/RSX">... </InvoiceHeader>
<PaymentTerms xmlns="http://www.spscommerce.com/RSX">... </PaymentTerms>
<Dates xmlns="http://www.spscommerce.com/RSX">...</Dates>
<Address xmlns="http://www.spscommerce.com/RSX">...</Address>
<References xmlns="http://www.spscommerce.com/RSX">...</References>
<ChargesAllowances xmlns="http://www.spscommerce.com/RSX">... </ChargesAllowances>
</Header>
<LineItem>
<InvoiceLine>...</InvoiceLine>
<ProductOrItemDescription>...</ProductOrItemDescription>
</LineItem>
<LineItem>
<InvoiceLine>...</InvoiceLine>
<ProductOrItemDescription>...</ProductOrItemDescription>
</LineItem>
<Summary>
<TotalAmount>756.8400</TotalAmount>
<TotalSalesAmount>727.1600</TotalSalesAmount>
<TotalLineItemNumber>2</TotalLineItemNumber>
</Summary>
</Header1>
</Invoice>
</Invoices>
<Invoices>...</Invoices>
<Invoices>...</Invoices>
<Invoices>...</Invoices>
</Invoices>
Any advice or other options would be greatly appreciated!
You've already got a template matching t:Header1 in your XSLT, so you shouldn't add another one matching it, as only one can apply. (In your case, if you did add a template matching t:Invoice\t:Header1 then because of the parent being specified, it would have a higher priority as the one just matching t:Header1 and be used instead).
What you will need to do, is put all the logic in the single template. In this case, all you need to do is remove the xsl:copy from that template to avoid the Header1 being copied to the output tree. Additionally, when you create Header, you are creating it in no namespace, not in the namespace bound to the prefix "t". Therefore, the child elements will be given new namespace declarations because they will still be in that namespace.
One way to do it is simply add a "namespace" attribute to the xsl:element, like so:
<xsl:element name="Header" namespace="http://www.spscommerce.com/RSX">
Alternatively, you can create the element by just doing <Header> but you will need to add a default namespace declaration to the XSLT too, to ensure it gets output in the correct namespace.
Try this XSLT:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:t="http://www.spscommerce.com/RSX"
xmlns="http://www.spscommerce.com/RSX"
exclude-result-prefixes="t">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<!--rule to suppress the undesired nodes-->
<xsl:template match="t:Invoices1|t:ivhID_Collection">
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="t:LineItem_Collection">
<xsl:apply-templates/>
</xsl:template>
<!-- Identity Transform -->
<xsl:template match="t:Header1">
<xsl:apply-templates select="#*" />
<Header>
<xsl:apply-templates select="#*|t:InvoiceHeader|t:PaymentTerms|t:Dates|t:Address|t:References|t:ChargesAllowances"/>
</Header>
<xsl:apply-templates select="t:LineItem_Collection|t:Summary"/>
</xsl:template>
</xsl:stylesheet>
As a side note, in your XSLT you were also doing this immediately after creating the Header element
<xsl:apply-templates select="#*|t:LineItem_Collection|t:Summary"/>
This would fail if the Header1 had attributes you wanted to copy, as it is an error to try to add attributes to a parent element after you have created child elements. This is why in my XSLT I have split the statement into two.
Input
<row>
<name-prf>Prokofiev, Serge (piano); Adolph Bolm (dancer)</name-prf>
</row>
Desired Output
<subject>
<name type="personal">
<namePart>Prokofiev, Serge (piano)</namePart>
<role>
<roleTerm type="code" authority="marcrelator">prf</roleTerm>
</role>
</name>
</subject>
<subject>
<name type="personal">
<namePart>Adolph Bolm (dancer)</namePart>
<role>
<roleTerm type="code" authority="marcrelator">prf</roleTerm>
</role>
</name>
</subject>
Current code
<subject>
<name type="personal">
<namePart>
<xsl:value-of select="name-prf"/>
</namePart>
<role>
<roleTerm type="code" authority="marcrelator">prf</roleTerm>
</role>
</name>
</subject>
I've been largely using value of rather than individual templates because the order is very different from the initial sheet, but I do feel like the solution here is probably forcing a template call multiple times?
I'm just not quite sure how to use tokenize in this context. I can use
2.0
Something like:
<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:template match="row">
<xsl:for-each select="tokenize(name-prf, '; ')">
<subject>
<name type="personal">
<namePart>
<xsl:value-of select="."/>
</namePart>
<role>
<roleTerm type="code" authority="marcrelator">prf</roleTerm>
</role>
</name>
</subject>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
If you are restricted to XSLT 1.0 add this as an extension function:
public XPathNodeIterator Tokenize(string value, string separators)
{
XElement root = new XElement("Root");
string[] tokens = value.Split(separators.ToCharArray());
for (int i = 0; i < tokens.Length; i++)
{
root.Add(tokens[i]);
}
return root.CreateNavigator().Select("/");
}
Ok, tokenize was the key, I wasn't able to straight tokenize but I could tokenize with a parameter, not sure why that made a difference:
<xsl:param name="seperator" select="'; '"/>
<xsl:for-each select="distinct-values(name-prf/tokenize(.,$seperator))">
<person><xsl:value-of select="."/></person>
</xsl:for-each>
I have source xml looking like this :
<Data>
<ActionPlaces>
<ActionPlace>
<ActionPlaceID>74</ActionPlaceID>
<PlaceName>Theatre Of Classic</PlaceName>
</ActionPlace>
</ActionPlaces>
<Actions>
<CommonAction Id="2075" Name="King">
<Action>
<ActionID>4706</ActionID>
<ActionPlaceID>74</ActionPlaceID>
</Action>
</CommonAction>
</Actions>
</Data>
Which is to transform to this:
<category name="King">
<name>King</name>
<parent name="Theatre Of Classic" />
</category>
I want to use variable :
<xsl:template match="ActionPlaces">
<xsl:variable name="id" select="/ActionPlace/ActionPlaceID"/>
<xsl:template match="CommonAction" >
<category name="<xsl:value-of select="#name"/> >
<name><xsl:value-of select="#name"/></name>
<parent <xsl:if test="/Action/ActionPlaceID = $id">
name=/Action/ActionPlaceID/> <- how to get name of theatre here?
</xsl:template>
Can variable store not only id but name also? And how to get it? What is the most common approach to handle this ?
Here's one option using XSL keys (as #michael-hor257k suggested):
Input
<Root>
<ActionPlaces>
<ActionPlace>
<ActionPlaceID>74</ActionPlaceID>
<PlaceName>Theatre Of Classic</PlaceName>
</ActionPlace>
</ActionPlaces>
<Actions>
<CommonAction Id="2075" Name="King">
<Action>
<ActionID>4706</ActionID>
<ActionPlaceID>74</ActionPlaceID>
</Action>
</CommonAction>
</Actions>
</Root>
Stylesheet
<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"/>
<!-- Collect all <ActionPlace> elements into an XSL key -->
<xsl:key name="ActionPlaceById" match="ActionPlace" use="ActionPlaceID"/>
<xsl:template match="/">
<xsl:apply-templates select="Root/Actions/CommonAction"/>
</xsl:template>
<xsl:template match="CommonAction">
<category name="{#Name}">
<name>
<xsl:value-of select="#Name"/>
</name>
<!--
Using the ActionPlaceById key we created earlier, fetch the <ActionPlace>
element that has an <ActionPlaceID> child that has the same value as the
<ActionPlaceID> descendant of the current <CommonAction> element.
-->
<parent name="{key('ActionPlaceById', Action/ActionPlaceID)/PlaceName}"/>
</category>
</xsl:template>
</xsl:stylesheet>
Output
<?xml version="1.0" encoding="utf-8"?>
<category name="King">
<name>King</name>
<parent name="Theatre Of Classic"/>
</category>
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.