Forum How do I...?

How to generate a list of sequential id's in nested loops

mark_anderson_us
I'm using XSL1.0. My editor/debugger is OxygenXML with Saxon (OxygenXML can't debug with MSXML) and it will deployed to work with a (very limited) 3rd party app that only uses MSXML and cannot use any scripting. This means I can't use a variable containing a nodeset if I want to be able to debug.

The problem could *probably* be expressed as how to sequentially number output of the following -

<xsl:for-each select="node1">
<xsl:variable name="current_ID" select="ID">
<xsl:for-each select="sub_node1">
<xsl:value-of select="../ID"/>-<xsl:value-of select="Sub_ID"/>
</xsl:for-each>
</xsl:for-each>

understanding that I cannot simply use this in my scenario:


<xsl:for-each select="node1/sub_node1">
<xsl:value-of select="position()"/>
</xsl:for-each>

Below is a manufactured example that shows the problem I'm trying to solve (mde HTML ouptut for simplicity here) as part of a much larger XSL/XML combo. I basically need to create manufacturing instructions. All nodes in the XML with the exception of products/versions (by qty) are in the correct order and I cannot change it. I need to generate the same set of sequential numbers from 3 different XSL's. My current context will always be shipments/deliveries/delivery_products (i.e. my XSL has to process the nodes in the seq shown). I need to produce a list of products sorted by version qty and their deliveries. Each row should have a sequential no (1-4) in example below

<shipments>
<product>
<name>Product 1</name>
<prod_id>P1</prod_id>
<version>
<version_id>P1_V1</version_id>
<qty>8800</qty>
</version>
<version>
<version_id>P1_V2</version_id>
<qty>1100</qty>
</version>
<version>
<version_id>P1_V3</version_id>
<qty>100</qty>
</version>
</product>
<product>
<name>Product 2</name>
<prod_id>P2</prod_id>
<version>
<version_id>P2_V1</version_id>
<qty>5000</qty>
</version>
<version>
<version_id>P2_V2</version_id>
<qty>5000</qty>
</version>
<version>
<version_id>P2_V3</version_id>
<qty>2000</qty>
</version>
</product>
<deliveries>
<del_id>1</del_id>
<destination>Miami</destination>
<delivery_products>
<version_id>P1_V1</version_id>
<qty>8000</qty>
</delivery_products>
<delivery_products>
<version_id>P2_V1</version_id>
<qty>5000</qty>
</delivery_products>
</deliveries>
<deliveries>
<del_id>2</del_id>
<destination>New York</destination>
<delivery_products>
<version_id>P1_V1</version_id>
<qty>800</qty>
</delivery_products>
<delivery_products>
<version_id>P2_V2</version_id>
<qty>1000</qty>
</delivery_products>
</deliveries>
</shipments>

Expected output is below. Note seq # starts from 1 and counts up to 4

<table>
<thead>
<tr>
<td class="col_head">
Seq
</td>
<td class="col_head">
Version
</td>
<td class="col_head">
Destination
</td>
<td class="col_head">
Qty
</td>
</tr>
</thead>
<tr>
<td colspan="4" class="rev_heading">Product 1</td>
</tr>
<tr>
<td>1</td>
<td>P1_V1</td>
<td>Miami</td>
<td>8000</td>
</tr>
<tr>
<td>2</td>
<td>P1_V1</td>
<td>New York</td>
<td>800</td>
</tr>
<tr>
<td colspan="4" class="rev_heading">Product 2</td>
</tr>
<tr>
<td>3</td>
<td>P2_V1</td>
<td>Miami</td>
<td>5000</td>
</tr>
<tr>
<td>4</td>
<td>P2_V2</td>
<td>New York</td>
<td>5000</td>
</tr>
</table>

Here's my XSL so far (just stuck a position() in for a place holder for the seq #)

<?xml version="1.0" encoding="UTF-8"?>

<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:output method="html" doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN" doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>

<xsl:template match="shipments">
<html>
<head>
<title>Seq Test</title>

<style type="text/css">
table {border: 1px solid black; border-collapse: collapse;}
td {border: 1px solid black; padding: 1px 5px 1px 5px;}
.col_head {font-weight: 600;}
.rev_heading {color: red; text-align: center; padding-top: 15px;}
</style>
</head>
<body>

<table>
<thead>
<tr>
<!-- SEQ# -->
<td class="col_head">
Seq
</td>
<!-- Imprint/Version -->
<td class="col_head">
Version
</td>
<!-- Ship to -->
<td class="col_head">
Destination
</td>
<!-- Qty -->
<td class="col_head">
Qty
</td>
</tr>
</thead>
<xsl:for-each select="product">
<xsl:sort data-type="number" select="qty"/>
<xsl:for-each select="version">
<xsl:variable name="curr_version" select="version_id"/>
<xsl:if test="position() = 1">
<tr>
<td colspan="4" class="rev_heading">
<xsl:value-of select="../name"/>
</td>
</tr>
</xsl:if>
<xsl:for-each select="../../deliveries/delivery_products[version_id = $curr_version]">
<tr >
<!-- SEQ# -->
<td>
<xsl:value-of select="position()"/>
</td>
<!-- Version -->
<td>
<xsl:value-of select="version_id"/>
</td>
<!-- Ship to -->
<td>
<xsl:value-of select="../destination"/>
</td>
<!-- QTY -->
<td>
<xsl:value-of select="qty"/>
</td>
</tr>
</xsl:for-each>
</xsl:for-each>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>

</xsl:stylesheet>



mikeday
Why not use CSS counters instead?
mark_anderson_us
Hi Mike

wasn't even aware they existed. Unfortunately in my case I can't as the 3 XSL's are entirely different. Some roll-up items to summary levels (shipping lists) and others print one page per pallet (so there will be a lot more - i.e. several pages with same seq no). Will read up on CSS counters for future projects though

Regards

Mark
mikeday
Okay, how about using xsl:number then? :)
mark_anderson_us
I tried that, but couldn't get it to work (tried a bunch of different values for attributes). The problem is I can't get a single node-set (as far as I can tell). I get one node-set and then within that have to get a second node-set.

Regards

Mark
mikeday
Okay maybe you can't use xsl:number, because the indirection and the sorting mean that any numbering based on the element position in the original input document will not be correct.

My strong recommendation would be to use a better XSLT processor, so you can use EXSLT and manipulate nodesets much more conveniently. Or, preprocess the input with one transform to get all the data in the right order, and then process it with a second transform to add the numbering. Much easier.

Otherwise, if you really truly can't do that for a very good reason, you could write recursive templates instead of using xsl:for-each.

By the way, this sort doesn't look right:
<xsl:sort data-type="number" select="qty"/>

Shouldn't it be "version/qty"? Or perhaps "sum(version/qty)"?
mark_anderson_us
Hi MIke

Thanks for reply. (Sort probably is wrong: threw it together quickly as example to show nested for-each and why doc position won;t work).

Unfortunatley I'm stuck with a 3rd party vendors poor XSL implementation (not even the new CompiledTransform in .NET is supported). If I had a choice, I'd jump to Saxon and 2.0 in a heartbeat.

Regards

Mark
mikeday
In that case, you can try using a recursive call-template and pass down a variable giving the position.