Reputation: 923
I have an XML document with similar content to
<?xml version="1.0" encoding="UTF-8"?>
<Mod>
<Input1>
<Name>BackInput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Input1>
<Input2>
<Name>NeutralInput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Input2>
<Input3>
<Name>LightingInput</Name>
<Transform>
<Subsystem>Lighting</Subsystem>
</Transform>
</Input3>
<Output1>
<Name>BackOutput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Output1>
<Output2>
<Name>NeutralOutput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Output2>
<Output3>
<Name>LightingOutput</Name>
<Transform>
<Subsystem>Lighting</Subsystem>
</Transform>
</Output3>
<VariableData>
<Threshold name="LightingMax">
<Component>Lighting</Component>
</Threshold>
</VariableData>
</Mod>
I would like to get unique Subsystems, and all unique preceding Name text. Sorted by the Subsystem and finally by the Name. With an expected output of
Lighting
LightingInput
LightingOutput
Transmission
BackInput
BackOutput
DriveInput
DriveOutput
NeutralInput
NeutralOutput
This is mocked up data. I can't seem to get my head around how to only output unique data items.
This is the XSLT I am using now. Feel free to comment on any aspect of the XSLT as this is the first time I am using it.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:variable name="newline" select="' '"/>
<xsl:variable name="tab" select="'	'"/>
<xsl:template match="/">
<xsl:copy>
<xsl:apply-templates select="//Subsystem|//Component">
<xsl:sort select="."/>
<xsl:sort select="../@name"/>
<xsl:sort select="../preceding-sibling::Name"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<xsl:template match="//Subsystem|//Component">
<xsl:value-of select="concat($newline, .)"/>
<xsl:if test="../preceding-sibling::Name">
<xsl:value-of select="concat($newline, $tab, ../preceding-sibling::Name)"/>
</xsl:if>
<xsl:if test="../@name">
<xsl:value-of select="concat($newline, $tab, ../@name)"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Output when the above XSLT is applied to the XML (note, I am getting a newline on the first line when using this, it seems like its because of my XSLT but if I move the first xsl:value-of
I don't get the output I am expecting)
<newline>
Lighting
LightingInput
Lighting
LightingOutput
Lighting
LightingMax
Transmission
BackInput
Transmission
BackOutput
Transmission
NeutralInput
Transmission
NeutralOutput
Upvotes: 2
Views: 81
Reputation: 107767
Consider the Muenchian Grouping to index document by specific values in XSLT 1.0 and loop through corresponding items such as grandparent names: ../../Name
:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" omit-xml-declaration="yes"/>
<xsl:key name="subid" match="Subsystem" use="."/>
<xsl:template match ="/Mod">
<xsl:apply-templates select="descendant::Subsystem[generate-id() =
generate-id(key('subid', .)[1])]">
<xsl:sort select="."/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match ="Subsystem">
<xsl:value-of select="."/><xsl:text>
</xsl:text><!-- LINE BREAK -->
<xsl:for-each select="key('subid', .)">
<xsl:sort select="../../Name"/>
<xsl:text>	</xsl:text> <!-- TAB -->
<xsl:value-of select="../../Name"/>
<xsl:text>
</xsl:text> <!-- LINE BREAK -->
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Upvotes: 0
Reputation: 52888
Feel free to comment on any aspect of the XSLT as this is the first time I am using it.
Here are a few comments on your existing stylesheet...
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:variable name="newline" select="' '"/>
<xsl:variable name="tab" select="'	'"/>
<xsl:template match="/">
<!--When your output method is text, you don't usually need to use xsl:copy.
Also, using xsl:copy when the context is the document node doesn't do anything
helpful.-->
<xsl:copy>
<xsl:apply-templates select="//Subsystem|//Component">
<xsl:sort select="."/>
<xsl:sort select="../@name"/>
<xsl:sort select="../preceding-sibling::Name"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>
<!--You don't need the "//" in the match pattern. Just use "Subsystem|Component".
See https://www.w3.org/TR/xslt-10/#patterns for more info.-->
<xsl:template match="//Subsystem|//Component">
<!--This outputs a newline for every Subsystem or Component. Instead, just output
a newline if the position() is greater than 1. That way you won't have an extra
newline at the beginning of your output.-->
<xsl:value-of select="concat($newline, .)"/>
<xsl:if test="../preceding-sibling::Name">
<xsl:value-of select="concat($newline, $tab, ../preceding-sibling::Name)"/>
</xsl:if>
<xsl:if test="../@name">
<xsl:value-of select="concat($newline, $tab, ../@name)"/>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
Since you're using XSLT 1.0, what I would do is use Muenchian Grouping.
First group the Subsystem
and Component
elements by their values (first "level1" xsl:key in the example). You're just going to output the value of the first node in this key. This is what's going to give you the unique list.
Then group the Name
elements and name
attributes by the value of the Component
or Subsystem
(second "names" xsl:key in the example). Getting the Component or Subsystem is a little tricky since you're selecting either an element or an attribute and they're at different levels in the tree. To do it, we first need to go back up the tree (..
) to the parent and then back down the tree (//
) to the Component or Subsystem.
Take some time to check out the Muenchian Grouping page linked above; it will help you to understand the grouping parts of my example.
Example...
XML Input
<Mod>
<Input1>
<Name>BackInput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Input1>
<Input2>
<Name>NeutralInput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Input2>
<Input3>
<Name>LightingInput</Name>
<Transform>
<Subsystem>Lighting</Subsystem>
</Transform>
</Input3>
<Output1>
<Name>BackOutput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Output1>
<Output2>
<Name>NeutralOutput</Name>
<Transform>
<Subsystem>Transmission</Subsystem>
</Transform>
</Output2>
<Output3>
<Name>LightingOutput</Name>
<Transform>
<Subsystem>Lighting</Subsystem>
</Transform>
</Output3>
<VariableData>
<Threshold name="LightingMax">
<Component>Lighting</Component>
</Threshold>
</VariableData>
</Mod>
XSLT 1.0
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:key name="level1"
match="Subsystem|Component"
use="normalize-space()"/>
<xsl:key name="names"
match="Name|*[@name]/@name"
use="normalize-space(..//*[self::Subsystem or self::Component])"/>
<xsl:template match="/Mod">
<xsl:for-each
select=".//*[self::Subsystem or self::Component][
count(.|key('level1',normalize-space())[1])=1]">
<xsl:sort select="normalize-space()"/>
<xsl:if test="position() > 1">
<xsl:value-of select="'
'"/>
</xsl:if>
<xsl:value-of select="normalize-space()"/>
<xsl:for-each select="key('names',normalize-space())">
<xsl:sort select="normalize-space()"/>
<xsl:value-of select="concat('
	',normalize-space())"/>
</xsl:for-each>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Output
Lighting
LightingInput
LightingMax
LightingOutput
Transmission
BackInput
BackOutput
NeutralInput
NeutralOutput
Fiddle: http://xsltfiddle.liberty-development.net/6qVRKvQ
Upvotes: 1