0

I have a list of items that I want to display in an HTML table. The list has "holes" though, that need to be filled in for it to display correctly (the x and y are the column and row in the table). I can't figure out where to even begin doing this.

<items>
  <row y="1">
    <item x="1" y="1" data="importantStuff1"/>
  </row>
  <row y="2">
    <item x="2" y="2" data="importantStuff3"/>
  </row>
  <row y="3">
    <item x="5" y="3" data="importantStuff1"/>
  </row>
  <row y="4">
    <item x="3" y="4" data="importantStuff2"/>
    <item x="4" y="4" data="importantStuff3"/>
  </row>
</items>

What I need is the following:

<items>
  <row y="1">
    <item x="1" y="1" data="importantStuff1"/>
    <item x="2" y="1" data="padding"/>
    <item x="3" y="1" data="padding"/>
    <item x="4" y="1" data="padding"/>
    <item x="5" y="1" data="padding"/>
  </row>
  <row y="2">
    <item x="1" y="2" data="padding"/>
    <item x="2" y="2" data="importantStuff3"/>
    <item x="3" y="2" data="padding"/>
    <item x="4" y="2" data="padding"/>
    <item x="5" y="2" data="padding"/>
  </row>
  <row y="3">
    <item x="1" y="3" data="padding"/>
    <item x="2" y="3" data="padding"/>
    <item x="3" y="3" data="padding"/>
    <item x="4" y="3" data="padding"/>
    <item x="5" y="3" data="importantStuff1"/>
  </row>
  <row y="4">
    <item x="1" y="4" data="padding"/>
    <item x="2" y="4" data="padding"/>
    <item x="3" y="4" data="importantStuff2"/>
    <item x="4" y="4" data="importantStuff3"/>
    <item x="5" y="4" data="padding"/>
  </row>
</items>

How can I pad the list to look like this? The items are guaranteed to be ordered and I know how many items there are on each axis.

EDIT: I didn't realize the question could be interpreted as that I wanted a simple list. There is additional data in each item that makes it important to preserve the existing item nodes. So what I need is a way to create only the padding nodes and leave the existing ones as-is.

In order to create a minimal example to demonstrate my problem, I went a little overboard. Sorry about that.

Fylke
  • 1,703
  • 3
  • 18
  • 27
  • 1
    Possible duplicate of [XSLT : Looping from 1 to 60](http://stackoverflow.com/questions/9076323/xslt-looping-from-1-to-60) Use those techniques to build the complete table and select the appropriate data from the input XML based on the loop indexes. – Jim Garrison Feb 14 '17 at 17:16
  • You need to clarify how exactly you "know how many items there are on each axis" and where can the stylesheet get this knowledge. – michael.hor257k Feb 15 '17 at 06:54
  • @michael.hor257k I have the unique entries that go on the axes of the matrix and am able to just get the position() of the last one. – Fylke Feb 15 '17 at 07:18
  • You can easily get the `y` value of the **last** `row`. It's not so easy to get the **maximum** value of `x` in **any** row. If that's what you mean. – michael.hor257k Feb 15 '17 at 07:38
  • Ah yes, you're right. But the table is entirely square, withouth empty columns or rows. I've already gotten rid of the empty ones. Another detail that is probably relevant is that the table is very large; ~500,000 rows and columns, so recursive methods might be problematic. – Fylke Feb 15 '17 at 08:01
  • In theory at least, if your input skips a row, the output needs to generate an empty row in that position. Same with columns. WRT recursion, I don't see that you have much choice. And I am not sure it is a problem. But see the link above in Jim Garrison's comment. – michael.hor257k Feb 15 '17 at 08:51

3 Answers3

2

If your example is as simple as your actual data, you should be able use a recursive template call to check each of the 5 items. If it's there, apply-templates to it. If it's not, create it.

Example...

XML Input

<items>
    <row y="1">
        <item x="1" y="1" data="importantStuff1"/>
    </row>
    <row y="2">
        <item x="2" y="2" data="importantStuff3"/>
    </row>
    <row y="3">
        <item x="5" y="3" data="importantStuff1"/>
    </row>
    <row y="4">
        <item x="3" y="4" data="importantStuff2"/>
        <item x="4" y="4" data="importantStuff3"/>
    </row>5
</items>

XSLT 1.0

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

  <xsl:param name="items" select="5"/>

  <!--Identity transform. Copy everything as-is unless overridden by
  a more specific template.--> 
  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()"/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="row">
    <xsl:copy>
      <xsl:apply-templates select="@*"/>
      <xsl:call-template name="outputItems">
        <xsl:with-param name="count" select="1"/>
      </xsl:call-template>
    </xsl:copy>
  </xsl:template>

  <xsl:template name="outputItems">
    <xsl:param name="count"/>
    <xsl:choose>
      <!--Item already exists.-->
      <xsl:when test="item[@x=$count]">
        <xsl:apply-templates select="item[@x=$count]"/>
      </xsl:when>
      <!--Item does not exist. create it.-->
      <xsl:otherwise>
        <item x="{$count}" y="{@y}" data="padding"/>
      </xsl:otherwise>
    </xsl:choose>
    <!--Call this template again if needed.-->
    <xsl:if test="$items > $count">
      <xsl:call-template name="outputItems">
        <xsl:with-param name="count" select="$count + 1"/>
      </xsl:call-template>
    </xsl:if>
  </xsl:template>

</xsl:stylesheet>

Output

<items>
   <row y="1">
      <item x="1" y="1" data="importantStuff1"/>
      <item x="2" y="1" data="padding"/>
      <item x="3" y="1" data="padding"/>
      <item x="4" y="1" data="padding"/>
      <item x="5" y="1" data="padding"/>
   </row>
   <row y="2">
      <item x="1" y="2" data="padding"/>
      <item x="2" y="2" data="importantStuff3"/>
      <item x="3" y="2" data="padding"/>
      <item x="4" y="2" data="padding"/>
      <item x="5" y="2" data="padding"/>
   </row>
   <row y="3">
      <item x="1" y="3" data="padding"/>
      <item x="2" y="3" data="padding"/>
      <item x="3" y="3" data="padding"/>
      <item x="4" y="3" data="padding"/>
      <item x="5" y="3" data="importantStuff1"/>
   </row>
   <row y="4">
      <item x="1" y="4" data="padding"/>
      <item x="2" y="4" data="padding"/>
      <item x="3" y="4" data="importantStuff2"/>
      <item x="4" y="4" data="importantStuff3"/>
      <item x="5" y="4" data="padding"/>
   </row>5
</items>
Daniel Haley
  • 46,915
  • 4
  • 65
  • 89
1

I know how many items there are on each axis.

Assuming you can pass this knowledge as parameters to the stylesheet, I would suggest you do it this way:

XSLT 1.0

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

<xsl:param name="rows" select="4"/>
<xsl:param name="cols" select="5"/>

<xsl:key name="item" match="item" use="concat(@x, '|', @y)" />

<xsl:template match="/items">
    <xsl:copy>
        <xsl:call-template name="generate-rows"/>
    </xsl:copy>
</xsl:template>

<xsl:template name="generate-rows">
    <xsl:param name="y" select="1"/>
    <xsl:if test="$y &lt;= $rows">
        <row y="{$y}">
            <xsl:call-template name="generate-cols">
                <xsl:with-param name="y" select="$y"/>
            </xsl:call-template>
        </row>
        <!-- recursive call -->
        <xsl:call-template name="generate-rows">
            <xsl:with-param name="y" select="$y + 1"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

<xsl:template name="generate-cols">
    <xsl:param name="y"/>
    <xsl:param name="x" select="1"/>
    <xsl:if test="$x &lt;= $cols">
         <item x="{$x}" y="{$y}" >
            <xsl:variable name="exisiting-item" select="key('item', concat($x, '|', $y))" />
            <xsl:attribute name="data">
                <xsl:choose>
                    <xsl:when test="$exisiting-item">
                        <xsl:value-of select="$exisiting-item/@data"/>
                    </xsl:when>
                    <xsl:otherwise>padding</xsl:otherwise>
                </xsl:choose>
            </xsl:attribute>
        </item>
        <!-- recursive call -->
        <xsl:call-template name="generate-cols">
            <xsl:with-param name="y" select="$y"/>
            <xsl:with-param name="x" select="$x + 1"/>
        </xsl:call-template>
    </xsl:if>
</xsl:template>

</xsl:stylesheet>

This will pre-generate a table of the supplied dimensions and fill each cell with the data from the corresponding item, if it exists.

--

Added:

If the table dimensions need to be deduced from the input, then change:

<xsl:param name="rows" select="4"/>
<xsl:param name="cols" select="5"/>

to:

<xsl:variable name="rows" select="/items/row[last()]/@y"/>
<xsl:variable name="cols">
    <xsl:for-each select="/items/row/item">
        <xsl:sort select="@x" data-type="number" order="ascending"/>
        <xsl:if test="position()=last()">
            <xsl:value-of select="@x"/>
        </xsl:if>
    </xsl:for-each>
</xsl:variable>

This takes the y value of the last row and the maximum x value of any item as the table dimensions.

michael.hor257k
  • 96,733
  • 5
  • 30
  • 46
  • This solution indeed works when I try it out on my, slightly more complicated, input over at xsltransform.net. The problem is that it won't work on the target machine, where the only XSLT processor I have access to is Saxon-HE 9.4.0 (the one that comes with JRE 1.7.0_17). The key lookup just returns no result and all elements end up as padding. – Fylke Feb 18 '17 at 10:56
  • I ended up making a variable and doing a regular "dumb" lookup with [@x = $x][@y = $y] instead since getting a slightly more modern XSLT version is apparently infeasable. – Fylke Feb 18 '17 at 11:53
  • I don''t think you should have any problems implementing this with Saxon 9.4. I would look for another cause. – michael.hor257k Feb 18 '17 at 12:03
  • I agree, it goes against all my intuition, but I've copied it exactly and it simply doesn't work on the target machine. Whereas it does work on xsltransform.net. Problem is that I can't afford to spend any more time on this, so I it's time to just drop it and go with the dumb solution. Thanks for the help though! – Fylke Feb 18 '17 at 12:11
0

You can achieve this with the following XSLT. This is some kind of a special solution, but creating the 'count' variable by using a namespaced XML island is a quite universal approach for emulating a 'for' loop with a fixed count.

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:cnt="http://cnt.com" exclude-result-prefixes="cnt">
  <xsl:output method="xml" indent="yes" />

  <!-- creating a "count" XML structure -->
  <cnt:count>
    <x cnt="1" />
    <x cnt="2" />
    <x cnt="3" />
    <x cnt="4" />
    <x cnt="5" />
  </cnt:count>

  <xsl:template match="items">
    <items>
      <xsl:for-each select="row">
        <xsl:variable name="varY" select="@y" />   <!-- saving the value of '@y' -->
        <row y="{$varY}">
          <xsl:for-each select="document('')/xsl:stylesheet/cnt:count/x">
            <item x="{@cnt}" y="{$varY}" />
          </xsl:for-each>      
        </row>
      </xsl:for-each>      
    </items>
  </xsl:template>

</xsl:stylesheet>

The output is as desired.

zx485
  • 24,099
  • 26
  • 45
  • 52
  • I'm not exactly sure, but I think this solution would not preserve the existing nodes. (I'm not able to test right now) I edited my question to reflect that I need to preserve the existing nodes. Sorry about that. – Fylke Feb 15 '17 at 01:10
  • @Fylke: Your question really has changed since I tried to answer it :-) – zx485 Feb 15 '17 at 01:13