0

I am trying to insert a container when the count of an attribute = X and a 2nd group based on the value of an attribute. The two attributes are not related.

Using XSLT- V1

I would like to first group based on the value of an attribute. Ie. anytime ID=01 would create a group. I would then like to insert a new attribute/ container when the count = X.

I am able to group based on attribute value, but not sure how to determine count and add a new container.

I have XML that looks like the following:

<Items>
  <Details>
    <ID>01</ID>
    <Name>Name for 01</Name>
    <Owner>User1</Owner>
    <Rev>01-A</Rev>
    <Rev_Owner>User2</Rev_Owner>
    <Rev_Code>US</Rev_Code>
  </Details>
  <Details>
    <ID>01</ID>
    <Name>Name for 01</Name>
    <Owner>User1</Owner>
    <Rev>01-B</Rev>
    <Rev_Owner>User3</Rev_Owner>
    <Rev_Code>CN</Rev_Code>
  </Details>
  <Details>
    <ID>02</ID>
    <Name>Name for 02</Name>
    <Owner>User1</Owner>
    <Rev>02-A</Rev>
    <Rev_Owner>User4</Rev_Owner>
    <Rev_Code>MX</Rev_Code>
  </Details>
  <Details>
    <ID>03</ID>
    <Name>Name for 03</Name>
    <Owner>User1</Owner>
    <Rev>03-A</Rev>
    <Rev_Owner>User5</Rev_Owner>
    <Rev_Code>CA</Rev_Code>
  </Details>
  <Details>
    <ID>02</ID>
    <Name>Name for 02</Name>
    <Owner>User1</Owner>
    <Rev>02-B</Rev>
    <Rev_Owner>User5</Rev_Owner>
    <Rev_Code>AU</Rev_Code>
  </Details>
  <Details>
    <ID>01</ID>
    <Name>Name for 01</Name>
    <Owner>User1</Owner>
    <Rev>02-C</Rev>
    <Rev_Owner>User5</Rev_Owner>
    <Rev_Code>JP</Rev_Code>
  </Details>
</Items>

I have below XSL that creates expected group for item ID


xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="no" indent="yes"/>

  <xsl:key name="ItemGroup" match="Details" use="ID"/>

  <xsl:template match="/*">
    <Items>
      <xsl:apply-templates/>
    </Items>
  </xsl:template>

  <xsl:template match="Details[generate-id()=generate-id(key('ItemGroup',ID)[1])]">
    <ItemID name="{ID}">
      <xsl:copy-of select="key('ItemGroup',ID)"/>
    </ItemID>
  </xsl:template>
  <xsl:template match="Details[not(generate-id()=generate-id(key('ItemGroup',ID)[1]))]"/>
</xsl:stylesheet>

Output for aboe XSL:

<Items>
  <ItemID name="01">
      <Details>
         <ID>01</ID>
         <Name>Name for 01</Name>
         <Owner>User1</Owner>
         <Rev>01-A</Rev>
         <Rev_Owner>User2</Rev_Owner>
         <Rev_Code>US</Rev_Code>
      </Details>
      <Details>
         <ID>01</ID>
         <Name>Name for 01</Name>
         <Owner>User1</Owner>
         <Rev>01-B</Rev>
         <Rev_Owner>User3</Rev_Owner>
         <Rev_Code>CN</Rev_Code>
      </Details>
      <Details>
         <ID>01</ID>
         <Name>Name for 01</Name>
         <Owner>User1</Owner>
         <Rev>02-C</Rev>
         <Rev_Owner>User5</Rev_Owner>
         <Rev_Code>JP</Rev_Code>
      </Details>
   </ItemID>

  <ItemID name="02">
      <Details>
         <ID>02</ID>
         <Name>Name for 02</Name>
         <Owner>User1</Owner>
         <Rev>02-A</Rev>
         <Rev_Owner>User4</Rev_Owner>
         <Rev_Code>MX</Rev_Code>
      </Details>
      <Details>
         <ID>02</ID>
         <Name>Name for 02</Name>
         <Owner>User1</Owner>
         <Rev>02-B</Rev>
         <Rev_Owner>User5</Rev_Owner>
         <Rev_Code>AU</Rev_Code>
      </Details>
   </ItemID>
  <ItemID name="03">
      <Details>
         <ID>03</ID>
         <Name>Name for 03</Name>
         <Owner>User1</Owner>
         <Rev>03-A</Rev>
         <Rev_Owner>User5</Rev_Owner>
         <Rev_Code>CA</Rev_Code>
      </Details>
   </ItemID>


</Items>

I would now like to add a variable for count of "details" = 3 for example (it would really be somewhere between 1,000- 5,000) and then expect below output

<Items>
  <Split>
      <ItemID name="01">
      <Details>
        <ID>01</ID>
        <Name>Name for 01</Name>
        <Owner>User1</Owner>
        <Rev>01-A</Rev>
        <Rev_Owner>User2</Rev_Owner>
        <Rev_Code>US</Rev_Code>
      </Details>
      <Details>
        <ID>01</ID>
        <Name>Name for 01</Name>
        <Owner>User1</Owner>
        <Rev>01-B</Rev>
        <Rev_Owner>User3</Rev_Owner>
        <Rev_Code>CN</Rev_Code>
      </Details>
      <Details>
        <ID>01</ID>
        <Name>Name for 01</Name>
        <Owner>User1</Owner>
        <Rev>02-C</Rev>
        <Rev_Owner>User5</Rev_Owner>
        <Rev_Code>JP</Rev_Code>
      </Details>
      </ItemID>
  </Split>
  <Split>
  <ItemID name="02">
    <Details>
      <ID>02</ID>
      <Name>Name for 02</Name>
      <Owner>User1</Owner>
      <Rev>02-A</Rev>
      <Rev_Owner>User4</Rev_Owner>
      <Rev_Code>MX</Rev_Code>
    </Details>
    <Details>
      <ID>02</ID>
      <Name>Name for 02</Name>
      <Owner>User1</Owner>
      <Rev>02-B</Rev>
      <Rev_Owner>User5</Rev_Owner>
      <Rev_Code>AU</Rev_Code>
    </Details>
  </ItemID>
  <ItemID name="03">
    <Details>
      <ID>03</ID>
      <Name>Name for 03</Name>
      <Owner>User1</Owner>
      <Rev>03-A</Rev>
      <Rev_Owner>User5</Rev_Owner>
      <Rev_Code>CA</Rev_Code>
    </Details>
  </ItemID>
  </Split>
  <Split>
     continued....

</Items>

many thanks!

Chris
  • 1
  • 1
  • Possible duplicate of [Xpath: how to select an option based on its text not value property?](https://stackoverflow.com/questions/1549256/xpath-how-to-select-an-option-based-on-its-text-not-value-property) – ceving Apr 10 '19 at 07:23
  • 1
    Can you please edit your question to show the XSLT you have got so far. Thank you! – Tim C Apr 10 '19 at 07:45
  • In a first step, do the `group-by="ID"` in a variable and then in a second step use positional grouping on the result of the first step. If you need further help then please state which XSLT processor and/or which XSLT version you use/can use. – Martin Honnen Apr 10 '19 at 08:48

2 Answers2

0

Assuming at least XSLT 2 you can use two grouping steps, the first is a simple group-by on the ID child element, the second then does positional grouping the result of the first step:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:xs="http://www.w3.org/2001/XMLSchema"
    exclude-result-prefixes="#all"
    version="3.0">

   <xsl:param name="split-size" as="xs:integer" select="3"/>

   <xsl:output indent="yes"/>

   <xsl:template match="Items">
       <xsl:copy>
           <xsl:variable name="groups">
               <xsl:for-each-group select="Details" group-by="ID">
                   <ItemID name="{current-grouping-key()}">
                       <xsl:copy-of select="current-group()"/>
                   </ItemID>
               </xsl:for-each-group>
           </xsl:variable>
           <xsl:for-each-group select="$groups/ItemID/Details" group-adjacent="(position() - 1) idiv $split-size">
               <split>
                   <xsl:copy-of select="current-group()/.."/>
               </split>
           </xsl:for-each-group>
       </xsl:copy>
   </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bFN1y9n

With XSLT 1, to perform a two step transformation, you need to use exsl:node-set or similar (depending on the particular XSLT processor used) to convert the result tree fragment from the first grouping step back into a node set so that you can select and navigate it; furthermore the positional "grouping" or splitting requires some selection along the siblings axis:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
    xmlns:exsl="http://exslt.org/common"
    exclude-result-prefixes="exsl"
    version="1.0">

   <xsl:param name="split-size" select="3"/>

   <xsl:key name="group" match="Details" use="ID"/>

   <xsl:output indent="yes"/>
   <xsl:strip-space elements="*"/>

   <xsl:template match="Items">
       <xsl:copy>
           <xsl:variable name="groups">
               <xsl:for-each select="Details[generate-id() = generate-id(key('group', ID)[1])]">
                   <ItemID name="{ID}">
                       <xsl:copy-of select="key('group', ID)"/>
                   </ItemID>
               </xsl:for-each>
           </xsl:variable>
           <xsl:variable name="Details" select="exsl:node-set($groups)/ItemID/Details"/>
           <xsl:for-each select="$Details[position() mod $split-size = 1]">
               <split>
                   <xsl:copy-of select="(. | (following-sibling::Details | ../following-sibling::ItemID/Details)[position() &lt; $split-size])/.."/>
               </split>
           </xsl:for-each>
       </xsl:copy>
   </xsl:template>

</xsl:stylesheet>

https://xsltfiddle.liberty-development.net/bFN1y9n/2

Martin Honnen
  • 160,499
  • 6
  • 90
  • 110
0

In addition to your existing key, I think you need another key (which would be used first) to group the Details by whether they have ID = 01 or not

<xsl:key name="ItemGroupOne" match="Details" use="ID = '01'"/>

Try this XSLT

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="no" indent="yes"/>
  <xsl:strip-space elements="*" />

  <xsl:key name="ItemGroupOne" match="Details" use="ID = '01'"/>
  <xsl:key name="ItemGroup" match="Details" use="ID"/>

  <xsl:template match="/*">
    <Items>
      <xsl:apply-templates/>
    </Items>
  </xsl:template>

  <xsl:template match="Details[generate-id()=generate-id(key('ItemGroupOne',ID = '01')[1])]">
    <Split>
      <xsl:apply-templates select="key('ItemGroupOne',ID = '01')" mode="items" />
    </Split>
  </xsl:template>

  <xsl:template match="Details[generate-id()=generate-id(key('ItemGroup',ID)[1])]" mode="items">
    <ItemID name="{ID}">
      <xsl:copy-of select="key('ItemGroup',ID)"/>
    </ItemID>    
  </xsl:template>

  <xsl:template match="Details"/>

  <xsl:template match="Details" mode="items"/>
</xsl:stylesheet>

The use of mode here is to avoid template conflicts.

Also note, for the final templates that ignore Details, you don't need the not logic in the condition here, as templates that match an element with a condition will have a higher priority than ones than just match the element with no condition.

Try it here: http://xsltfiddle.liberty-development.net/gWvjQfr

Or, maybe write it like this, if you want to remove the use of the "mode" and the templates that ignore elements...

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="no" indent="yes"/>
  <xsl:strip-space elements="*" />

  <xsl:key name="ItemGroupOne" match="Details" use="ID = '01'"/>
  <xsl:key name="ItemGroup" match="Details" use="ID"/>

  <xsl:template match="/*">
    <Items>
      <xsl:apply-templates select="Details[generate-id()=generate-id(key('ItemGroupOne',ID = '01')[1])]" />
    </Items>
  </xsl:template>

  <xsl:template match="Details">
    <Split>
      <xsl:for-each select="key('ItemGroupOne',ID = '01')[generate-id()=generate-id(key('ItemGroup',ID)[1])]">
        <ItemID name="{ID}">
          <xsl:copy-of select="key('ItemGroup',ID)"/>
        </ItemID>    
      </xsl:for-each>
    </Split>
  </xsl:template>
</xsl:stylesheet>
Tim C
  • 70,053
  • 14
  • 74
  • 93