Thursday, November 09, 2006

Using collection() and saxon:discard-document() to create reports

You can process directories of XML using the collection() function, and keep memory usage constant by using the Saxon extension saxon:discard-document()


<xsl:for-each select="for $x in collection('file:///c:/xmlDir?select=*.xml;recurse=yes;on-error=ignore') return saxon:discard-document($x)">


You have to be careful that Saxon doesn't optimize out the call to saxon:discard-document() - this basic outer xsl:for-each works well and has become boilerplate code for whenever I start a new report.

This technique allows you to do things that would otherwise not be feasible with XSLT, and would take longer in another language. For example finding, grouping and sorting all links in your collection of XML files. Coding the XSLT takes minutes and running it takes time proportional to your dataset size, but the restriction of system memory has gone.

Monday, September 04, 2006

Table Normalization in XSLT 2.0

Some time ago I wrote a 1.0 stylesheet that normalizes a table - it's currently available on Dave Pawson's FAQ.

The problem of table normalization is to remove colspans and rowspans from a table placing a copy of the content in each cell.

This table:


+-----------+-----------+
| a | b |
| +-----+-----+
| | c | d |
+-----------+-----+ |
| e | |
+-----+-----+-----+ |
| f | g | h | |
+-----+-----+-----+-----+


Would become this:


+-----+-----+-----+-----+
| a | a | b | b |
+-----+-----+-----+-----+
| a | a | c | d |
+-----+-----+-----+-----+
| e | e | e | d |
+-----+-----+-----+-----+
| f | g | h | d |
+-----+-----+-----+-----+


The 1.0 solution I wrote used a neat recursive template that maintained pointers to the previous and current rows to deal with the tricky rowspans (colspans are the easy part). In rewriting the transform in 2.0 I've simplified it a great deal, but sadly not with any new 2.0 only feature, just with a better algorithm. Here's the transform:


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

<xsl:output method="xhtml" indent="yes"
omit-xml-declaration="yes"
encoding="UTF-8" />

<xsl:variable name="table_with_no_colspans">
<xsl:apply-templates mode="colspan"/>
</xsl:variable>

<xsl:variable name="table_with_no_rowspans">
<xsl:for-each select="$table_with_no_colspans">
<xsl:apply-templates mode="rowspan"/>
</xsl:for-each>
</xsl:variable>

<xsl:template match="/">
<xsl:apply-templates select="$table_with_no_rowspans" mode="final"/>
</xsl:template>

<xsl:template match="@*|*" mode="#all">
<xsl:copy>
<xsl:apply-templates select="@*|*" mode="#current"/>
</xsl:copy>
</xsl:template>

<xsl:template match="td" mode="colspan">
<xsl:choose>
<xsl:when test="@colspan">
<xsl:variable name="this" select="." as="element()"/>
<xsl:for-each select="1 to @colspan">
<td>
<xsl:copy-of select="$this/@*[not(name() = 'colspan')][not(name() = 'width')]"/>
<xsl:copy-of select="$this/node()"/>
</td>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

<xsl:template match="tbody" mode="rowspan">
<xsl:copy>
<xsl:copy-of select="tr[1]" />
<xsl:apply-templates select="tr[2]" mode="rowspan">
<xsl:with-param name="previousRow" select="tr[1]"/>
</xsl:apply-templates>
</xsl:copy>
</xsl:template>

<xsl:template match="tr" mode="rowspan">
<xsl:param name="previousRow"/>

<xsl:variable name="currentRow" select="."/>

<xsl:variable name="normalizedTDs">
<xsl:for-each select="$previousRow/td">
<xsl:choose>
<xsl:when test="@rowspan > 1">
<xsl:copy>
<xsl:attribute name="rowspan">
<xsl:value-of select="@rowspan - 1" />
</xsl:attribute>

<xsl:copy-of select="@*[not(name() = 'rowspan')]"/>
<xsl:copy-of select="node()"/>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="$currentRow/td[1 + count(current()/preceding-sibling::td[not(@rowspan) or (@rowspan = 1)])]"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:variable>

<xsl:variable name="newRow" as="element(tr)">
<xsl:copy>
<xsl:copy-of select="$currentRow/@*"/>
<xsl:copy-of select="$normalizedTDs"/>
</xsl:copy>
</xsl:variable>

<xsl:copy-of select="$newRow"/>

<xsl:apply-templates select="following-sibling::tr[1]" mode="rowspan">
<xsl:with-param name="previousRow" select="$newRow"/>
</xsl:apply-templates>
</xsl:template>

<xsl:template match="td" mode="final">
<xsl:choose>
<xsl:when test="@rowspan">
<xsl:copy>
<xsl:copy-of select="@*[not(name() = 'rowspan')]"/>
<xsl:copy-of select="node()"/>
</xsl:copy>
</xsl:when>
<xsl:otherwise>
<xsl:copy-of select="."/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>

</xsl:stylesheet>

Friday, July 07, 2006

Kernow 1.4 released

I've finally got around to releasing a new version of Kernow.

This version adds a few new features and I've also fixed a few annoying bugs:


  • Added optional caching URI and entity resolvers (including an entity resolver for documents referenced through the collection() function)
  • Added directory validation using SaxonSA, Xerces or JAXP (choice of XSD or RNG where possible).
  • Added the ability to run schema aware XQueries
  • Added default, lax and strict validation for schema aware transforms and queries
  • Added the ability to choose the type of files to process in a directory (eg, xml, xhtml, or well-formed html)
  • Fixed a number of small bugs, such as the progress bar occasionally throwing an NPE.

Wednesday, June 14, 2006

Saxon 8.7.3 released

Mike Kay has released Saxon 8.7.3

Saxon-B: http://saxon.sf.net/
Saxon-SA: http://www.saxonica.com/

"This maintenance release clears 35 documented bugs, and on .NET it adds the
capability to process a DOM Document "in situ" rather than by first copying
it to create a Saxon tree.

(Saxon 8.7.2, in case you missed it, was a rebuild of Saxon-SA on .NET with
no changed functionality.)"

Monday, June 12, 2006

A Caching EntityResolver for the collection() function

XSLT 2.0 allows you to write a "standalone stylesheet", that is, a transform with no input XML. Instead you must provide the name of the template where execution should begin. The stylesheet can then use the document() or doc() functions to process individual files, and the collection() function to process directories of XML. I use the standalone XSLT model a lot where you have multiple input files and a single output file, for example generating a report or creating an index page.

I recently ran into a problem though, where a few thousand of several thousand input files referenced entity files hosted at the w3.org. This only became apparent when the transforms started failing for no reason. It turns out that the network admins where I work had turned off the proxy, so each and every entity file was being fetched from the w3c - where making 500 requests in 10 minutes mean you get a 24 hour ban...

In a standard transform catching and caching requests for entity files is easy - you write a custom EntityResolver and tell the XMLReader to use that. For the collection() function however it's a lot more complicated. In Saxon 8.7.1, the URI used in the collection() function is resolved using the StandardCollectionURIResolver(). You can use your own one of these by calling setCollectionURIResolver() on the Configuration, and then do what you like within that class. To create a CollectionURIResolver() that caches entities files, it's pretty much a case of copying whats in the StandardCollectionURIResolver() and then modifying it to suit your needs.

I've added this functionality to the next release of Kernow, so that standalone transforms and XQueries have the option of a caching EntityResolver. I get the impression it's early days here, and this will become a common enough requirement that a setter will be exposed for an EntityResolver on the XMLReader used by the collection() function, making this kind of task much easier in the future.

Thursday, May 25, 2006

Character encoding, character references and Windows-1252

Consider this XML:

<?xml version="1.0" encoding="Windows-1252" ?>
<foo>&_#150;0x96</foo>

Here there is the character reference #150 and the actual character 0x96 that character reference #150 resolves to. The underscore is used to prevent the character reference being resolved by the browser, and the string '0x96' is used instead of the actual character 0x96 because blogger complains otherwise (stick with me...) Note the encoding used in the prolog.

Transforming that XML with this stylesheet:

<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform";>
<xsl:output encoding="US-ASCII"/>
<xsl:template match="/">
<xsl:copy-of select="."/>
</xsl:template>
</xsl:stylesheet>

Gives this result:

<foo>&_#150;&_#8211;</foo>

Here you can see the character reference has been written back out as the same character, however character 0x96 has become 0x2013 (8211 in decimal). Character 150 is the unicode character "START OF GUARDED AREA" in the non-displayed C1 control character range, but in the Windows-1252 encoding it's mapped to to the displayable character 0x2013 "en-dash" (a short dash). Microsoft squeezed more characters into the single byte range by replacing non-displayed control characters with more useful displayable characters, but mistakenly went on to label files encoded in this way as ISO-8859-1 in some MS Office applications. In ISO-8859-1 the characters in the C0 and C1 ranges are the non-displayable control characters, but this mis-labelling was so widespread that parsers began detecting this situation and silently switching the read encoding to Windows-1252.

This problem surfaces when serving XHTML to an XHTML browser. While browsers are reading files using their HTML parsers, any file mis-labelled as ISO-8859-1 that contains characters in the C0 or C1 ranges will still auto-magically display the characters in those ranges, as the forgiving parsers auto-switch the read encoding. However, when an XHTML file is served (using the correct mime type eg "application/xhtml+xml") XHTML browsers such as Firefox will parse the file using its stricter XML parser - and all the characters in the C0 and C1 ranges will remain as those non-displayed characters. The auto-switch won't take place and characters such as 0x96 (en-dash) that were once displayed will just disappear.

This problem only occurs when an XML file is saved in Windows-1252 but is labelled as something else, usually IS0-8859-1. The most common culprit is Notepad, where a user has edited and saved an XML file without realising/caring that Notepad is unaware of the XML prolog.

So back to the example above:

<?xml version="1.0" encoding="Windows-1252" ?>
<foo>&_#150;0x96</foo>

The main point to realise here is that character references (the #150 in the example) are always resolved using Unicode codepoints, regardless of the specified encoding. Actual characters in the file will be read using the specified encoding. Therefore the #150 is resolved to 0x96 (its Unicode codepoint), while the actual character 0x96 in the source becomes 0x2013 (#8211) as specified in the Windows-1252 encoding.

The result of the transformation demonstrates this when serialised using the US-ASCII encoding (so all bytes above 127 will be written out as character references) looks like this:

<foo>&_#150;&_#8211;</foo>

A great example I think :)

Wikipedia has lots more information: http://en.wikipedia.org/wiki/ISO_8859-1

*There are two versions of ISO-8859-1 - The ISO and IANA versions. The ISO versions doesn't contain the C0 and C1 control characters, the IANA version does contain them. The XML recommendation uses the IANA version.

Sunday, May 07, 2006

Kernow 1.3 released

I've uploaded a new version Kernow that contains Saxon 8.7.1 with a few minor bugs fixed. These mainly centered around paths - the systemId of the source XML and base uri used by xsl:result-document.

The latter was pretty straightforward, the base uri used by xsl:result-document is set using Controller.setBaseOutputURI() - this value is now set to either the uri of the stylesheet, or the output file/directory (depending if one is supplied). This makes sense, as if you are outputting to a given file/directory, you would like the output from your xsl:result-document instructions to be relative to that (in my view, anyway). If you haven't supplied an output file/directory and you're outputting to the Kernow output window, then it should be resovled against the stylesheet.

Setting the systemId of the TransformerHandler to be that of the XML file seemed wrong to me at first, as you would expect it to be used by the stylesheet. After thinking about it a little, it makes some sense: the TransformerHandler knows the systemId of the stylesheet from when it was created - it doesn't know the location of the source XML as that arrives as a series of SAX events - therefore it's possible to set it through setSystemId. Perhaps someone knows for sure...

Tuesday, May 02, 2006

Sudoku Solver complete - and shes fast

(NOTE: I've improved on this version here)

I finally got around to finishing the Sudoku solver after several weeks of other things getting the way.

I've improved the heuristics that check for the allowed values for a given cell. It now checks the row and column "friends" - the other rows and columns in a group (eg columns 1 and 2 when the cell is in column 3, or columns 1 and 3 when the cell is in 2). This is quite straightforward for a human and is done almost without noticing, but its reasonably involved to code.

The other change was to recursively insert all the values into cells with only one possible value, and then re-evaluate the allowed values with those cells inserted. This takes care of the situation where there are initially two possible values, but after inserting a definite value elsewhere there is only one possible values left.

Ultimately all this reduces the number of times the transform has to guess - on certain boards it would take a long time to solve purely because it was guessing wrongly early on, and then have to go through all the permutations before working its way back to that early point and trying the next number. Now there should be the maximum number of values in place before it has to guess (if at all) so that it doesn't waste time backtracking.

Here it is. To try it out run it using Saxon with "-it main" set, or run it against a dummy input XML. The are a few sample boards included, to try them out change the main template to point the variable containing the board, or cut and paste it's contents into the param at the top of the stylesheet.

If you find a board that this stylesheet can't process quickly, please let me know.



<xsl:stylesheet version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:saxon="http://saxon.sf.net/"
xmlns:fn="sudoku">

<xsl:param name="board" select="(
1,0,0, 3,0,0, 6,0,0,
0,2,0, 5,0,0, 0,0,4,
0,0,9, 0,0,0, 5,2,0,

0,0,0, 9,6,3, 0,0,0,
7,1,6, 0,0,0, 0,0,0,
0,0,0, 0,8,0, 0,4,0,

9,0,0, 0,0,5, 3,0,7,
8,0,0, 4,0,6, 0,0,0,
3,5,0, 0,0,0, 0,0,1
)" as="xs:integer+"/>

<xsl:param name="verbose" select="false()" as="xs:boolean"/>

<xsl:variable name="rowStarts" select="(1, 10, 19, 28, 37, 46, 55, 64, 73)" as="xs:integer+"/>
<xsl:variable name="topLeftGroup" select="(1, 2, 3, 10, 11, 12, 19, 20, 21)" as="xs:integer+"/>
<xsl:variable name="topGroup" select="(4, 5, 6, 13, 14, 15, 22, 23, 24)" as="xs:integer+"/>
<xsl:variable name="topRightGroup" select="(7, 8, 9, 16, 17, 18, 25, 26, 27)" as="xs:integer+"/>
<xsl:variable name="midLeftGroup" select="(28, 29, 30, 37, 38, 39, 46, 47, 48)" as="xs:integer+"/>
<xsl:variable name="center" select="(31, 32, 33, 40, 41, 42, 49, 50, 51)" as="xs:integer+"/>
<xsl:variable name="midRightGroup" select="(34, 35, 36, 43, 44, 45, 52, 53, 54)" as="xs:integer+"/>
<xsl:variable name="bottomLeftGroup" select="(55, 56, 57, 64, 65, 66, 73, 74, 75)" as="xs:integer+"/>
<xsl:variable name="bottomGroup" select="(58, 59, 60, 67, 68, 69, 76, 77, 78)" as="xs:integer+"/>
<xsl:variable name="bottomRightGroup" select="(61, 62, 63, 70, 71, 72, 79, 80, 81)" as="xs:integer+"/>

<xsl:function name="fn:getRowStart" as="xs:integer+" saxon:memo-function="yes">
<xsl:param name="index" as="xs:integer+"/>
<xsl:sequence select="xs:integer(floor(($index - 1) div 9) * 9) + 1"/>
</xsl:function>

<xsl:function name="fn:getRowNumber" as="xs:integer+" saxon:memo-function="yes">
<xsl:param name="index" as="xs:integer+"/>
<xsl:sequence select="(($index - 1) idiv 9) + 1"/>
</xsl:function>

<xsl:function name="fn:getColNumber" as="xs:integer+" saxon:memo-function="yes">
<xsl:param name="index" as="xs:integer+"/>
<xsl:sequence select="(($index - 1) mod 9) + 1"/>
</xsl:function>

<xsl:function name="fn:getRow" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:sequence select="subsequence($board, fn:getRowStart($index), 9)"/>
</xsl:function>

<xsl:function name="fn:getCol" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:variable name="gap" select="($index - 1) mod 9" as="xs:integer"/>
<xsl:variable name="colIndexes" select="for $x in $rowStarts return $x + $gap" as="xs:integer+"/>
<xsl:sequence select="for $x in $colIndexes return $board[$x]"/>
</xsl:function>

<xsl:function name="fn:getGroup" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:choose>
<xsl:when test="$index = $topLeftGroup">
<xsl:sequence select="for $x in $topLeftGroup return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $topGroup">
<xsl:sequence select="for $x in $topGroup return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $topRightGroup">
<xsl:sequence select="for $x in $topRightGroup return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $midLeftGroup">
<xsl:sequence select="for $x in $midLeftGroup return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $center">
<xsl:sequence select="for $x in $center return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $midRightGroup">
<xsl:sequence select="for $x in $midRightGroup return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $bottomLeftGroup">
<xsl:sequence select="for $x in $bottomLeftGroup return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $bottomGroup">
<xsl:sequence select="for $x in $bottomGroup return $board[$x]"/>
</xsl:when>
<xsl:when test="$index = $bottomRightGroup">
<xsl:sequence select="for $x in $bottomRightGroup return $board[$x]"/>
</xsl:when>
</xsl:choose>
</xsl:function>

<xsl:function name="fn:getOtherValuesInTriple" as="xs:integer+" saxon:memo-function="yes">
<xsl:param name="num" as="xs:integer"/>
<xsl:sequence select="(xs:integer(((($num -1) idiv 3) * 3) + 1) to xs:integer(((($num - 1) idiv 3) * 3) + 3))[. != $num]"/>
</xsl:function>

<xsl:function name="fn:getRowFriends" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:sequence select="for $x in fn:getOtherValuesInTriple($index) return $board[$x]"/>
</xsl:function>

<xsl:function name="fn:getColFriends" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:sequence select="for $x in fn:getOtherValuesInTriple(fn:getRowNumber($index))
return
$board[(($x * 9) - 8) + (($index - 1) mod 9)]"/>
</xsl:function>

<xsl:function name="fn:reducePossibleValues" as="xs:integer*">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:param name="possibleValues" as="xs:integer+"/>

<xsl:variable name="rowFriends" select="fn:getRowFriends($board, $index)" as="xs:integer+"/>
<xsl:variable name="colFriends" select="fn:getColFriends($board, $index)" as="xs:integer+"/>

<xsl:variable name="otherRow1" select="fn:getRow($board, 9 * fn:getOtherValuesInTriple(fn:getRowNumber($index))[1])" as="xs:integer+"/>
<xsl:variable name="otherRow2" select="fn:getRow($board, 9 * fn:getOtherValuesInTriple(fn:getRowNumber($index))[2])" as="xs:integer+"/>

<xsl:variable name="otherCol1" select="fn:getCol($board, fn:getOtherValuesInTriple($index)[1])" as="xs:integer+"/>
<xsl:variable name="otherCol2" select="fn:getCol($board, fn:getOtherValuesInTriple($index)[2])" as="xs:integer+"/>

<xsl:variable name="reducedValues" as="xs:integer*">
<xsl:for-each select="$possibleValues">
<xsl:if test=". = $otherCol1 and . = $otherCol2">
<xsl:choose>
<xsl:when test="not($colFriends = 0)">
<xsl:sequence select="."/>
</xsl:when>
<xsl:when test="$colFriends != 0">
<xsl:choose>
<xsl:when test="$colFriends[1] = 0">
<xsl:if test=". = $otherRow1">
<xsl:sequence select="."/>
</xsl:if>
</xsl:when>
<xsl:when test="$colFriends[2] = 0">
<xsl:if test=". = $otherRow2">
<xsl:sequence select="."/>
</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:when>
</xsl:choose>
</xsl:if>

<xsl:if test=". = $otherRow1 and . = $otherRow2">
<xsl:choose>
<xsl:when test="not($rowFriends = 0)">
<xsl:sequence select="."/>
</xsl:when>
<xsl:when test="$rowFriends != 0">
<xsl:choose>
<xsl:when test="$rowFriends[1] = 0">
<xsl:if test=". = $otherCol1">
<xsl:sequence select="."/>
</xsl:if>
</xsl:when>
<xsl:when test="$rowFriends[2] = 0">
<xsl:if test=". = $otherCol2">
<xsl:sequence select="."/>
</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:when>
</xsl:choose>
</xsl:if>
</xsl:for-each>
</xsl:variable>

<xsl:sequence select="if (count($reducedValues) = 1) then $reducedValues else $possibleValues"/>

</xsl:function>

<xsl:function name="fn:getAllowedValues" as="xs:integer*">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>

<xsl:variable name="possibleValues" select="(1 to 9)[not(. = (fn:getRow($board, $index), fn:getCol($board, $index), fn:getGroup($board, $index)))]" as="xs:integer*"/>

<xsl:choose>
<xsl:when test="count($possibleValues) > 1">
<xsl:variable name="reducedValues" select="fn:reducePossibleValues($board, $index, $possibleValues)" as="xs:integer*"/>

<xsl:sequence select="$reducedValues"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$possibleValues"/>
</xsl:otherwise>
</xsl:choose>

</xsl:function>

<xsl:function name="fn:tryValues" as="xs:integer*">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="emptyCells" as="xs:integer+"/>
<xsl:param name="possibleValues" as="xs:integer+"/>

<xsl:variable name="index" select="$emptyCells[1]" as="xs:integer"/>

<xsl:variable name="newBoard" select="(subsequence($board, 1, $index - 1), $possibleValues[1], subsequence($board, $index + 1))" as="xs:integer+"/>

<xsl:if test="$verbose">
<xsl:message>
<xsl:value-of select="concat('Trying ', $possibleValues[1], ' out of a possible ', string-join(for $i in $possibleValues return xs:string($i), ' '), ' at index ', $index)"/>
</xsl:message>
</xsl:if>

<xsl:variable name="result" select="fn:populateValues($newBoard, subsequence($emptyCells, 2))" as="xs:integer*"/>

<xsl:sequence select="if (empty($result)) then
if (count($possibleValues) > 1) then
fn:tryValues($board, $emptyCells, subsequence($possibleValues, 2))
else
()
else
$result"/>
</xsl:function>

<xsl:function name="fn:populateValues" as="xs:integer*">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="emptyCells" as="xs:integer*"/>

<xsl:choose>
<xsl:when test="empty($emptyCells)">
<xsl:message>Done!</xsl:message>
<xsl:sequence select="$board"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="index" select="$emptyCells[1]" as="xs:integer"/>
<xsl:variable name="possibleValues" select="fn:getAllowedValues($board, $index)" as="xs:integer*"/>
<xsl:choose>
<xsl:when test="count($possibleValues) = 0">
<xsl:if test="$verbose">
<xsl:message>! Cannot go any further !</xsl:message>
</xsl:if>
<xsl:sequence select="()"/>
</xsl:when>
<xsl:when test="count($possibleValues) > 1">
<xsl:sequence select="fn:tryValues($board, $emptyCells, $possibleValues)"/>
</xsl:when>
<xsl:when test="count($possibleValues) = 1">
<xsl:variable name="newBoard" select="(subsequence($board, 1, $index - 1), $possibleValues[1], subsequence($board, $index + 1))" as="xs:integer+"/>

<xsl:if test="$verbose">
<xsl:message>
<xsl:value-of>
<xsl:text>Only one value </xsl:text>
<xsl:value-of select="$possibleValues[1]"/>
<xsl:text> for index </xsl:text>
<xsl:value-of select="$index"/>
</xsl:value-of>
</xsl:message>
</xsl:if>

<xsl:sequence select="fn:populateValues($newBoard, subsequence($emptyCells, 2))"/>
</xsl:when>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:function>

<xsl:function name="fn:populateSingleValueCells" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="emptyCellsXML" as="element()*"/>

<xsl:for-each select="1 to 81">
<xsl:variable name="pos" select="." as="xs:integer"/>
<xsl:choose>
<xsl:when test="$emptyCellsXML[@index = $pos]/@possibleValues = 1">
<xsl:sequence select="$emptyCellsXML[@index = $pos]/@values"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$board[$pos]"/>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>

</xsl:function>

<xsl:function name="fn:populateSingleValueCells" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>

<xsl:variable name="emptyCells" select="for $x in (1 to 81) return $x[$board[$x] = 0]" as="xs:integer*"/>

<xsl:variable name="emptyCellsXML" as="element()*">
<xsl:for-each select="$emptyCells">
<xsl:variable name="allowedValues" select="fn:getAllowedValues($board, .)" as="xs:integer*"/>
<cell index="{.}" possibleValues="{count($allowedValues)}" values="{$allowedValues}"/>
</xsl:for-each>
</xsl:variable>

<xsl:choose>
<xsl:when test="$emptyCellsXML/@possibleValues = 1">
<xsl:variable name="newBoard" select="fn:populateSingleValueCells($board, $emptyCellsXML)" as="xs:integer+"/>
<xsl:sequence select="fn:populateSingleValueCells($newBoard)"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$board"/>
</xsl:otherwise>
</xsl:choose>

</xsl:function>

<xsl:function name="fn:solveSudoku" as="xs:integer+">
<xsl:param name="startBoard" as="xs:integer+"/>

<xsl:variable name="board" select="fn:populateSingleValueCells($startBoard)" as="xs:integer+"/>

<xsl:variable name="emptyCells" select="for $x in (1 to 81) return $x[$board[$x] = 0]" as="xs:integer*"/>

<xsl:variable name="emptyCellsXML" as="element()*">
<xsl:for-each select="$emptyCells">
<xsl:variable name="allowedValues" select="fn:getAllowedValues($board, .)" as="xs:integer*"/>
<cell index="{.}" possibleValues="{count($allowedValues)}" values="{$allowedValues}"/>
</xsl:for-each>
</xsl:variable>

<xsl:variable name="emptyCellsOrdered" as="xs:integer*">
<xsl:for-each select="$emptyCellsXML">
<xsl:sort select="@possibleValues" data-type="number" order="ascending"/>
<xsl:sequence select="@index"/>
</xsl:for-each>
</xsl:variable>

<xsl:variable name="endBoard" select="fn:populateValues($board, $emptyCells)" as="xs:integer*"/>

<xsl:choose>
<xsl:when test="empty($endBoard)">
<xsl:message>! Invalid board - The starting board is not correct</xsl:message>
<xsl:sequence select="$startBoard"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$endBoard"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>

<xsl:template match="/tt" name="maintt">
<div>
<xsl:variable name="emptyCellsXML" as="element()*">
<xsl:for-each select="for $x in (1 to 81) return $x[$board[$x] = 0]">
<cell index="{.}" possibleValues="{count(fn:getAllowedValues($board, .))}" values="{fn:getAllowedValues($board, .)}"/>
</xsl:for-each>
</xsl:variable>

<xsl:for-each select="$emptyCellsXML">
<xsl:sort select="@possibleValues" data-type="number" order="ascending"/>
<xsl:copy-of select="."/>
</xsl:for-each>
</div>
</xsl:template>

<xsl:template match="/" name="main">
<xsl:call-template name="drawResult">
<xsl:with-param name="board" select="fn:solveSudoku($board)"/>
</xsl:call-template>
</xsl:template>

<xsl:template name="drawResult">
<xsl:param name="board" as="xs:integer+"/>
<html>
<head>
<title>Sudoku - XSLT</title>
<style>
table { border-collapse: collapse;
border: 1px solid black; }
td { padding: 10px; }
.norm { border-left: 1px solid #CCC;
border-top: 1px solid #CCC;}
.csep { border-left: 1px solid black; }
.rsep { border-top: 1px solid black; }
</style>
</head>
<body>
<xsl:call-template name="drawBoard">
<xsl:with-param name="board" select="$board"/>
</xsl:call-template>
</body>
</html>
</xsl:template>

<xsl:template name="drawBoard">
<xsl:param name="board" as="xs:integer+"/>
<table>
<xsl:for-each select="1 to 9">
<xsl:variable name="i" select="."/>
<tr>
<xsl:for-each select="1 to 9">
<xsl:variable name="pos" select="(($i - 1) * 9) + ."/>
<td class="{if (position() mod 3 = 1) then 'csep' else ('norm')} {if ($i mod 3 = 1) then 'rsep' else ('norm')}">
<xsl:value-of select="$board[$pos]"/>
</td>
</xsl:for-each>
</tr>
</xsl:for-each>
</table>
</xsl:template>

<xsl:template name="drawBasicBoard">
<xsl:param name="board" as="xs:integer+"/>
<xsl:for-each select="1 to 9">
<xsl:variable name="i" select="."/>

<xsl:for-each select="1 to 9">
<xsl:variable name="pos" select="(($i - 1) * 9) + ."/>
<xsl:value-of select="$board[$pos]"/>
<xsl:value-of select="if ($pos mod 3 = 0) then ', ' else ', '"/>
</xsl:for-each>

<xsl:value-of select="if ($i mod 3 = 0) then ' ' else ' '"/>
</xsl:for-each>
</xsl:template>

<!-- Easy board, 32 existing numbers -->
<xsl:variable name="testBoard1" select="(
0,2,0, 0,0,0, 0,3,6,
0,0,7, 4,0,0, 0,9,0,
0,0,5, 6,0,0, 0,4,8,

0,0,0, 9,3,0, 0,1,2,
2,9,0, 0,0,0, 0,7,5,
1,5,0, 0,8,2, 0,0,0,

6,7,0, 0,0,9, 1,0,0,
0,3,0, 0,0,7, 6,0,0,
4,8,0, 0,0,0, 0,2,0
)" as="xs:integer+"/>

<!-- Hard board, 24 existing numbers -->
<xsl:variable name="testBoard2" select="(
1,0,0, 5,6,0, 0,0,0,
9,0,0, 0,0,0, 2,0,8,
0,0,0, 0,0,0, 7,0,0,

0,8,0, 9,0,7, 0,0,2,
2,0,0, 0,0,0, 0,0,1,
6,0,0, 3,0,2, 0,4,0,

0,0,5, 0,0,0, 0,0,0,
4,0,3, 0,0,0, 0,0,9,
0,0,0, 0,4,1, 0,0,6
)" as="xs:integer+"/>

<!-- Extremely Hard board -->
<xsl:variable name="testBoard3" select="(
4,0,6, 7,8,0, 2,0,0,
0,5,7, 0,0,0, 0,0,0,
8,0,0, 0,0,0, 4,0,0,

0,0,0, 0,5,0, 0,9,0,
9,1,2, 0,0,0, 0,0,0,
0,4,0, 0,1,0, 0,0,3,

0,0,4, 3,0,0, 0,2,0,
7,0,3, 0,0,6, 0,0,0,
0,0,0, 5,0,7, 6,0,8
)" as="xs:integer+"/>

<!-- Fiendish board -->
<xsl:variable name="testBoard4" select="(
0,0,0, 0,0,5, 0,0,0,
0,0,0, 0,2,0, 9,0,0,
0,8,4, 9,0,0, 7,0,0,

2,0,0, 0,9,0, 4,0,0,
0,3,0, 6,0,2, 0,8,0,
0,0,7, 0,3,0, 0,0,6,

0,0,2, 0,0,9, 8,1,0,
0,0,6, 0,4,0, 0,0,0,
0,0,0, 5,0,0, 0,0,0
)" as="xs:integer+"/>

<!-- Fiendish 2 board -->
<xsl:variable name="testBoard5" select="(
0,0,0, 0,0,5, 1,0,0,
0,3,5, 0,0,0, 0,4,0,
8,0,0, 4,0,0, 0,2,0,

9,0,0, 0,3,0, 5,0,0,
0,0,0, 2,0,8, 0,0,0,
0,0,7, 0,9,0, 0,0,8,

0,5,0, 0,0,9, 0,0,2,
0,4,0, 0,0,0, 9,8,0,
0,0,1, 7,0,0, 0,0,0
)" as="xs:integer+"/>

<xsl:variable name="testBoard_Fiendish_060323" select="(
5,3,0, 0,9,0, 0,0,6,
0,0,0, 1,5,0, 0,0,3,
0,0,2, 0,0,0, 8,0,0,

0,0,0, 0,0,0, 0,8,0,
1,7,0, 0,4,0, 0,2,9,
0,9,0, 0,0,0, 0,0,0,

0,0,6, 0,0,0, 3,0,0,
4,0,0, 0,1,2, 0,0,0,
3,0,0, 0,6,0, 0,5,4
)" as="xs:integer+"/>

<!-- Fiendish 060403-->
<xsl:variable name="testBoard38" select="(
5,0,0, 0,0,1, 0,0,2,
0,6,0, 0,7,0, 0,0,0,
0,0,0, 6,0,0, 8,0,0,

0,0,3, 0,0,5, 0,0,8,
0,9,0, 0,1,0, 0,2,0,
2,0,0, 3,0,0, 7,0,0,

0,0,1, 0,0,2, 0,0,0,
0,0,0, 0,6,0, 0,9,0,
8,0,0, 7,0,0, 0,0,5
)" as="xs:integer+"/>

<!-- Failing board, an erroneous 9 has been added at index 1 -->
<xsl:variable name="testBoard_Fail" select="(
9,2,0, 0,0,0, 0,3,6,
0,0,7, 4,0,0, 0,9,0,
0,0,5, 6,0,0, 0,4,8,

0,0,0, 9,3,0, 0,1,2,
2,9,0, 0,0,0, 0,7,5,
1,5,0, 0,8,2, 0,0,0,

6,7,0, 0,0,9, 1,0,0,
0,3,0, 0,0,7, 6,0,0,
4,8,0, 0,0,0, 0,2,0
)" as="xs:integer+"/>

</xsl:stylesheet>

Monday, March 27, 2006

VTD-XML

Ian Jones pointed me to an article on javaworld about VTD-XML, which is apparently "A new option that overcomes the problems of DOM and SAX".

It all sounds promising, although the article is light on code examples and heavy on comparisons, so I'll reserve judgement until I've used it. It does seem to concentrate on speed without mentioning conformity, which is not a good sign.

Friday, March 24, 2006

Well when I said "blisteringly fast" I should have said "very fast, but give it a week or two and we'll get faster"...

I've since found a way of improving the performance of my solution... and I think Dimitre has improved his. Watch this space, because neither is quite ready yet. I'll stick my neck out and say once it's done, mine should solve any board pretty much instantly. (I'm going to regret saying that, because mine probably won't but Dimitre's will...)

Monday, March 20, 2006

Dimitre's tuned Sudoku solution

Never one to settle for second place Dimitre set about performance tuning his Sudoku solving stylesheet... with dramatic effect.

His latest effort is *blisteringly* fast...

With this board, known as "Fiendish 2" his solution takes just 7.1 seconds. To put that into perspective, my performance tuned implementation takes well over 5 minutes! I'll have to find out why the time is so bad for me...

Here's the "Fiendish 2" board, in the format Dimite's solution requires:



<board>
<row>0,0,0,0,0,5,1,0,0</row>
<row>0,3,5,0,0,0,0,4,0</row>
<row>8,0,0,4,0,0,0,2,0</row>
<row>9,0,0,0,3,0,5,0,0</row>
<row>0,0,0,2,0,8,0,0,0</row>
<row>0,0,7,0,9,0,0,0,8</row>
<row>0,5,0,0,0,9,0,0,2</row>
<row>0,4,0,0,0,0,9,8,0</row>
<row>0,0,1,7,0,0,0,0,0</row>
</board>




Dimitre's stylesheet:



<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:f="http://fxsl.sf.net/"
exclude-result-prefixes="f xs">

<xsl:output omit-xml-declaration="yes" indent="yes"/>

<xsl:template match="/">


<xsl:variable name="vFixed" select="f:cellsGroup('Fixed', /*/*)"/>
<xsl:variable name="vEmpty" select="f:cellsGroup('Empty', /*/*)"/>

<xsl:variable name="vallFillers" as="element()*">
<xsl:for-each select=
"f:makeFillers(f:cellsGroup('Fixed', /*/*), f:cellsGroup('Empty', /*/*))">
<xsl:sort select="@row"/>
<xsl:sort select="@col"/>

<xsl:sequence select="."/>
</xsl:for-each>
</xsl:variable>


<xsl:sequence
select="f:sudoku($vFixed, $vEmpty, $vallFillers)"/>
</xsl:template>

<xsl:variable name="vAllVals" select="1 to 9" as="xs:integer+"/>

<xsl:function name="f:cellsGroup" as="element()*">
<xsl:param name="pgrpType" as="xs:string"/>
<xsl:param name="pRows" as="element()*"/>
<xsl:sequence select=
"for $i in 1 to count($pRows),
$vRow in $pRows[$i],
$vNum in tokenize($vRow, ',')
[if($pgrpType='Fixed')
then . ne '0'
else . eq '0'
][if($pgrpType='Empty')
then 1
else true()],
$k in index-of(tokenize($vRow, ','),$vNum)
return
f:makeCell($i,$k, xs:integer($vNum))
"/>
</xsl:function>

<xsl:function name="f:makeCell" as="element()">
<xsl:param name="pnRow" as="xs:integer"/>
<xsl:param name="pnCol" as="xs:integer"/>
<xsl:param name="pnVal" as="xs:integer"/>

<cell row="{$pnRow}" col="{$pnCol}" val="{$pnVal}"/>
</xsl:function>

<xsl:function name="f:sudoku" as="item()*">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCells" as="element()*"/>
<xsl:param name="pallFillers" as="element()*"/>

<!--
<xsl:message>
f:sudoku:
Fixed: <xsl:sequence select="count($pFixedCells)"/>
Empty: <xsl:sequence select="count($pEmptyCells)"/>
</xsl:message>
-->
<xsl:choose>
<xsl:when test="empty($pEmptyCells)">
<xsl:sequence select="f:printBoard($pFixedCells)"/>
</xsl:when>
<xsl:otherwise>
<xsl:if test="f:canContinue($pFixedCells, $pEmptyCells, $pallFillers)">
<xsl:variable name="vBestCells" as="element()*"
select="f:bestCellsToTry($pEmptyCells,$pallFillers)"/>
<xsl:variable name="vBestCell" select="$vBestCells[1]"/>
<xsl:variable name="vcellFillers" as="element()+"
select="f:cellFillers($pallFillers,$vBestCell)"/>

<xsl:sequence select=
"f:tryFillers($pFixedCells,$pEmptyCells, $pallFillers,
$vcellFillers,$vBestCell)"
/>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:function>

<xsl:function name="f:canContinue" as="xs:boolean">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCells" as="element()*"/>
<xsl:param name="pallFillers" as="element()*"/>
<xsl:sequence select=
"empty($pEmptyCells[empty(f:cellFillers($pallFillers,.))])"
/>
<!-- -->
</xsl:function>

<xsl:function name="f:cellFillers" as="element()*">
<xsl:param name="pallFillers" as="element()*"/>
<xsl:param name="pemptyCell" as="element()"/>

<xsl:sequence select="$pallFillers[@row eq $pemptyCell/@row
and
@col eq $pemptyCell/@col
]"/>

</xsl:function>

<xsl:function name="f:bestCellsToTry" as="element()*">
<xsl:param name="pEmptyCells" as="element()*"/>
<xsl:param name="pallFillers" as="element()*"/>

<xsl:for-each select="$pEmptyCells">
<xsl:sort select="count(f:cellFillers($pallFillers,.))" order="ascending"/>

<xsl:sequence select="."/>
</xsl:for-each>

<xsl:variable name="vbestRow" as="element()+">
<xsl:for-each-group select="$pEmptyCells" group-by="@row">
<xsl:sort select="count(current-group())" order="ascending"/>
<xsl:sequence select=
"if(position() = 1)
then current-group()
else ()"/>
</xsl:for-each-group>
</xsl:variable>
<!-- Output the resulting cell -->
<xsl:for-each-group select="$vbestRow"
group-by="count(f:column($pEmptyCells, current()/@col))">
<xsl:sort select="current-grouping-key()" order="ascending"/>

<xsl:sequence select="."/>
</xsl:for-each-group>
</xsl:function>

<xsl:function name="f:makeFillers" as="element()*">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCells" as="element()*"/>

<xsl:sequence select="$pEmptyCells/f:makeFillersForCell($pFixedCells,.)"/>
</xsl:function>

<xsl:function name="f:makeFillersForCell" as="element()*">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCell" as="element()"/>

<xsl:for-each select="$vAllVals">
<xsl:if test="not(. = f:column($pFixedCells,$pEmptyCell/@col)/@val)
and
not(. = f:row($pFixedCells,$pEmptyCell/@row)/@val)
and
not(. = f:region($pFixedCells, $pEmptyCell)/@val)
">
<xsl:sequence select="f:makeCell($pEmptyCell/@row,$pEmptyCell/@col,.)"/>
</xsl:if>
</xsl:for-each>
</xsl:function>

<xsl:function name="f:tryFillers" as="item()*">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCells" as="element()*"/>
<xsl:param name="pallFillers" as="element()*"/>
<xsl:param name="pFillers" as="element()*"/>
<xsl:param name="pBestCell" as="element()"/>

<xsl:if test="exists($pFillers)">
<xsl:variable name="vtheFiller" select="$pFillers[1]"/>
<!--
<xsl:message>
Trying filler: <xsl:sequence select="$vtheFiller"/>
</xsl:message>
-->
<xsl:variable name="vreducedFilllers" as="element()*"
select="f:reduceAllFillers($pallFillers, $pBestCell,$vtheFiller)"/>
<!--
<xsl:variable name="vmadeFilllers" as="element()*"
select="f:makeFillers(($pFixedCells,$vtheFiller),$pEmptyCells[not(. is $pBestCell)])"/>
<xsl:if test="count($vmadeFilllers) != count($vreducedFilllers)">
<xsl:message>
Problem:
count($vmadeFilllers) = <xsl:value-of select="count($vmadeFilllers)"/>
count($vreducedFilllers) = <xsl:value-of select="count($vreducedFilllers)"/>

madeFillers:
<xsl:copy-of select="$vmadeFilllers"/>

reducedFillers:
<xsl:copy-of select="$vreducedFilllers"/>
</xsl:message>
</xsl:if>
-->
<xsl:variable name="vSolution" select=
"f:sudoku(($pFixedCells,$vtheFiller),$pEmptyCells[not(. is $pBestCell)],
f:reduceAllFillers($pallFillers, $pBestCell,$vtheFiller)
(: f:makeFillers(($pFixedCells,$vtheFiller),$pEmptyCells[not(. is $pBestCell)]) :)
)
"/>

<xsl:choose>
<xsl:when test="exists($vSolution)">
<xsl:sequence select="$vSolution"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select=
"f:tryFillers($pFixedCells,$pEmptyCells, $pallFillers,
$pFillers[position() gt 1],$pBestCell)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:function>

<xsl:function name="f:reduceAllFillers" as="element()*">
<xsl:param name="pallFillers" as="element()*"/>
<xsl:param name="ppickedCell" as="element()"/>
<xsl:param name="pthisFiller" as="element()"/>

<xsl:sequence select=
"$pallFillers[(@row ne $ppickedCell/@row
or
@col ne $ppickedCell/@col
)
and
(
if(@row = $ppickedCell/@row
or
@col = $ppickedCell/@col
or
(
((@row - 1) idiv 3 eq ($ppickedCell/@row -1) idiv 3)
and
((@col - 1) idiv 3 eq ($ppickedCell/@col -1) idiv 3)
)
)
then
@val ne $pthisFiller/@val
else
true()
)
]"
/>
</xsl:function>

<xsl:function name="f:column" as="element()*">
<xsl:param name="pCells" as="element()*"/>
<xsl:param name="pColno" as="xs:integer"/>

<xsl:sequence select="$pCells[@col = $pColno]"/>
</xsl:function>

<xsl:function name="f:row" as="element()*">
<xsl:param name="pCells" as="element()*"/>
<xsl:param name="pRowno" as="xs:integer"/>

<xsl:sequence select="$pCells[@row = $pRowno]"/>
</xsl:function>

<xsl:function name="f:region" as="element()*">
<xsl:param name="pCells" as="element()*"/>
<xsl:param name="paCell" as="element()"/>

<xsl:variable name="vregRowStart" as="xs:integer"
select="3*(($paCell/@row -1) idiv 3) +1"/>
<xsl:variable name="vregColStart" as="xs:integer"
select="3*(($paCell/@col -1) idiv 3) +1"/>

<xsl:sequence select=
"$pCells[xs:integer(@row) ge $vregRowStart and xs:integer(@row) lt ($vregRowStart +3)
and
xs:integer(@col) ge $vregColStart and xs:integer(@col) lt ($vregColStart +3)
]"/>
</xsl:function>

<xsl:function name="f:printBoard">
<xsl:param name="pCells" as="element()+"/>

<xsl:for-each-group select="$pCells" group-by="@row">
<xsl:sort select="current-grouping-key()"/>
<row>
<xsl:for-each select="current-group()">
<xsl:sort select="@col"/>
<xsl:value-of select=
"concat(@val, if(position() ne last()) then ', ' else ())"
/>
</xsl:for-each>
</row>
</xsl:for-each-group>
</xsl:function>
</xsl:stylesheet>

Thursday, March 16, 2006

Dimitre Novatchev's Sudoku solution

The Sudoku solving stylesheet craze has been catching on...

Dimitre Novatchev, creator of FXSL and xsl-list regular, has written a stylesheet (in typical Dimitre style) which attacks the problem from a slightly different angle.


<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:f="http://fxsl.sf.net/"
exclude-result-prefixes="f xs">

<xsl:output omit-xml-declaration="yes" indent="yes"/>

<xsl:template match="/">
<xsl:sequence
select="f:sudoku(f:cellsGroup('Fixed', /*/*),
f:cellsGroup('Empty', /*/*))"/>
</xsl:template>

<xsl:variable name="vAllVals" select="1 to 9" as="xs:integer+"/>

<xsl:function name="f:cellsGroup" as="element()*">
<xsl:param name="pgrpType" as="xs:string"/>
<xsl:param name="pRows" as="element()*"/>
<xsl:sequence select=
"for $i in 1 to count($pRows),
$vRow in $pRows[$i],
$vNum in tokenize($vRow, ',')
[if($pgrpType='Fixed')
then . ne '0'
else . eq '0'
][if($pgrpType='Empty')
then 1
else true()],
$k in index-of(tokenize($vRow, ','),$vNum)
return
f:makeCell($i,$k, xs:integer($vNum))
"/>
</xsl:function>

<xsl:function name="f:makeCell" as="element()">
<xsl:param name="pnRow" as="xs:integer"/>
<xsl:param name="pnCol" as="xs:integer"/>
<xsl:param name="pnVal" as="xs:integer"/>

<cell row="{$pnRow}" col="{$pnCol}" val="{$pnVal}"/>
</xsl:function>

<xsl:function name="f:sudoku" as="item()*">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCells" as="element()*"/>

<xsl:choose>
<xsl:when test="empty($pEmptyCells)">
<xsl:sequence select="f:printBoard($pFixedCells)"/>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vBestCells" as="element()*"
select="f:bestCellsToTry($pEmptyCells)"/>
<xsl:if test="empty($vBestCells[empty(f:fillers($pFixedCells,.))])">
<xsl:variable name="vBestCell" select="$vBestCells[1]"/>
<xsl:variable name="vFillers" as="element()+"
select="f:fillers($pFixedCells,$vBestCell)"/>

<xsl:sequence select=
"f:tryFillers($pFixedCells,$pEmptyCells, $vFillers,$vBestCell)"
/>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:function>

<xsl:function name="f:bestCellsToTry" as="element()*">
<xsl:param name="pEmptyCells" as="element()*"/>

<xsl:variable name="vbestRow" as="element()+">
<xsl:for-each-group select="$pEmptyCells" group-by="@row">
<xsl:sort select="count(current-group())" order="ascending"/>
<xsl:sequence select=
"if(position() = 1)
then current-group()
else ()"/>
</xsl:for-each-group>
</xsl:variable>
<!-- Output the resulting cell -->
<xsl:for-each-group select="$vbestRow"
group-by="count(f:column($pEmptyCells, current()/@col))">
<xsl:sort select="current-grouping-key()" order="ascending"/>

<xsl:sequence select="."/>
</xsl:for-each-group>
</xsl:function>

<xsl:function name="f:fillers" as="element()*">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCell" as="element()"/>

<xsl:for-each select="$vAllVals">
<xsl:if test="not(. = f:column($pFixedCells,$pEmptyCell/@col)/@val)
and
not(. = f:row($pFixedCells,$pEmptyCell/@row)/@val)
and
not(. = f:region($pFixedCells, $pEmptyCell)/@val)
">
<xsl:sequence
select="f:makeCell($pEmptyCell/@row,$pEmptyCell/@col,.)"/>
</xsl:if>
</xsl:for-each>
</xsl:function>

<xsl:function name="f:tryFillers" as="item()*">
<xsl:param name="pFixedCells" as="element()*"/>
<xsl:param name="pEmptyCells" as="element()*"/>
<xsl:param name="pFillers" as="element()*"/>
<xsl:param name="pBestCell" as="element()"/>

<xsl:if test="exists($pFillers)">
<xsl:variable name="vtheFiller" select="$pFillers[1]"/>

<xsl:variable name="vSolution" select=
"f:sudoku(($pFixedCells,$vtheFiller),$pEmptyCells[not(. is $pBestCell)])
"/>

<xsl:choose>
<xsl:when test="exists($vSolution)">
<xsl:sequence select="$vSolution"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select=
"f:tryFillers($pFixedCells,$pEmptyCells, $pFillers[position()
gt 1],$pBestCell)"/>
</xsl:otherwise>
</xsl:choose>
</xsl:if>
</xsl:function>

<xsl:function name="f:column" as="element()*">
<xsl:param name="pCells" as="element()*"/>
<xsl:param name="pColno" as="xs:integer"/>

<xsl:sequence select="$pCells[@col = $pColno]"/>
</xsl:function>

<xsl:function name="f:row" as="element()*">
<xsl:param name="pCells" as="element()*"/>
<xsl:param name="pRowno" as="xs:integer"/>

<xsl:sequence select="$pCells[@row = $pRowno]"/>
</xsl:function>

<xsl:function name="f:region" as="element()*">
<xsl:param name="pCells" as="element()*"/>
<xsl:param name="paCell" as="element()"/>

<xsl:variable name="vregRowStart" as="xs:integer"
select="3*(($paCell/@row -1) idiv 3) +1"/>
<xsl:variable name="vregColStart" as="xs:integer"
select="3*(($paCell/@col -1) idiv 3) +1"/>

<xsl:sequence select=
"$pCells[xs:integer(@row) ge $vregRowStart and xs:integer(@row)
lt ($vregRowStart +3)
and
xs:integer(@col) ge $vregColStart and xs:integer(@col) lt
($vregColStart +3)
]"/>
</xsl:function>

<xsl:function name="f:printBoard">
<xsl:param name="pCells" as="element()+"/>

<xsl:for-each-group select="$pCells" group-by="@row">
<xsl:sort select="current-grouping-key()"/>
<row>
<xsl:for-each select="current-group()">
<xsl:sort select="@col"/>
<xsl:value-of select=
"concat(@val, if(position() ne last()) then ', ' else ())"
/>
</xsl:for-each>
</row>
</xsl:for-each-group>
</xsl:function>
</xsl:stylesheet>

Wednesday, March 15, 2006

xq2xml

David Carlisle is currently undertaking a project to convert XQuery expressions into other languages, by first converting the query to XML and then on from there.

The "xq2xml" page can be found at http://monet.nag.co.uk/xq2xml/index.html

His notes on the differences between XQuery and XSLT can be found at http://monet.nag.co.uk/xq2xml/xq2xslnotes.html

It's all very interesting reading, but I kept asking myself "why?" so I mailed him asking if there's a real world application behind all this...

David's reply: "is 'teach myself xquery' a real world application?"

I guess the "because it's there" mentality applies to programming too :)

Tuesday, March 14, 2006

David Carlisle's XQuery conversion of the Sudoku stylesheet

After posting the Sudoku stylesheet to xsl-list, David Carlisle mailed me back off-list with the XQuery equivalent, and very fine it is too.

I did start doing some performance comparisons between this and the XSLT 2.0 version, but when the first few results were produced I remembered Saxon compiles much of it into the same internal represenation...


declare namespace fn = "sudoku";

declare variable $board as xs:integer+ := (
1,0,0, 3,0,0, 6,0,0,
0,2,0, 5,0,0, 0,0,4,
0,0,9, 0,0,0, 5,2,0,

0,0,0, 9,6,3, 0,0,0,
7,1,6, 0,0,0, 0,0,0,
0,0,0, 0,8,0, 0,4,0,

9,0,0, 0,0,5, 3,0,7,
8,0,0, 4,0,6, 0,0,0,
3,5,0, 0,0,0, 0,0,1);

declare variable $rowStarts as xs:integer+ := (1, 10, 19, 28, 37, 46, 55, 64,73);

declare variable $groups as xs:integer+ := (
1,1,1, 2,2,2, 3,3,3,
1,1,1, 2,2,2, 3,3,3,
1,1,1, 2,2,2, 3,3,3,

4,4,4, 5,5,5, 6,6,6,
4,4,4, 5,5,5, 6,6,6,
4,4,4, 5,5,5, 6,6,6,

7,7,7, 8,8,8, 9,9,9,
7,7,7, 8,8,8, 9,9,9,
7,7,7, 8,8,8, 9,9,9
);


declare function fn:getRow ($board as xs:integer+, $index as xs:integer) as xs:integer+ {
let $rowStart := floor(($index - 1) div 9) * 9
return
$board[position() > $rowStart and position() <= $rowStart + 9]
};


declare function fn:getCol ($board as xs:integer+, $index as xs:integer) as xs:integer+ {
let $gap := ($index - 1) mod 9,
$colIndexes := for $x in $rowStarts return $x + $gap
return
$board[position() = $colIndexes]};


declare function fn:getGroup ($board as xs:integer+, $index as xs:integer) as xs:integer+ {
let $group := $groups[$index]
return
$board[for $x in position() return $groups[$x]= $group]
};


declare function fn:getAllowedValues ($board as xs:integer+, $index as xs:integer) as xs:integer* {
let $existingValues := (fn:getRow($board,
$index), fn:getCol($board, $index), fn:getGroup($board, $index))
return
for $x in (1 to 9) return
if (not($x = $existingValues))
then $x
else ()
};


declare function fn:tryValues($board as xs:integer+,
$emptyCells as xs:integer+,
$possibleValues as xs:integer+) as xs:integer* {
let $index as xs:integer := $emptyCells[1],
$newBoard as xs:integer+ := ($board[position() <$index],
$possibleValues[1], $board[position() > $index]),
$result as xs:integer* := fn:populateValues($newBoard, $emptyCells[position() != 1])
return
if (empty($result)) then
if (count($possibleValues) > 1) then
fn:tryValues($board, $emptyCells, $possibleValues[position() != 1])
else
()
else
$result
};

declare function fn:populateValues($board as xs:integer+,
$emptyCells as xs:integer*) as xs:integer*{

if (not(empty($emptyCells)))
then
let $index as xs:integer :=$emptyCells[1],
$possibleValues as xs:integer* := distinct-values(fn:getAllowedValues($board, $index))
return
if (count($possibleValues) > 1)
then
fn:tryValues($board, $emptyCells, $possibleValues)
else if (count($possibleValues) = 1)
then
let $newBoard as xs:integer+ :=($board[position() <
$index], $possibleValues[1], $board[position() > $index])
return
fn:populateValues($newBoard, $emptyCells[position() != 1])
else ()
else
$board
};

declare function fn:solveSudoku ($startBoard as xs:integer+) as xs:integer+{
let $emptyCells as xs:integer* :=for $x in (1 to 81) return if
($startBoard[$x] = 0) then $x else (),
$endBoard as xs:integer* :=fn:populateValues($startBoard,$emptyCells)
return
if (empty($endBoard))
then
$startBoard
else
$endBoard};


declare function fn:drawResult ($board as xs:integer+) as element(){
<html>
<head>
<title>Sudoku - XSLT</title>
<style>
table {{ border-collapse: collapse;
border: 1px solid black; }}
td {{ padding: 10px; }}
.norm {{ border-left: 1px solid #CCC;
border-top: 1px solid #CCC;}}
.csep {{ border-left: 1px solid black; }}
.rsep {{ border-top: 1px solid black; }}
</style>
</head>
<body>
{fn:drawBoard($board)}
</body>
</html>
};

declare function fn:drawBoard ($board as xs:integer+) as element(){
<table>
{ for $i in 1 to 9
return
<tr>
{for $j at $p in 1 to 9
let $pos := (($i - 1) * 9) + $j
return
<td class="{if ($p mod 3 = 1) then 'csep' else ('norm')}
{if ($i mod 3 = 1) then 'rsep' else ('norm')}">
{$board[$pos]}
</td>
}
</tr>
}
</table>
};


fn:drawResult(fn:solveSudoku($board))

Friday, March 10, 2006

A Sudoku solution in XSLT 2.0


Here's a stylesheet I've written to solve a Sudoku puzzle. It came about when some friends and I were talking about Sudoku over lunch, and one them pointed at me and said "aah I bet XSLT can't do that!" .... which laid down the challenge.

After studying Michael Kay's Knights Tour stylesheet (in the samples directory of the Saxon download) for some time I was able to get going on this. This uses the same recursive technique, trying a particular "route" and then backtracking to the point where a different route can be taken when the last one failed. It uses an intelligent brute force technique (if those aren't mutually exclusive...) - it finds all of the available values for a cell, and then tries each one in turn until the puzzle is solved.

Once the stylesheet was written, it was quite interesting to try out different approaches to get the best performance. For example, the same friend (who reckons he's some kind of Sudoku expert) says that once you have the center squares its just a matter of time before you solve the rest of the board. So instead of just processing the "empty" squares (the zero values here) in the order they appear from 1 to 81, I processed the center squares first followed by the rest. This led to quite an improvement in performance...

It didn't make much sense to sense to then process the empty cells in the top-left group as they aren't affect by the center group, so I modified it to process the center group, then the top-middle group, followed by the rest. This has given the best performance so far...

Anyway, here it is. There are some test boards at the bottom of the stylesheet so just change the parameter value in the main template to use one of them.



<xsl:stylesheet version="2.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fn="sudoku">

<xsl:param name="board" select="(
1,0,0, 3,0,0, 6,0,0,
0,2,0, 5,0,0, 0,0,4,
0,0,9, 0,0,0, 5,2,0,

0,0,0, 9,6,3, 0,0,0,
7,1,6, 0,0,0, 0,0,0,
0,0,0, 0,8,0, 0,4,0,

9,0,0, 0,0,5, 3,0,7,
8,0,0, 4,0,6, 0,0,0,
3,5,0, 0,0,0, 0,0,1
)" as="xs:integer+"/>

<xsl:param name="verbose" select="false()" as="xs:boolean"/>

<xsl:variable name="rowStarts" select="(1, 10, 19, 28, 37, 46, 55, 64, 73)" as="xs:integer+"/>
<xsl:variable name="topLeftGroup" select="(1, 2, 3, 10, 11, 12, 19, 20, 21)" as="xs:integer+"/>
<xsl:variable name="topGroup" select="(4, 5, 6, 13, 14, 15, 22, 23, 24)" as="xs:integer+"/>
<xsl:variable name="topRightGroup" select="(7, 8, 9, 16, 17, 18, 25, 26, 27)" as="xs:integer+"/>
<xsl:variable name="midLeftGroup" select="(28, 29, 30, 37, 38, 39, 46, 47, 48)" as="xs:integer+"/>
<xsl:variable name="center" select="(31, 32, 33, 40, 41, 42, 49, 50, 51)" as="xs:integer+"/>
<xsl:variable name="midRightGroup" select="(34, 35, 36, 43, 44, 45, 52, 53, 54)" as="xs:integer+"/>
<xsl:variable name="bottomLeftGroup" select="(55, 56, 57, 64, 65, 66, 73, 74, 75)" as="xs:integer+"/>
<xsl:variable name="bottomGroup" select="(58, 59, 60, 67, 68, 69, 76, 77, 78)" as="xs:integer+"/>
<xsl:variable name="bottomRightGroup" select="(61, 62, 63, 70, 71, 72, 79, 80, 81)" as="xs:integer+"/>

<xsl:function name="fn:getRow" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:variable name="rowStart" select="floor(($index - 1) div 9) * 9"/>
<xsl:sequence select="$board[position() > $rowStart and position() <= $rowStart + 9]"/>
</xsl:function>

<xsl:function name="fn:getCol" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:variable name="gap" select="($index - 1) mod 9"/>
<xsl:variable name="colIndexes" select="for $x in $rowStarts return $x + $gap" as="xs:integer+"/>
<xsl:sequence select="$board[some $x in position() satisfies $x = $colIndexes]"/>
</xsl:function>

<xsl:function name="fn:getGroup" as="xs:integer+">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:choose>
<xsl:when test="$index = $topLeftGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $topLeftGroup]"/>
</xsl:when>
<xsl:when test="$index = $topGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $topGroup]"/>
</xsl:when>
<xsl:when test="$index = $topRightGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $topRightGroup]"/>
</xsl:when>
<xsl:when test="$index = $midLeftGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $midLeftGroup]"/>
</xsl:when>
<xsl:when test="$index = $center">
<xsl:sequence select="$board[some $x in position() satisfies $x = $center]"/>
</xsl:when>
<xsl:when test="$index = $midRightGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $midRightGroup]"/>
</xsl:when>
<xsl:when test="$index = $bottomLeftGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $bottomLeftGroup]"/>
</xsl:when>
<xsl:when test="$index = $bottomGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $bottomGroup]"/>
</xsl:when>
<xsl:when test="$index = $bottomRightGroup">
<xsl:sequence select="$board[some $x in position() satisfies $x = $bottomRightGroup]"/>
</xsl:when>
</xsl:choose>
</xsl:function>

<xsl:function name="fn:getAllowedValues" as="xs:integer*">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="index" as="xs:integer+"/>
<xsl:variable name="existingValues" select="(fn:getRow($board, $index), fn:getCol($board, $index), fn:getGroup($board, $index))" as="xs:integer+"/>
<xsl:sequence select="for $x in (1 to 9) return
if (not($x = $existingValues))
then $x
else ()"/>
</xsl:function>

<xsl:function name="fn:tryValues" as="xs:integer*">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="emptyCells" as="xs:integer+"/>
<xsl:param name="possibleValues" as="xs:integer+"/>

<xsl:variable name="index" select="$emptyCells[1]" as="xs:integer"/>
<xsl:variable name="newBoard" select="($board[position() < $index], $possibleValues[1], $board[position() > $index])" as="xs:integer+"/>

<xsl:if test="$verbose">
<xsl:message>Trying
<xsl:value-of select="$possibleValues[1]"/> out of a possible
<xsl:value-of select="$possibleValues"/> at index
<xsl:value-of select="$index"/>
</xsl:message>
</xsl:if>

<xsl:variable name="result" select="fn:populateValues($newBoard, $emptyCells[position() != 1])" as="xs:integer*"/>

<xsl:sequence select="if (empty($result)) then
if (count($possibleValues) > 1) then
fn:tryValues($board, $emptyCells, $possibleValues[position() != 1])
else
()
else
$result"/>
</xsl:function>

<xsl:function name="fn:populateValues" as="xs:integer*">
<xsl:param name="board" as="xs:integer+"/>
<xsl:param name="emptyCells" as="xs:integer*"/>

<xsl:choose>
<xsl:when test="not(empty($emptyCells))">
<xsl:variable name="index" select="$emptyCells[1]" as="xs:integer"/>
<xsl:variable name="possibleValues" select="distinct-values(fn:getAllowedValues($board, $index))" as="xs:integer*"/>
<xsl:choose>
<xsl:when test="count($possibleValues) > 1">
<xsl:sequence select="fn:tryValues($board, $emptyCells, $possibleValues)"/>
</xsl:when>
<xsl:when test="count($possibleValues) = 1">
<xsl:variable name="newBoard" select="($board[position() < $index], $possibleValues[1], $board[position() > $index])" as="xs:integer+"/>

<xsl:if test="$verbose">
<xsl:message>Only one value
<xsl:value-of select="$possibleValues[1]"/> for index
<xsl:value-of select="$index"/>
</xsl:message>
</xsl:if>

<xsl:sequence select="fn:populateValues($newBoard, $emptyCells[position() != 1])"/>
</xsl:when>
<xsl:otherwise>
<xsl:if test="$verbose">
<xsl:message>! Cannot go any further !</xsl:message>
</xsl:if>
<xsl:sequence select="()"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:message>Done!</xsl:message>
<xsl:sequence select="$board"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>

<xsl:function name="fn:solveSudoku" as="xs:integer+">
<xsl:param name="startBoard" as="xs:integer+"/>

<!-- First process the center cells, then the top, then the rest of the board.
This should give better performance than starting top-left and working from there. -->
<xsl:variable name="theRest" select="for $x in 1 to 81 return $x[not($x = $center)][not($x = $topGroup)]" as="xs:integer+"/>

<xsl:variable name="emptyCells" select="for $x in ($center, $topGroup, $theRest) return if ($startBoard[$x] = 0) then $x else ()" as="xs:integer*"/>

<xsl:variable name="endBoard" select="fn:populateValues($startBoard, $emptyCells)" as="xs:integer*"/>

<xsl:choose>
<xsl:when test="empty($endBoard)">
<xsl:message>! Invalid board - The starting board is not correct</xsl:message>
<xsl:sequence select="$startBoard"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$endBoard"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>

<xsl:template match="/" name="main">
<xsl:call-template name="drawResult">
<xsl:with-param name="board" select="fn:solveSudoku($board)"/>
</xsl:call-template>
</xsl:template>

<xsl:template name="drawResult">
<xsl:param name="board" as="xs:integer+"/>
<html>
<head>
<title>Sudoku - XSLT</title>
<style>
table { border-collapse: collapse;
border: 1px solid black; }
td { padding: 10px; }
.norm { border-left: 1px solid #CCC;
border-top: 1px solid #CCC;}
.csep { border-left: 1px solid black; }
.rsep { border-top: 1px solid black; }
</style>
</head>
<body>
<xsl:call-template name="drawBoard">
<xsl:with-param name="board" select="$board"/>
</xsl:call-template>
</body>
</html>
</xsl:template>

<xsl:template name="drawBoard">
<xsl:param name="board" as="xs:integer+"/>
<table>
<xsl:for-each select="1 to 9">
<xsl:variable name="i" select="."/>
<tr>
<xsl:for-each select="1 to 9">
<xsl:variable name="pos" select="(($i - 1) * 9) + ."/>
<td class="{if (position() mod 3 = 1) then 'csep' else ('norm')} {if ($i mod 3 = 1) then 'rsep' else ('norm')}">
<xsl:value-of select="$board[$pos]"/>
</td>
</xsl:for-each>
</tr>
</xsl:for-each>
</table>
</xsl:template>

<!-- Easy board, 32 existing numbers -->
<xsl:variable name="testBoard1" select="(
0,2,0, 0,0,0, 0,3,6,
0,0,7, 4,0,0, 0,9,0,
0,0,5, 6,0,0, 0,4,8,

0,0,0, 9,3,0, 0,1,2,
2,9,0, 0,0,0, 0,7,5,
1,5,0, 0,8,2, 0,0,0,

6,7,0, 0,0,9, 1,0,0,
0,3,0, 0,0,7, 6,0,0,
4,8,0, 0,0,0, 0,2,0
)" as="xs:integer+"/>

<!-- Hard board, 24 existing numbers -->
<xsl:variable name="testBoard2" select="(
1,0,0, 5,6,0, 0,0,0,
9,0,0, 0,0,0, 2,0,8,
0,0,0, 0,0,0, 7,0,0,

0,8,0, 9,0,7, 0,0,2,
2,0,0, 0,0,0, 0,0,1,
6,0,0, 3,0,2, 0,4,0,

0,0,5, 0,0,0, 0,0,0,
4,0,3, 0,0,0, 0,0,9,
0,0,0, 0,4,1, 0,0,6
)" as="xs:integer+"/>

<!-- Extremely Hard board -->
<xsl:variable name="testBoard3" select="(
4,0,6, 7,8,0, 2,0,0,
0,5,7, 0,0,0, 0,0,0,
8,0,0, 0,0,0, 4,0,0,

0,0,0, 0,5,0, 0,9,0,
9,1,2, 0,0,0, 0,0,0,
0,4,0, 0,1,0, 0,0,3,

0,0,4, 3,0,0, 0,2,0,
7,0,3, 0,0,6, 0,0,0,
0,0,0, 5,0,7, 6,0,8
)" as="xs:integer+"/>

<!-- Failing board, an erroneous 9 has been added at index 1 -->
<xsl:variable name="testBoard_Fail" select="(
9,2,0, 0,0,0, 0,3,6,
0,0,7, 4,0,0, 0,9,0,
0,0,5, 6,0,0, 0,4,8,

0,0,0, 9,3,0, 0,1,2,
2,9,0, 0,0,0, 0,7,5,
1,5,0, 0,8,2, 0,0,0,

6,7,0, 0,0,9, 1,0,0,
0,3,0, 0,0,7, 6,0,0,
4,8,0, 0,0,0, 0,2,0
)" as="xs:integer+"/>

</xsl:stylesheet>

Kernow 1.2

I've released Kernow 1.2 which, after fixing a broken zip, is available from http://sourceforge.net/projects/easytransformer/

(It used to be called EasyTransformer, but since it nows supports XQuery and I really didn't like the name Easy.... EZ... eee zee... etc a name change was long overdue).

Kernow is an open source graphical front end for Saxon, the XSLT and XQuery processor. It's written in Java, with the Swing UI created using Matisse - the new UI editor in Netbeans 5. For me eclipse still wins on the IDE front (as it's just better for everyday usage), but Matisse is really special so it's worth putting up with Netbeans just for that.

Kernow basically gives you file choosers for the files involved in the transform, parameter and named template discovery, a caching entity resovler and improved directory transforms through compiling the stylesheet. It also stores all of the files involved and makes them selectable through combo boxes to reduce the amout of setup time when you get going. Hopefully it's useful to someone...
The first post. I've set this blog up as a place to link through to some of the things I'm doing...