Sorting and moving elements into a new element - xslt

Even with all the good tips on this site, I still have some trouble with my xslt. I'm pretty new to it. I have this source file:
<?xml version="1.0" encoding="utf-8"?>
<file>
<id>1</id>
<row type="A">
<name>ABC</name>
</row>
<row type="B">
<name>BCA</name>
</row>
<row type="A">
<name>CBA</name>
</row>
</file>
and I want to add an element and sort the rows on type, to get this result
<file>
<id>1</id>
<details>
<row type="A">
<name>ABC</name>
</row>
<row type="A">
<name>CBA</name>
</row>
<row type="B">
<name>BCA</name>
</row>
</details>
</file>
I'm able to sort the rows using this:
<xsl:template match="file">
<xsl:copy>
<xsl:apply-templates select="#*/row"/>
<xsl:apply-templates>
<xsl:sort select="#type" data-type="text"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
and I'm able to move the rows using this
<xsl:template match="file">
<xsl:copy>
<xsl:copy-of select="#*" />
<xsl:apply-templates select="*[not(name(.)='row')]" />
<details>
<xsl:apply-templates select="row" />
</details>
</xsl:copy>
</xsl:template>
but I'm not able to produce the correct answer when I try to combine them. Hopefully I understand more of XSLT when I see how things are combined. Since I'm creating a new element <details>, I think the sorting has to be done before the creation of the new <details> element. I have to use xslt 1.0.

Something like this seems to work:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="file">
<xsl:copy>
<xsl:copy-of select="#*"/>
<xsl:copy-of select="row[1]/preceding-sibling::*" />
<details>
<xsl:for-each select="row">
<xsl:sort select="#type" data-type="text"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</details>
<xsl:copy-of select="row[last()]/following-sibling::*" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Here is the result I got:
<?xml version="1.0" encoding="utf-8"?>
<file>
<id>1</id>
<details>
<row type="A">
<name>ABC</name>
</row>
<row type="A">
<name>CBA</name>
</row>
<row type="B">
<name>BCA</name>
</row>
</details>
</file>

Related

Add attributes from to node to parent

After reading a lot about this question already, I still do not find final solution for my problem as I am an absolut beginner with xsl.
I want to add all attributes of child nodes to parent level.
This is what I have:
<rankings date="2021-03-15">
<ranking rank="1" rank_change="0" points="12008">
<player initials="" nationality="SRB" last_name="Djokovic" first_name="Novak" id="7" display_name="Novak Djokovic"/>
</ranking>
<ranking rank="2" rank_change="1" points="9940">
<player initials="" nationality="RUS" last_name="Medvedev" first_name="Daniil" id="35844" display_name="Daniil Medvedev"/>
</ranking>
<ranking rank="3" rank_change="-1" points="9670">
<player initials="" nationality="ESP" last_name="Nadal" first_name="Rafael" id="4" display_name="Rafael Nadal"/>
</ranking>
</rankings>
This is what I tried (miss identity tranform I think)
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="rankings">
<data>
<xsl:apply-templates select="*"/>
</data>
</xsl:template>
<xsl:template match="ranking | player">
<row>
<xsl:apply-templates select="#* | node()"/>
</row>
</xsl:template>
<xsl:template match="ranking/#* | player/#*">
<xsl:element name="{name(.)}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:template>
</xsl:stylesheet>
With following result:
<data>
<row>
<rank>1</rank>
<rank_change>0</rank_change>
<points>12008</points>
<row>
<initials/>
<nationality>SRB</nationality>
<last_name>Djokovic</last_name>
<first_name>Novak</first_name>
<id>7</id>
<display_name>Novak Djokovic</display_name>
</row>
</row>
</data>
This is my goal:
<data>
<row>
<rank>1</rank>
<rank_change>0</rank_change>
<points>12008</points>
<initials/>
<nationality>SRB</nationality>
<last_name>Djokovic</last_name>
<first_name>Novak</first_name>
<id>7</id>
<display_name>Novak Djokovic</display_name>
</row>
</data>
I hope one of you can help me with this.
Cheers,
Phil
try splitting ranking and player in its own template
<xsl:template match="ranking">
<row>
<xsl:apply-templates select="#* | node()"/>
</row>
</xsl:template>
<xsl:template match="player">
<xsl:apply-templates select="#* | node()"/>
</xsl:template>
Result:
<data>
<row>
<rank>1</rank>
<rank_change>0</rank_change>
<points>12008</points>
<initials/>
<nationality>SRB</nationality>
<last_name>Djokovic</last_name>
<first_name>Novak</first_name>
<id>7</id>
<display_name>Novak Djokovic</display_name>
</row>
<row>
<rank>2</rank>
<rank_change>1</rank_change>
<points>9940</points>
<initials/>
<nationality>RUS</nationality>
<last_name>Medvedev</last_name>
<first_name>Daniil</first_name>
<id>35844</id>
<display_name>Daniil Medvedev</display_name>
</row>
<row>
<rank>3</rank>
<rank_change>-1</rank_change>
<points>9670</points>
<initials/>
<nationality>ESP</nationality>
<last_name>Nadal</last_name>
<first_name>Rafael</first_name>
<id>4</id>
<display_name>Rafael Nadal</display_name>
</row>
</data>
If I am guessing correctly what your real goal is, 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="rankings">
<data>
<xsl:for-each select="ranking">
<row>
<xsl:for-each select=".//#*">
<xsl:element name="{name(.)}">
<xsl:value-of select="."/>
</xsl:element>
</xsl:for-each>
</row>
</xsl:for-each>
</data>
</xsl:template>
</xsl:stylesheet>

Removing comma from number format with unique XSLT coding

With the example below, the XSLT is doing a few things, it is grouping by column 1 and column2, if it is the same, then it will group the column 3 amount. As well, within the PayAmount tags, it is reversing negative (-) number into a positive and vice versa. What I am having troubles with is writing logic to remove the comma (,) from column 3 in the output.
Below is my XSLT Code
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:template match="data">
<xsl:for-each select="row[generate-id(.) = generate-id(key(''rows'', concat(column1, ''||'', column2)))]">
</Record>
<Detail>
<Amount>
<xsl:variable name="mySum">
<xsl:value-of select="sum(key(''rows'', concat(column1, ''||'', column2))/column3)" />
</xsl:variable>
<xsl:value-of select="translate(($mySum * ($mySum >= 0) - $mySum * not($mySum >= 0)),'','','''')" />
</Amount>
</Detail>
</Record>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I have tried the following logic, but the comma remains in the output:
- <xsl:value-of select="translate(($mySum * ($mySum >= 0) - $mySum * not($mySum >= 0)),'','', '''')" />
- <xsl:value-of select="format-number($mySum * ($mySum >= 0) - $mySum * not($mySum >= 0),'#,##0.00')" />
- <xsl:value-of select="translate(sum(key(''rows'', concat(column1, ''||'', column2))/column3),'','','''')" />
Below is a sample data in XML:
<data>
<row>
<column1>200040</column1>
<column2>Auto</column2>
<column3>-500.00</column3>
</row>
<row>
<column1>200040</column1>
<column2>Auto</column2>
<column3>-5,000.00</column3>
</row>
<row>
<column1>200040</column1>
<column2>Auto</column2>
<column3>-1,000.00</column3>
</row>
<row>
<column1>200040</column1>
<column2>Auto</column2>
<column3>300.00</column3>
</row>
<row>
<column1>200040</column1>
<column2>Auto</column2>
<column3>-4,000.00</column3>
</row>
<row>
<column1>200041</column1>
<column2>Bike</column2>
<column3>-1,700.00</column3>
</row>
<row>
<column1>200041</column1>
<column2>Bike</column2>
<column3>-1,000.00</column3>
</row>
<row>
<column1>200041</column1>
<column2>Bike</column2>
<column3>800.00</column3>
</row>
<row>
<column1>200045</column1>
<column2>Bus</column2>
<column3>200.00</column3>
</row>
<row>
<column1>200045</column1>
<column2>Bus</column2>
<column3>-10,000.00</column3>
</row>
<row>
<column1>200045</column1>
<column2>Bus</column2>
<column3>5,000.00</column3>
</row>
</data>
The output I would like to achieve after running the XSLT is
200040 | Auto | 10200.00
200041 | Bike | 1900.00
200045 | Bus | 4800.00
Any help would be greatly appreciated!
A value that contains a comma is not a number and cannot be summed. You need to remove the commas before you attempt to sum the values.
Here's a simplified example:
XSLT 1.0 + EXSLT node-set()
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes" />
<xsl:key name="a" match="amount" use="#key"/>
<xsl:template match="data">
<!-- first pass: convert amounts to numbers -->
<xsl:variable name="amounts">
<xsl:for-each select="row">
<amount key="{concat(column1, '|', column2)}">
<xsl:value-of select="translate(column3, ',', '')"/>
</amount>
</xsl:for-each>
</xsl:variable>
<!-- output -->
<Output>
<xsl:for-each select="exsl:node-set($amounts)/amount[generate-id() = generate-id(key('a', #key))]">
<Record>
<Detail1>
<xsl:value-of select="substring-before(#key, '|')"/>
</Detail1>
<Detail2>
<xsl:value-of select="substring-after(#key, '|')"/>
</Detail2>
<xsl:variable name="sum" select="sum(key('a', #key))"/>
<Amount>
<xsl:value-of select="format-number($sum, '0.00')"/>
</Amount>
</Record>
</xsl:for-each>
</Output>
</xsl:template>
</xsl:stylesheet>
Applied to your input example, this will produce:
Result
<?xml version="1.0" encoding="UTF-8"?>
<Output>
<Record>
<Detail1>200040</Detail1>
<Detail2>Auto</Detail2>
<Amount>-10200.00</Amount>
</Record>
<Record>
<Detail1>200041</Detail1>
<Detail2>Bike</Detail2>
<Amount>-1900.00</Amount>
</Record>
<Record>
<Detail1>200045</Detail1>
<Detail2>Bus</Detail2>
<Amount>-4800.00</Amount>
</Record>
</Output>
To reverse negative amounts to positive (and vice versa), you could simply change:
<xsl:value-of select="format-number($sum, '0.00')"/>
to:
<xsl:value-of select="format-number(-$sum, '0.00')"/>

XSLT Apply Templates to derive specific parent & children records, children can be parent too

I need help with my XSLT. Here's my XML structure.
<root>
<row>
<component>mainfield_1</component>
<type>Field</type>
<where_used>
<component>subfield_2</component>
<type>Field</type>
</where_used>
<where_used>
<component>report_1</component>
<type>Report</type>
</where_used>
</row>
<row>
<component>subfield_2</component>
<type>Field</type>
<where_used>
<component>report_2</component>
<type>report</type>
</where_used>
</row>
<row>
<component>mainfield_3</component>
<type>Field</type>
</row>
</root>
I would like it to be transformed into the following:
<root>
<row>
<component>mainfield_1</component>
<type>Field</type>
</row>
<row>
<component>subfield_2</component>
<type>Field</type>
</row>
<row>
<component>report_1</component>
<type>Report</type>
</row>
<row>
<component>report_2</component>
<type>report</type>
</row>
</root>
Basically, I am trying to get all the distinct dependencies of component mainfield_1. Here's my sample code but it is not enough to find any matching parent that has the same component name as the children.
<xsl:template match="root">
<root>
<xsl:apply-templates select="row[component='mainfield_1']"/>
</root>
</xsl:template>
<xsl:template match="row">
<row>
<component>
<xsl:value-of select="component"/>
</component>
<type>
<xsl:value-of select="type" />
</type>
</row>
<xsl:apply-templates select="where_used"/>
</xsl:template>
<xsl:template match="where_used">
<row>
<component>
<xsl:value-of select="component"/>
</component>
<type>
<xsl:value-of select="type" />
</type>
</row>
</xsl:template>
If I run the above, I will not be able to get this.
<row>
<component>report_2</component>
<type>report</type>
</row>
Please help.
Consider using a key to look up the row items by component
<xsl:key name="rows" match="row" use="component" />
Then you can have a template for where_used nodes that refer to a separate row, allowing you to select that row instead
<xsl:template match="where_used[key('rows', component)]">
<xsl:apply-templates select="key('rows', component)" />
</xsl:template>
Try this XSLT
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" indent="yes" />
<xsl:key name="rows" match="row" use="component" />
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()"/>
</xsl:copy>
</xsl:template>
<xsl:template match="root">
<root>
<xsl:apply-templates select="row[component='mainfield_1']"/>
</root>
</xsl:template>
<xsl:template match="row">
<row>
<xsl:apply-templates select="* except where_used" />
</row>
<xsl:apply-templates select="where_used"/>
</xsl:template>
<xsl:template match="where_used">
<row>
<xsl:apply-templates />
</row>
</xsl:template>
<xsl:template match="where_used[key('rows', component)]">
<xsl:apply-templates select="key('rows', component)" />
</xsl:template>
</xsl:stylesheet>
Note I have used the identity template too, to avoid having to explicitly copy existing nodes that don't need to be changed.

Grouping and moving remaining nodes using XSLT 1.0

I have the following xml,
<?xml version="1.0" encoding="UTF-8"?>
<response>
<case>
<CMEDIA>Phone</CMEDIA>
</case>
<results>
<row>
<IKEY>TestKey1</IKEY>
<OBJECTID>TestObject1</OBJECTID>
</row>
<row>
<IKEY>TestKey1</IKEY>
<OBJECTID>TestObject2</OBJECTID>
</row>
<row>
<IKEY>TestKey1</IKEY>
<OBJECTID>TestObject3</OBJECTID>
</row>
<row>
<IKEY>TestKey4</IKEY>
<OBJECTID>TestObject4</OBJECTID>
</row>
</results>
</response>
My requirement is to group all the matching <IKEY> rows and move them under one <row> and moving all <OBJECTID> nodes under that new <row>.
<?xml version="1.0" encoding="UTF-8"?>
<response>
<case>
<CMEDIA>Phone</CMEDIA>
</case>
<results>
<row>
<IKEY>TestKey1</IKEY>
<OBJECTID>TestObject1</OBJECTID>
<OBJECTID>TestObject2</OBJECTID>
<OBJECTID>TestObject3</OBJECTID>
</row>
<row>
<IKEY>TestKey4</IKEY>
<OBJECTID>TestObject4</OBJECTID>
</row>
</results>
</response>
I am trying with the following xsl for grouping based on <IKEY>, but I am not able to move all <OBJECTID> nodes to new <row>(Here I have to use only XSLT 1.0).
<?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="ikey" match="row" use="string(IKEY)" />
<xsl:template match="results">
<xsl:copy>
<xsl:apply-templates select="row[generate-id() = generate-id(key('ikey', string(IKEY))[1])]" mode="ikey" />
</xsl:copy>
</xsl:template>
<xsl:template match="row" mode="ikey">
<xsl:choose>
<xsl:when test="IKEY">
<row>
<xsl:apply-templates select="IKEY|OBJECTID" />
</row>
</xsl:when>
</xsl:choose>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
</xsl:stylesheet>
Can somebody tell me what I am missing here?
Change
<xsl:apply-templates select="IKEY|OBJECTID" />
to
<xsl:apply-templates select="IKEY|key('ikey', IKEY)/OBJECTID" />

Create Excel (SpeadsheetML) output with XSLT

I have this XML file and I want to create an XSL file to convert it to Excel. Each row should represent a logo. The columns will be the key attributes like color, id, description plus any other key for other logos.
<Top>
<logo>
<field key="id">172-32-1176</field>
<field key="color">Blue</field>
<field key="description"><p>Short Description</p></field>
<field key="startdate">408 496-7223</field>
</logo>
<logo>
<field key="id">111-111-111</field>
<field key="color">Red</field>
</logo>
<!-- ... -->
</Top>
The XSL file is something like this:
<xsl:stylesheet
version="1.0"
xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:user="urn:my-scripts"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
>
<xsl:template match="/">
<Workbook
xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:html="http://www.w3.org/TR/REC-html40"
>
<xsl:apply-templates/>
</Workbook>
</xsl:template>
<xsl:template match="/*">
<Worksheet>
<xsl:attribute name="ss:Name">
<xsl:value-of> select="local-name(/*)"/>
</xsl:attribute>
<Table x:FullColumns="1" x:FullRows="1">
<Row>
<xsl:for-each select="*/*">
<Cell>
<Data ss:Type="String">
<xsl:value-of select="#key"/>
</Data>
</Cell>
</xsl:for-each>
</Row>
<xsl:apply-templates/>
</Table>
</Worksheet>
</xsl:template>
<xsl:template match="/*/*">
<Row>
<xsl:apply-templates/>
</Row>
</xsl:template>
<xsl:template match="/*/*/*">
<Cell>
<Data ss:Type="String">
<xsl:value-of select="."/>
</Data>
</Cell>
<!-- <xsl:apply-templates/> -->
</xsl:template>
</xsl:stylesheet>
But data are not correctly placed under the columns and column names are repeating. How can this be done?
The columns could be in any order and also column stardate should be empty for second row in excel. Similarly for more .
You were very close. Try to be more specific when it comes to template matching - don't say template match"/*/*/*" when you can say template match="field".
Other than that, this is your approach, only slightly modified:
<xsl:stylesheet
version="1.0"
xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:user="urn:my-scripts"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
>
<xsl:output method="xml" encoding="utf-8" indent="yes" />
<xsl:template match="/">
<Workbook
xmlns="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:o="urn:schemas-microsoft-com:office:office"
xmlns:x="urn:schemas-microsoft-com:office:excel"
xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
xmlns:html="http://www.w3.org/TR/REC-html40"
>
<xsl:apply-templates select="Top" />
</Workbook>
</xsl:template>
<xsl:template match="Top">
<Worksheet ss:Name="{local-name()}">
<Table x:FullColumns="1" x:FullRows="1">
<Row>
<!-- header row, made from the first logo -->
<xsl:apply-templates select="logo[1]/field/#key" />
</Row>
<xsl:apply-templates select="logo" />
</Table>
</Worksheet>
</xsl:template>
<!-- a <logo> will turn into a <Row> -->
<xsl:template match="logo">
<Row>
<xsl:apply-templates select="field" />
</Row>
</xsl:template>
<!-- convenience: <field> and #key both turn into a <Cell> -->
<xsl:template match="field | field/#key">
<Cell>
<Data ss:Type="String">
<xsl:value-of select="."/>
</Data>
</Cell>
</xsl:template>
</xsl:stylesheet>
Your "repeating column names" problem roots in this expression:
<xsl:for-each select="*/*">
In your context, this selects any third level element in the document (literally all <field> nodes in all <logo>s), and makes a header row out of them. I replaced it with
<xsl:apply-templates select="logo[1]/field/#key" />
which makes a header row out of the first <logo> only.
If a certain column order is required (other than document order) or not all <field> nodes are in the same order for all <logo>s, things get more complex. Tell me if you need that.