Search code examples
xsltxslt-1.0muenchian-grouping

XSLT Group occurrences based on a value


I am usually good at XSLT programming but this has got me stumped. I think it is related to Muenchian Grouping but I am not sure as when I try the result is not what I want so I maybe wrong here. I have the following XML that I need to transform into the output XML. I am restricted to using XSLT 1.0.

Input XML

        <ns2:ListOfAssetMgmt-AssetXaQe xmlns:ns2="http://system.com/CustomUI">
            <ns3:AssetMgmt-AssetXaQe xmlns:ns3="http://www.system.com/xml/QE%20%%20%Points">
                <ns3:Id>1</ns3:Id>
                <ns3:Name2>Direction</ns3:Name2>
                <ns3:TextValue>Injection</ns3:TextValue>
            </ns3:AssetMgmt-AssetXaQe>
            <ns3:AssetMgmt-AssetXaQe xmlns:ns3="http://www.system.com/xml/QE%20%%20%Points">
                <ns3:Id>1</ns3:Id>
                <ns3:Name2>MeterType</ns3:Name2>
                <ns3:TextValue>YMR</ns3:TextValue>
            </ns3:AssetMgmt-AssetXaQe>
            <ns3:AssetMgmt-AssetXaQe xmlns:ns3="http://www.system.com/xml/QE%20%%20%Points">
                <ns3:Id>2</ns3:Id>
                <ns3:Name2>LoadProfile</ns3:Name2>
                <ns3:TextValue>Wind</ns3:TextValue>
            </ns3:AssetMgmt-AssetXaQe>
            <ns3:AssetMgmt-AssetXaQe xmlns:ns3="http://www.system.com/xml/QE%20%%20%Points">
                <ns3:Id>2</ns3:Id>
                <ns3:Name2>Direction</ns3:Name2>
                <ns3:TextValue>Rotation</ns3:TextValue>
            </ns3:AssetMgmt-AssetXaQe>
            <ns3:AssetMgmt-AssetXaQe xmlns:ns3="http://www.system.com/xml/QE%20%%20%Points">
                <ns3:Id>2</ns3:Id>
                <ns3:Name2>MeterType</ns3:Name2>
                <ns3:TextValue>ZMR</ns3:TextValue>
            </ns3:AssetMgmt-AssetXaQe>
       </ns3:ListOfAssetMgmt-AssetXaQe>

Output XML

  <Assets>
    <QuotingEngineServicePointAssets>
      <MeterConfigurationId>1</MeterConfigurationId>
      <Direction>Injection</Direction>
      <MeterType>YMR</MeterType>
      <TimeOfUse></TimeOfUse>
      <Segment></Segment>
      <Tension></Tension>
      <ReadingFrequency></ReadingFrequency>
      <BillingFrequency></BillingFrequency>
      <LoadProfile></LoadProfile>
      <LocalProduction></LocalProduction>
      <DistributionTariff></DistributionTariff>
    </QuotingEngineServicePointAssets>
    <QuotingEngineServicePointAssets>
      <MeterConfigurationId>2</MeterConfigurationId>
      <Direction>Rotation</Direction>
      <MeterType>ZMR</MeterType>
      <TimeOfUse></TimeOfUse>
      <Segment></Segment>
      <Tension></Tension>
      <ReadingFrequency></ReadingFrequency>
      <BillingFrequency></BillingFrequency>
      <LoadProfile>Wind</LoadProfile>
      <LocalProduction></LocalProduction>
      <DistributionTariff></DistributionTariff>
    </QuotingEngineServicePointAssets>
 </Assets>

The other tags like TimeOfUse, Segment etc. may also need to be populated if they exist on the input XML. If they don't exist on the Input XML then the element should not be output on the Output XML but I have shown them here for what could be output. Any assistance would be very much appreciated.


Solution

  • I agree with michael.hor257k in the statement that your question does not contain a textual description of what you want to achieve. Please always include that to help us help you.

    Nevertheless, I looked at your inputs and outputs and think I got an idea on the basic logic behind the examples.

    I would build the process in two stages:

    • First, calculate a unique list of all relevant IDs to have some basis to work on later. You could e.g. simply iterate over the lists and store that in a variable. I used two loops to keep the context clean:
    <!-- get distinct IDs, variable contains <id>1</id><id>2</id> -->
    <xsl:variable name="distinctValues">
        <xsl:for-each select="//AssetMgmt-AssetXaQe">
            <xsl:variable name="ctx1" select="." />
            <!-- filter out IDs that already occured to list them only once -->
            <xsl:if test="not(preceding-sibling::AssetMgmt-AssetXaQe[Id/text() = $ctx1/Id/text()])">
                <id><xsl:value-of select="$ctx1/Id/text()" /></id>
            </xsl:if>
        </xsl:for-each>
    </xsl:variable>
    
    • As @michael.hor257k pointed out, the following would be more efficient, but that's the solution I know less about:
    <xsl:key name="AssetMgmt-by-Id" match="AssetMgmt-AssetXaQe" use="Id" />
    
    <xsl:for-each select="AssetMgmt-AssetXaQe[count(. | key('AssetMgmt-by-Id', Id)[1]) = 1]">
        <!-- do whatever needs to be done per unique ID -->
    </for-each>
    
    • Second, you can iterate over this list, iterate over all the items with the corresponding ID and and dynamically generate tags (using xslt:element) named based on each Name2-tags content:
    <xsl:variable name="$ctx2" select="." />
    
    <xsl:for-each select="$distinctValues">
        <QuotingEngineServicePointAssets>
            <MeterConfigurationId>
                <xsl:value-of select="." />
            </MeterConfigurationId>
    
            <!-- iterate over all the entries that belong to this ID -->
            <xsl:for-each select="$ctx2//AssetMgmt-AssetXaQe[Id/text() = .]">
                <!-- dynamically generate tags based on the content of the Name2-Tag -->
                <xsl:element name="{./Name2}">
                    <xsl:value-of select="./TextValue">
                </xsl:element>
            </xsl:for-each>
        </QuotingEngineServicePointAssets>
    </xsl:for-each>
    

    That should do the trick. I left the trouble regarding namespaces for you, good luck!