RJ7
RJ7

Reputation: 923

Group output in XSLT but only show unique headings?

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="'&#10;'"/>
<xsl:variable name="tab" select="'&#9;'"/>
<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

Answers (2)

Parfait
Parfait

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>&#xa;</xsl:text><!-- LINE BREAK -->

        <xsl:for-each select="key('subid', .)">
            <xsl:sort select="../../Name"/>
            <xsl:text>&#009;</xsl:text>            <!--    TAB     -->
            <xsl:value-of select="../../Name"/>
            <xsl:text>&#xa;</xsl:text>             <!-- LINE BREAK -->
        </xsl:for-each>
   </xsl:template>

</xsl:stylesheet>

XSLTransform.Net DEMO

Upvotes: 0

Daniel Haley
Daniel Haley

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="'&#10;'"/>
  <xsl:variable name="tab" select="'&#9;'"/>

  <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="'&#xA;'"/>
      </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('&#xA;&#x9;',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

Related Questions