Grouping XSLT elements by value (XSLT 1.0) - xslt-1.0

I am having a really hard time trying to group different elements by a common value using XSLT 1.0.
Using the following XML:
<root>
<segment>
<id>ABCD123</id>
</segment>
<segment>
<contact>
<field1>ABCD123</field1>
<field2>(111)345-7890</field2>
</contact>
</segment>
<segment>
<details>
<field1>ABCD123</field1>
<field5>More Details for ABCD123</field5>
</details>
</segment>
<segment>
<id>XZX098</id>
</segment>
<segment>
<contact>
<field1>XZX098</field1>
<field2>(111)443-9999</field2>
</contact>
</segment>
<segment>
<details>
<field1>XZX098</field1>
<field5>More Details for XZX098</field5>
</details>
</segment>
</root>
Transform into this:
<File>
<Record>
<id>ABCD123</id>
<phone>(111)345-7890</phone>
<details>More Details for ABCD123</details>
</Record>
<Record>
<id>XZX098</id>
<phone>(111)443-9999</phone>
<details>More Details for XZX098</details>
</Record>
</File>
I'm trying to group records by the 'id', and then get the contact, and details information that matches that 'id'.
Any help is greatly appreciated.

I don't see any grouping per se required here - just a simple lookup of cross-referenced data:
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:key name="contact-details" match="contact|details" use="field1" />
<xsl:template match="root">
<File>
<xsl:for-each select="segment/id">
<Record>
<xsl:copy-of select="."/>
<phone>
<xsl:value-of select="key('contact-details', .)/field2"/>
</phone>
<details>
<xsl:value-of select="key('contact-details', .)/field5"/>
</details>
</Record>
</xsl:for-each>
</File>
</xsl:template>
</xsl:stylesheet>

Related

How do I handle files of different lengths / more 'lines'

My problem is, is that my input will have varying lengths / lines. My current code (thanks to michael.hor257k on my previous question) works with my test file, but I'm sure it won't work with files that have more items.
For example:
An input file with 100 records
or
an input file with 2 records.
I am also not sure how to ignore the last record as it is junk created in the original csv file that's converted to the input file.
Input:
<csv-xml>
<record line="1">
<csv-field-1>1</csv-field-1>
<csv-field-2>12345</csv-field-2>
<csv-field-3>7654321</csv-field-3>
<csv-field-4>1</csv-field-4>
<csv-field-5>08/08/19</csv-field-5>
<csv-field-6>08/08/19</csv-field-6>
</record>
<record line="2">
<csv-field-1>2</csv-field-1>
<csv-field-2>12345</csv-field-2>
<csv-field-3>12345678</csv-field-3>
<csv-field-4>3</csv-field-4>
</record>
<record line="3">
<csv-field-1>2</csv-field-1>
<csv-field-2>12345</csv-field-2>
<csv-field-3>22345679</csv-field-3>
<csv-field-4>7</csv-field-4>
</record>
<record line="4">
<csv-field-1>2</csv-field-1>
<csv-field-2>12345</csv-field-2>
<csv-field-3>32345680</csv-field-3>
<csv-field-4>6</csv-field-4>
</record>
<record line="5">
<csv-field-1>2</csv-field-1>
<csv-field-2>12345</csv-field-2>
<csv-field-3>42345681</csv-field-3>
<csv-field-4>2</csv-field-4>
</record>
<record line="6">
<csv-field-1>3</csv-field-1>
<csv-field-2>12345</csv-field-2>
<csv-field-3></csv-field-3>
</record>
</csv-xml>
Code:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" >
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<orders>
<order>
<accountNo>
<xsl:value-of select="csv-xml/record[#line>='1']/csv-field-3"/>
</accountNo>
<orderDate>
<xsl:value-of select="csv-xml/record[#line>='1']/csv-field-5"/>
</orderDate>
<orderItems>
<orderItem>
<productCode>
<xsl:value-of select="csv-xml/record[#line>='2']/csv-field-3"/>
</productCode>
<Quantity>
<xsl:value-of select="csv-xml/record[#line>='2']/csv-field-4"/>
</Quantity>
</orderItem>
<orderItem>
<productCode>
<xsl:value-of select="csv-xml/record[#line>='3']/csv-field-3"/>
</productCode>
<Quantity>
<xsl:value-of select="csv-xml/record[#line>='3']/csv-field-4"/>
</Quantity>
</orderItem>
<orderItem>
<productCode>
<xsl:value-of select="csv-xml/record[#line>='4']/csv-field-3"/>
</productCode>
<Quantity>
<xsl:value-of select="csv-xml/record[#line>='4']/csv-field-4"/>
</Quantity>
</orderItem>
<orderItem>
<productCode>
<xsl:value-of select="csv-xml/record[#line>='5']/csv-field-3"/>
</productCode>
<Quantity>
<xsl:value-of select="csv-xml/record[#line>='5']/csv-field-4"/>
</Quantity>
</orderItem>
</orderItems>
</order>
</orders>
</xsl:template>
</xsl:stylesheet>
The output I have:
<?xml version="1.0"?>
<orders>
<order>
<accountNo>7654321</accountNo>
<orderDate>08/08/19</orderDate>
<orderItems>
<orderItem>
<productCode>12345678</productCode>
<Quantity>3</Quantity>
</orderItem>
<orderItem>
<productCode>22345679</productCode>
<Quantity>7</Quantity>
</orderItem>
<orderItem>
<productCode>32345680</productCode>
<Quantity>6</Quantity>
</orderItem>
<orderItem>
<productCode>42345681</productCode>
<Quantity>2</Quantity>
</orderItem>
</orderItems>
</order>
</orders>
The output I want:
<?xml version="1.0"?>
<orders><order accountNo="7654321" orderDate="08/08/19">
<orderItems>
<orderItem productCode="12345678" quantity="3"/>
<orderItem productCode="22345679" quantity="7"/>
<orderItem productCode="32345680" quantity="6"/>
<orderItem productCode="42345681" quantity="2"/>
</orderItems>
</order>
</orders>
How about simply:
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:template match="/csv-xml">
<xsl:variable name="header" select="record[1]" />
<orders>
<order accountNo="{$header/csv-field-3}" orderDate="{$header/csv-field-5}">
<orderItems>
<xsl:for-each select="record[position() != 1 and position() != last()]">
<orderItem productCode="{csv-field-3}" quantity="{csv-field-4}"/>
</xsl:for-each>
</orderItems>
</order>
</orders>
</xsl:template>
</xsl:stylesheet>
P.S. Read about Attribute Value Templates.

XSLT: Convert Name/Value pair and transform an XML

I need to convert a name value pair into XML. I'm able to generate an XML, but the element name should be grouped and it should not be duplicated. Please see below. The FieldValue element contains 2 OrderItem values in the Detail node. If the FieldValue with OrderItem repeats, then the result should be grouped into one OrderItem node. Please help.
Source XML:
<SC>
<Header>
<Record>
<FieldName>Schema</FieldName>
<FieldValue>OrderHeader</FieldValue>
</Record>
<Record>
<FieldName>Order</FieldName>
<FieldValue>1234</FieldValue>
</Record>
</Header>
<Detail>
<Record>
<FieldName>Schema</FieldName>
<FieldValue>OrderItem</FieldValue>
</Record>
<Record>
<FieldName>Item</FieldName>
<FieldValue>1</FieldValue>
</Record>
<Record>
<FieldName>Qty</FieldName>
<FieldValue>10</FieldValue>
</Record>
</Detail>
<Detail>
<Record>
<FieldName>Schema</FieldName>
<FieldValue>OrderItem</FieldValue>
</Record>
<Record>
<FieldName>Item</FieldName>
<FieldValue>2</FieldValue>
</Record>
<Record>
<FieldName>Qty</FieldName>
<FieldValue>20</FieldValue>
</Record>
</Detail>
</SC>
Target XML:
<Order>
<OrderItem>
<Item>
<Item>1</Item>
<Qty>10</Qty>
</Item>
<Item>
<Item>2</Item>
<Qty>20</Qty>
</Item>
</OrderItem>
</Order>
XSLT:
<xsl:template match="#*|node()">
<Order>
<xsl:for-each select="Detail">
<Item>
<xsl:apply-templates select="Record[position()>1]"/>
</Item>
</xsl:for-each>
</Order>
</xsl:template>
<xsl:template match="Record">
<xsl:element name="{FieldName}">
<xsl:value-of select="FieldValue"/>
</xsl:element>
</xsl:template>
The grouping can be done as follows:
<?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 indent="yes"/>
<xsl:template match="SC">
<Order>
<xsl:for-each-group select="Detail" group-by="Record[1]/FieldValue">
<xsl:element name="{current-grouping-key()}">
<xsl:apply-templates select="current-group()"/>
</xsl:element>
</xsl:for-each-group>
</Order>
</xsl:template>
<xsl:template match="Detail">
<Item>
<xsl:apply-templates select="Record[position() gt 1]"/>
</Item>
</xsl:template>
<xsl:template match="Record">
<xsl:element name="{FieldName}">
<xsl:value-of select="FieldValue"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
It appears that you are trying to define two XSLT templates, when one should be sufficient. You want to match on the root and then that you want to iterate over each SC/Detail.
Then, you want to take the FieldValue of the sibling of the FieldName node that is 'Item' (for item value) and 'Qty' (for quantity value), but only those listed under 'Record'.
Note: You have specified a doubly-nested <Item> in your transformed output and this solution reflects that requirement.
This XSLT should do what you are requesting:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:template match="/">
<xsl:for-each select="SC/Detail">
<Order>
<OrderItem>
<Item>
<Item>
<xsl:value-of select="Record[FieldName[text()='Item']]/FieldValue" />
</Item>
<Qty>
<xsl:value-of select="Record[FieldName[text()='Qty']]/FieldValue" />
</Qty>
</Item>
</OrderItem>
</Order>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

How to group a specific number of blocks in an xml based on number of tags

I have a big xml like below and I would like to group a specific number of tags under one block; The expected input and output below will make my question clearer. Any help is greatly appreciated
The input file is
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Root>
<ListABC>
<ABC name="name1" class="class1" age="age1" />
<ABC name="name2" class="class2" age="age2" />
<ABC name="name3" class="class3" age="age3" />
<ABC name="name4" class="class4" age="age4" />
<ABC name="name5" class="class5" age="age5" />
</ListABC>
<ListABC>
<EOF tag1="1" tag2="2" tag3="3"/>
</ListABC>
</Root>
I need to create a tag ListABC after every 2 ABC elements and at the same time, the last ListABC which contains EOF element should not be impacted at all. This is how I need the output
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<Root>
<ListABC>
<ABC name="name1" class="class1" age="age1" />
<ABC name="name2" class="class2" age="age2" />
</ListABC>
<ListABC>
<ABC name="name3" class="class3" age="age3" />
<ABC name="name4" class="class4" age="age4" />
</ListABC>
<ListABC>
<ABC name="name5" class="class5" age="age5" />
</ListABC>
<ListABC>
<EOF tag1="1" tag2="2" tag3="3"/>
</ListABC>
</Root>
Thanks much!
How about:
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="/Root">
<xsl:copy>
<xsl:for-each select="ListABC[not (EOF)]/ABC[position() mod 2 = 1]">
<ListABC>
<xsl:copy-of select=". | following-sibling::ABC[1]"/>
</ListABC>
</xsl:for-each>
<xsl:copy-of select="ListABC[EOF]"/>
</xsl:copy>
</xsl:template>
</xsl:stylesheet>

Filter keys out of response

I have an xslt sheet in which I have 2 response objects. $response1 contains a list of ids something like:
<response>
<idlist>
<id>1</id>
<id>2</id>
</idlist>
</response>
And $response2 contains a number of objects:
<response2>
<obj id="1" name="obj1"/>
<obj id="2" name="obj2"/>
<obj id="3" name="obj3"/>
<obj id="4" name="obj4"/>
</response2>
I want to make a copy of response2 but filtering out any objects where the id matches thos contained in response 1
<xsl:variable name="copy">
<xsl:copy-of select="$response2/*[not contains($response1, id)]"/>
</xsl:variable>
any ideas greatly appreciated
C
Given a well-formed input such as:
<root>
<response>
<idlist>
<id>1</id>
<id>2</id>
</idlist>
</response>
<response2>
<obj id="1" name="obj1"/>
<obj id="2" name="obj2"/>
<obj id="3" name="obj3"/>
<obj id="4" name="obj4"/>
</response2>
</root>
the following stylesheet:
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:template match="/root">
<xsl:variable name="ids" select="response/idlist/id" />
<output>
<xsl:copy-of select="response2/obj[not(#id=$ids)]"/>
</output>
</xsl:template>
</xsl:stylesheet>
will return:
<?xml version="1.0" encoding="UTF-8"?>
<output>
<obj id="3" name="obj3"/>
<obj id="4" name="obj4"/>
</output>
A better solution is to use a key to link the nodes by matching id:
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:key name="id" match="id" use="." />
<xsl:template match="/root">
<output>
<xsl:copy-of select="response2/obj[not(key('id', #id))]"/>
</output>
</xsl:template>
</xsl:stylesheet>

XSLT to get first five unique result

I am beginner in XSLT 1.0. I am using XSLT to transform XML to XML.
I would like to get first 5 unique Airline from the received XML.
Source XML:
<Response>
<Flights>
<Flight>
<Segments>
<Segment>
<Airline>AB</Airline>
</Segment>
</Segments>
</Flight>
<Flight>
<Segments>
<Segment>
<Airline>CD</Airline>
</Segment>
</Segments>
</Flight>
<Flight>
<Segments>
<Segment>
<Airline>EF</Airline>
</Segment>
</Segments>
</Flight>
<Flight>
<Segments>
<Segment>
<Airline>EF</Airline>
</Segment>
</Segments>
</Flight>
<Flight>
<Segments>
<Segment>
<Airline>EF</Airline>
</Segment>
</Segments>
</Flight>
<Flight>
<Segments>
<Segment>
<Airline>EF</Airline>
</Segment>
</Segments>
</Flight>
<Flight>
<Segments>
<Segment>
<Airline>SD</Airline>
</Segment>
</Segments>
</Flight>
</Flights>
<OtherRecommens>
<RecommFlight>
<Airline>XY</Airline>
</RecommFlight>
<RecommFlight>
<Airline>ZZ</Airline>
</RecommFlight>
<RecommFlight>
<Airline>XY</Airline>
</RecommFlight>
<RecommFlight>
<Airline>AB</Airline>
</RecommFlight>
</OtherRecommens>
</Response>
XSLT:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:template match="Response">
<xsl:element name="Root">
<xsl:variable name="Airlines" select="//Airline"/>
<xsl:for-each select="$Airlines">
<xsl:if test="not(preceding::Airline[.=current()/text()])">
<xsl:element name="SpecificAirline">
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:if>
</xsl:for-each>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
By applying above XSLT i get the below output:
Output:
<Root>
<SpecificAirline>AB</SpecificAirline>
<SpecificAirline>CD</SpecificAirline>
<SpecificAirline>EF</SpecificAirline>
<SpecificAirline>SD</SpecificAirline>
<SpecificAirline>XY</SpecificAirline>
<SpecificAirline>ZZ</SpecificAirline>
</Root>
As per my requirement i need to get only first five airlines
Expected Output:
<Root>
<SpecificAirline>AB</SpecificAirline>
<SpecificAirline>CD</SpecificAirline>
<SpecificAirline>EF</SpecificAirline>
<SpecificAirline>SD</SpecificAirline>
<SpecificAirline>XY</SpecificAirline>
</Root>
Please help. Thanks.
I have used a grouping described here: how to apply group by on xslt elements
And array index limitation 5 >= position() described here: http://www.w3schools.com/xpath/xpath_functions.asp#context
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="xml" indent="yes" />
<xsl:key name="airlinetext" match="Airline" use="text()" />
<xsl:template match="Response">
<xsl:element name="Root">
<xsl:apply-templates select="(//Airline[generate-id(.)=generate-id(key('airlinetext',text())[1])])[5>=position()]"/>
</xsl:element>
</xsl:template>
<xsl:template match="Airline">
<xsl:element name="SpecificAirline">
<xsl:value-of select="text()"/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>