XSLT 3-tier attribute grouping

OK, I KNOW that changes to this have been requested and answered; I read them all day, but I'm still stuck. So here goes:

I need to create a pivot list in HTML from some XML.

Given this XML:

<Root><!-- yes, I know I don't need a 'Root' element! Legacy code... --> <Plans> <Plan AreaID="1" UnitID="83"> <Part ID="9122" Name="foo" /> <Part ID="9126" Name="bar" /> </Plan> <Plan AreaID="1" UnitID="86"> <Part ID="8650" Name="baz" /> </Plan> <Plan AreaID="2" UnitID="26"> <Part ID="215" Name="quux" /> </Plan> <Plan AreaID="1" UnitID="95"> <Part ID="7350" Name="meh" /> </Plan> </Plans> </Root> 

I need to fix:

 <ol> <li>Area 1: <ol><!-- units in Area 1 --> <li>Unit 83: <ol> <li>Part 9122 (foo)</li> <li>Part 9126 (bar)</li> </ol> </li> <li>Unit 86: <ol> <li>Part 8650 (baz)</li> </ol> <li>Unit 95: <ol> <li>Part 7350 (meh)</li> </ol> </li> </ol><!-- /units in Area 1--> </li> <li>Area 2: <ol><!-- units in Area 2 --> <li>Unit 26: <ol> <li>Part 215 (quux)</li> </ol> </li> </ol><!-- /units in Area 2--> </li> </ol> 

I have an external grouping - I get the top-level list items for Region 1 and 2. But I can’t get the sequence of Units in the Regions - I either don’t get any output, or I repeat the same value, I didn’t even reach the Part level : - (

I worked on the stylesheet as follows:

 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" <xsl:output method="html" indent="yes"/> <xsl:key name="kAreaID" match="Plan" use="@AreaID" /> <xsl:key name="kUnitID" match="Plan" use="@UnitID" /> <xsl:template match="/Root/Plans"> <html><head><title>test grouping</title></head> <body> <ol> <xsl:for-each select="./Plan[generate-id(.) = generate-id( key( 'kAreaID', @AreaID )[1] )]" > <xsl:sort order="ascending" select="./@AreaID" /> <li>Area <xsl:value-of select="@AreaID"/>: <ol> <xsl:for-each select="key( 'kUnitID', @UnitID )"> <li>Unit <xsl:value-of select="@UnitID"/>: <ol> <li>(Parts go here...)</li> </ol> </li> </xsl:for-each> </ol> </li> </xsl:for-each> </ol> </body> </html> </xsl:template> </xsl:stylesheet> 

Any help is much appreciated!

+4
source share
4 answers

The following is the solution of Muenchian grouping.

Based on the original XML that you provided, I thought that grouping by AreaID would be enough, but it turns out that a second grouping by UnitID is also required.

Here is my modified XSLT 1.0 solution. This is not much more complicated than the original solution:

 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" > <xsl:key name="kPlanByArea" match="Plan" use="@AreaID" /> <xsl:key name="kPlanByAreaAndUnit" match="Plan" use="concat(@AreaID, ',', @UnitID)" /> <xsl:template match="/"> <xsl:apply-templates select="Root/Plans" /> </xsl:template> <!-- main template --> <xsl:template match="Plans"> <ol> <!-- group by '{@AreaID}' (note the template mode!) --> <xsl:apply-templates mode="area-group" select=" Plan[ generate-id() = generate-id( key('kPlanByArea', @AreaID)[1] ) ] "> <xsl:sort select="@AreaID" data-type="number" /> </xsl:apply-templates> </ol> </xsl:template> <!-- template to output each '{@AreaID}' group --> <xsl:template match="Plan" mode="area-group"> <li> <xsl:value-of select="concat('Area ', @AreaID)" /> <ol> <!-- group by '{@AreaID},{@UnitID}' --> <xsl:apply-templates mode="unit-group" select=" key('kPlanByArea', @AreaID)[ generate-id() = generate-id( key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1] ) ] "> <xsl:sort select="@UnitID" data-type="number" /> </xsl:apply-templates> </ol> </li> </xsl:template> <!-- template to output each '{@AreaID},{@UnitID}' group --> <xsl:template match="Plan" mode="unit-group"> <li> <xsl:value-of select="concat('Unit ', @UnitID)" /> <ol> <xsl:apply-templates select=" key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))/Part "> <xsl:sort select="@UnitID" data-type="number" /> </xsl:apply-templates> </ol> </li> </xsl:template> <!-- template to output Parts into a list --> <xsl:template match="Part"> <li> <xsl:value-of select="concat('Part ', @ID, ' (', @Name ,')')" /> </li> </xsl:template> </xsl:stylesheet> 

Since your XML is missing, I added a UnitID to group:

 <Plan AreaID="1" UnitID="86"> <Part ID="8651" Name="zzz" /> </Plan> 

And here is the conclusion:

 <ol> <li>Area 1 <ol> <li>Unit 83 <ol> <li>Part 9122 (foo)</li> <li>Part 9126 (bar)</li> </ol> </li> <li>Unit 86 <ol> <li>Part 8650 (baz)</li> <li>Part 8651 (zzz)</li> </ol> </li> <li>Unit 95 <ol> <li>Part 7350 (meh)</li> </ol> </li> </ol> </li> <li>Area 2 <ol> <li>Unit 26 <ol> <li>Part 215 (quux)</li> </ol> </li> </ol> </li> </ol> 

Since it seems to you very difficult with the XSL key, here is my attempt to explain:

An <xsl:key> absolutely equivalent to the associative array (map, hash, as you call it), known to many programming languages. It:

 <xsl:key name="kPlanByAreaAndUnit" match="Plan" use="concat(@AreaID, ',', @UnitID)" /> 

creates a data structure that can be expressed in JavaScript as follows:

 var kPlanByAreaAndUnit = { "1,83": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="83"'], "1,86": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="86"'], /* ... */ "1,95": ['array of all <Plan> nodes with @AreaID="1" and @UnitID="95"'] }; 

The data structure access function is called key() . So this is an XPath expression:

 key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID)) 

is the logical equivalent (in JavaScript again):

 kPlanByAreaAndUnit[this.AreaID + ',' + this.UnitID]; 

returns an array (a node-set, or rather) of all nodes corresponding to the given key string (the key is always a string). This node-set can be used like any other node-set in XSLT, i.e. The one you get through the "traditional" XPath. This means that you can apply conditions (predicates) to it:

 <!-- first node only... --> key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1] <!-- nodes that have <Part> children only... --> key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[Part] 

or use it as a basis for XPath navigation:

 <!-- the actual <Part> children of matched nodes... --> key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))/Part 

etc. It also means that we can use it as the "select" expression for <xsl:apply-templates> , and we can use it as a base for grouping. This brings us to the core of the aforementioned stylesheet (if you wrapped your head around this, you also understood the rest of the solution):

 key('kPlanByArea', @AreaID)[ generate-id() = generate-id( key('kPlanByAreaAndUnit', concat(@AreaID, ',', @UnitID))[1] ) ] 

In JavaScript, again this can be expressed as:

 // the result will be a node-set, so we prepare an array var selectedNodes = []; // "key('kPlanByArea', @AreaID)" var nodeSet = kPlanByArea[this.AreaID]; // "[...]" - the [] actually triggers a loop that applies // the predicate expression to all nodes in the set, so we do: for (var i = 0; i < nodeSet.length; i++) { // use the current node for any calculations var c = nodeSet[i]; if ( // if the current node === the *first* node in kPlanByAreaAndUnit... generateId(c) == generateId(kPlanByAreaAndUnit[c.AreaID + ',' + c.UnitID][0]) ) { // ...include it in the resulting selection selectedNodes.push(c) } } 

After the expression is done, only those nodes are selected that are the first ones with the given combination “AreaID, UnitID” - effectively we grouped them by their combination “AreaID, UnitID”.

Applying a template to this node -set only causes each combination to appear once. My <xsl:template match="Plan" mode="unit-group"> then again gets a complete list to achieve full output for each group.

I hope that using JavaScript to explain the concept was a useful idea.

+18
source

I do not think you need to use the kUnitID key. Instead, replace the following line ...

 <xsl:for-each select="key( 'kUnitID', @UnitID )"> 

.. with this line instead, which should intersect all parts corresponding to the current AreaID

 <xsl:for-each select="key( 'kAreaID', @AreaID )"> 

And in this loop, for your code (Parts here here ...), you can simply iterate over the parts

 <xsl:for-each select="Part"> <li>Part (<xsl:value-of select="@ID" />)</li> </xsl:for-each> 
+1
source

Ok, I have given up the keys and Muenchian grouping for now. I almost do not understand this, and hacking it did not bring the desired results. I understand recursion, therefore, and so I went with this recursive approach, which gives the desired result. I found it at http://www.biglist.com/lists/xsl-list/archives/200412/msg00865.html

The discussion thread warns that performance suffers from Muenchian's big input and approach, and the solution below is verbose and repetitive (I could do refactoring to make it smaller and harder to understand ;-), but 1) it actually works for me, and 2) for my current problem, the input sets are quite small, no more than a dozen lower-level nodes.

 <?xml version="1.0" encoding="UTF-8"?> <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <!-- recursive grouping http://www.biglist.com/lists/xsl-list/archives/200412/msg00865.html --> <xsl:template match="//Plans"> <html> <head> <title>test grouping</title> </head> <body> <ol> <xsl:call-template name="PlanGrouping"> <xsl:with-param name="list" select="Plan"/> </xsl:call-template> </ol> </body> </html> </xsl:template> <xsl:template name="PlanGrouping"> <xsl:param name="list"/> <!-- Selecting the first Area ID as group identifier and the group itself--> <xsl:variable name="group-identifier" select="$list[1]/@AreaID"/> <xsl:variable name="group" select="$list[@AreaID = $group-identifier]"/> <!-- Do some work for the group --> <li> Area <xsl:value-of select="$group-identifier"/>: <ol> <xsl:call-template name="AreaGrouping"> <xsl:with-param name="list" select="$list[(@AreaID = $group-identifier)]"/> </xsl:call-template> </ol> </li> <!-- If there are other groups left, calls itself --> <xsl:if test="count($list)>count($group)"> <xsl:call-template name="PlanGrouping"> <xsl:with-param name="list" select="$list[not(@AreaID = $group-identifier)]"/> </xsl:call-template> </xsl:if> </xsl:template> <xsl:template name="AreaGrouping"> <xsl:param name="list"/> <!-- Selecting the first Unit ID as group identifier and the group itself--> <xsl:variable name="group-identifier" select="$list[1]/@UnitID"/> <xsl:variable name="group" select="$list[@UnitID = $group-identifier]"/> <!-- Do some work for the group --> <li> Unit <xsl:value-of select="$group-identifier"/>: <ol> <xsl:call-template name="Parts"> <xsl:with-param name="list" select="$list[(@UnitID = $group-identifier)]"/> </xsl:call-template> </ol> </li> <!-- If there are other groups left, calls itself --> <xsl:if test="count($list)>count($group)"> <xsl:call-template name="AreaGrouping"> <xsl:with-param name="list" select="$list[not(@UnitID = $group-identifier)]"/> </xsl:call-template> </xsl:if> </xsl:template> <xsl:template name="Parts"> <xsl:param name="list"/> <xsl:for-each select="$list/Part"> <li> Part <xsl:value-of select="@ID"/> (<xsl:value-of select="@Name"/>) </li> </xsl:for-each> </xsl:template> </xsl:stylesheet> 
+1
source

This does what you want, but with recursion, not with grouping. Sorry, I'm still learning how to use grouping:

 <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"> <xsl:output method="html" indent="yes"/> <xsl:key name="kAreaID" match="Plan" use="@AreaID" /> <xsl:key name="kUnitID" match="Plan" use="@UnitID" /> <xsl:template match="/Root/Plans"> <html> <head> <title>test grouping</title> </head> <body> <ol> <xsl:for-each select="./Plan[generate-id(.) = generate-id( key( 'kAreaID', @AreaID )[1] )]" > <xsl:sort order="ascending" select="./@AreaID" /> <xsl:variable name="curArea" select="@AreaID"/> <li> Area <xsl:value-of select="$curArea"/>: <ol> <xsl:for-each select="ancestor::Root/Plans/Plan[@AreaID = $curArea]"> <xsl:variable name="curUnit" select="@UnitID"/> <li> Unit <xsl:value-of select="$curUnit"/>: <ol> <xsl:for-each select="ancestor::Root/Plans/Plan[@AreaID = $curArea and @UnitID = $curUnit]/Part"> <li> Part <xsl:value-of select="concat(@ID, ' (', @Name, ')')"/> </li> </xsl:for-each> </ol> </li> </xsl:for-each> </ol> </li> </xsl:for-each> </ol> </body> </html> </xsl:template> </xsl:stylesheet> 
0
source

Source: https://habr.com/ru/post/1285716/


All Articles