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

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.

Related

How to loop through all same name parent and child nodes

I have an XML file where I have same name nodes: Parent and child, for example:
<Root>
<Field name="ID">
<description>Test 1</description>
</Field>
<Field name="Period">
<description>Test 2</description>
<Field name="Name">
<description>Test 3</description>
</Field>
<Field name="Name2">
<description>Test 4</description>
<Field name="address">
<description>Test 5</description>
</Field>
<Field name="partyID">
<description>Test 6</description>
<Field name="E-ID">
<Field name="address">
<description>Test 7</description>
</Field>
</Field>
</Field>
</Field>
</Field>
</Root>
The problem is, I am not sure how deep we can have child elements with the same name:
I used template-match on top parent node:
<xsl:template match="Field">
<xsl:value-of select="description"/>
<xsl:for-each select="Field">
<xsl:value-of select="description"/>
</xsl:for-each>
</xsl:template>
This code is not providing me all the child node values. I am looking for a code which can loop through all same name nodes and provide value of description element.
I cannot add multiple for-each because as I said, this is unknown how many times we will have Field node inside another Field node.
Please help me to solve this problem.
You could use recursion to process all Field elements from the root of the tree to the leaves - for example (you did not post the expected result, so I made something up):
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">
<output>
<xsl:apply-templates select="Field"/>
</output>
</xsl:template>
<xsl:template match="Field">
<desc>
<xsl:value-of select="description"/>
</desc>
<xsl:apply-templates select="Field"/>
</xsl:template>
</xsl:stylesheet>
Alternatively, you could just select all Field elements regardless of their position in the tree hierarchy:
<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">
<output>
<xsl:for-each select="//Field">
<desc>
<xsl:value-of select="description"/>
</desc>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>

Advanced muenchian grouping: group by items in child collection

I'm familiar with simple muenchian grouping in XSL, but I've encountered a problem, which I honestly don't even know, how to approach it.
So I've got an XML:
<whiskies>
<whisky name="ABC" percent="" region="" type="">
<tastingNotesCollection/>
<bottles>
<bottle price="" size="" level="0" date=""/>
<bottle price="" size="" level="70" date=""/>
<bottle price="" size="" level="100" date=""/>
</bottles>
<comments/>
</whisky>
<whisky name="DEF" percent="" region="" type="">
<tastingNotesCollection/>
<bottles>
<bottle price="" size="" level="0" date=""/>
<bottle price="" size="" level="100" date=""/>
</bottles>
<comments/>
</whisky>
<whisky name="GHI" percent="" region="" type="">
<tastingNotesCollection/>
<bottles>
<bottle price="" size="" level="30" date=""/>
</bottles>
<comments/>
</whisky>
<whiskies>
And the goal is to group the whiskies by levels of a bottle:
So level="0" is considered empty.
Anything from level="1" to level="99" is considered open.
And level="100" is considered unopened.
So the transformed result (will be done in HTML) should look like this:
<h1>Empty</h1>
<ul>
<li>ABC</li>
<li>DEF</li>
</ul>
<h1>Open</h1>
<ul>
<li>ABC</li>
<li>GHI</li>
</ul>
<h1>Unopened</h1>
<ul>
<li>ABC</li>
<li>DEF</li>
</ul>
As you can see, the same whisky can show up in multiple groups, depending on how much bottles there are and how full those bottles are.
The second problem is, that the "Open" group doesn't have an exact value and can be aynthing from 1 to 99.
So yeah, don't really know if this can be solved at all or how to even start on this. Any tips appreciated.
I don't think you want to use Muenchian grouping for this. You would need to define a key that enumerates all values in the range from 1 to 99 - and I believe that would take away any advantage that using a key would otherwise bring.
Since your result has either exactly or at most 3 groups (your question is ambiguous in this respect), you could do 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="/whiskies">
<output>
<group status="empty">
<xsl:for-each select="whisky[bottles/bottle/#level=0]">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="open">
<xsl:for-each select="whisky[bottles/bottle[#level>0 and #level < 100]]">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="unopened">
<xsl:for-each select="whisky[bottles/bottle/#level=100]">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
</output>
</xsl:template>
</xsl:stylesheet>
to get (after fixing the input to be a well-formed XML!):
Result
<?xml version="1.0" encoding="utf-8"?>
<output>
<group status="empty">
<name>ABC</name>
<name>DEF</name>
</group>
<group status="open">
<name>ABC</name>
<name>GHI</name>
</group>
<group status="unopened">
<name>ABC</name>
<name>DEF</name>
</group>
</output>
Make your own adjustment for HTML output.
Added:
Here is an alternative approach using keys (though still not Muenchian grouping) which might be more performant:
<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="w" match="whisky" use="bottles/bottle/#level" />
<xsl:key name="w1" match="whisky" use="boolean(bottles/bottle[#level!=0 and #level!=100])" />
<xsl:template match="/whiskies">
<output>
<group status="empty">
<xsl:for-each select="key('w', 0)">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="open">
<xsl:for-each select="key('w1', true())">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="unopened">
<xsl:for-each select="key('w', 100)">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
</output>
</xsl:template>
</xsl:stylesheet>
Or even:
<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="bottle" match="bottle" use="ceiling(#level div 99)" />
<xsl:template match="/whiskies">
<output>
<group status="empty">
<xsl:for-each select="key('bottle', 0)/../..">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="open">
<xsl:for-each select="key('bottle', 1)/../..">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
<group status="unopened">
<xsl:for-each select="key('bottle', 2)/../..">
<name>
<xsl:value-of select="#name"/>
</name>
</xsl:for-each>
</group>
</output>
</xsl:template>
</xsl:stylesheet>
If you did want to use xsl:key, you could create 3 of them with the filter criteria:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes" />
<xsl:key name="Empty" match="whisky[bottles/bottle/#level=0]" use="#name"/>
<xsl:key name="Open" match="whisky[bottles/bottle/#level[. > 1 and . < 99]]" use="#name"/>
<xsl:key name="Unopened" match="whisky[bottles/bottle/#level=100]" use="#name"/>
<xsl:template match="/">
<xsl:variable name="whiskies" select="/whiskies/whisky/#name"/>
<xsl:for-each select="document('')/xsl:stylesheet/xsl:key/#name">
<xsl:variable name="status" select="."/>
<h1><xsl:value-of select="$status"/></h1>
<ul>
<xsl:for-each select="$whiskies[key($status, .)]">
<li><xsl:value-of select="."/></li>
</xsl:for-each>
</ul>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

Grouping XSLT elements by value (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>

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>

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>