How to read an xsd:dateTime with msxml? - c++

I'm using the msxml to parse an xml file. Language is C++. The xml file contains some dates and times using the xsd:dateTime format (Something like that: 2009-04-29T12:00:00Z)
Is there an easy way to convert xsd:dateTime to something like SYSTEMTIME, FILETIME or VariantTime?

Maybe this helps you: Using strptime to parse ISO 8601 formated timestamps on ioncannon.net.

You can use a javascript:
<xsl:transform
id="integra-transformer"
version="1.0"
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:date="urn:date-scripts">
<xsl:output method="xml" indent="yes" />
<msxsl:script
implements-prefix="date"
language="javascript">
<![CDATA[
Date.prototype.toISO8601ShortString = function () {
var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; }
var str = "";
var date = new Date();
str += date.getUTCFullYear();
str += "-" + zeropad(date.getUTCMonth() + 1);
str += "-" + zeropad(date.getUTCDate());
str += "T" + zeropad(date.getUTCHours()) +
":" + zeropad(date.getUTCMinutes());
return str;
}
function getToday()
{
var d;
d = new Date();
return(d.toISO8601ShortString());
}
]]>
</msxsl:script>
<xsl:template match="/">
<date>
<xsl:value-of select="date:getToday()"/>
</date>
</xsl:template>
</xsl:transform>
Don't forget to enable scripts befor you do the transformation:
xslDom.setProperty("AllowXsltScript", true)

Related

Increment the repeated value in xslt inside for-each

Here I want to count/Increment the repeated emp_no using for each.
and I just want to concatenate emp_no with duplicate no count..
Here is my XSLT style sheet.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array" exclude-result-prefixes="#all"
version="3.0">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="input"/>
<xsl:variable name="newline" select="'
'"/>
<xsl:template match="/" name="xsl:initial-template">
<xsl:variable name="input-as-map" select="parse-json($input)" as="map(*)"/>
<root>
<file1>
<xsl:for-each select="$input-as-map?file1?*">
<xsl:value-of select="$newline"/>
<xsl:value-of select="concat(?emp_no,',',count(?emp_no))"/>
<xsl:value-of select="$newline"/>
</xsl:for-each>
</file1>
</root>
</xsl:template>
</xsl:stylesheet>
input json:-
{
"file1": [
{
"emp_no": "0002"
},
{
"emp_no": "0009"
},
{
"emp_no": "0002"
},
{
"emp_no": "0009"
},
{
"emp_no": "0009"
}
]
}
Expected output:-
<root>
<file1>
0002,1
0009,1
0002,2
0009,2
0009,3
</file1>
</root>
Any suggestions also would be helpful.
One solution would be to replace the xsl:for-each with xsl:iterate, maintaining a parameter value containing a map from emp_no to an integer counter.
Another would be to convert the array to XML and use xsl:number.
Or you could use fn:fold-left(), again maintaining a map from emp_no to an integer counter as you step through the array.
None of them particularly easy, I'm afraid: perhaps someone else has a better idea.
Here is Mike's third suggestion spelled out:
<file>
<xsl:value-of
select="fold-left(
parse-json($json)?file1?*,
map{},
function($am, $m) {
let $c := head($am)($m?emp_no),
$new-am := map:put(head($am), $m?emp_no, if (empty($c)) then 1 else $c + 1)
return ($new-am, tail($am), $m?emp_no || ',' || $new-am($m?emp_no))
}
) => tail()"
separator="
"/>
</file>

Date time conversion results in FORG0001 Invalid dateTime value - XSLT 3.0

Resources:
Java 1.8
Saxon-HE-10.3
XSLT3.0
I am merging 2 XML files and want to do date manipulation in the merged output file xml.
I am taking the 2 files as input using java and generate the merged xml - This works fine.
Now, I added the date manipulation in the xsl after the merging operation.
Date Manipulation - convert the given date to epoch/milliseconds
Date format which comes with the input file
<PROP NAME="START_DATE">
<PVAL>16-Aug-2018</PVAL>
</PROP>
The issue is when I try to convert the given date to dateTime or convert to epoch, I get the below error on compilation.
Error at char 10 in expression in xsl:value-of/#select on line 60 column 105 of CASTransform.xsl: FORG0001 Invalid dateTime value "/01--20T00:00:00" (Non-numeric year component) at template reformat-date on line 55 of CASTransform.xsl:
invoked by xsl:call-template at file:/home/Merger/scripts/CASTransform.xsl#50 In template rule with match="element(Q{}RECORDS)/element(Q{}RECORD)/element(Q{}PROP)[(Q{http://www.w3.org/2001/XMLSchema}string(data(attribute::attribute(Q{}NAME)))) eq "START_DATE"]/element(Q{}PVAL)" on line 48 of CASTransform.xsl
invoked by xsl:apply-templates at file:/home/Merger/scripts/CASTransform.xsl#45 In template rule with match="(element()|(text()|(comment()|processing-instruction())))" on line 43 of CASTransform.xsl
invoked by xsl:apply-templates at file:/home/Merger/scripts/CASTransform.xsl#35 at template main on line 14 of CASTransform.xsl: Exception in thread "main" net.sf.saxon.s9api.SaxonApiException: Invalid dateTime value "/01--20T00:00:00" (Non-numeric year component)
at net.sf.saxon.s9api.Xslt30Transformer.callTemplate(Xslt30Transformer.java:488)
XSL Code
<xsl:stylesheet version="3.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:map="http://www.w3.org/2005/xpath-functions/map"
xmlns:array="http://www.w3.org/2005/xpath-functions/array"
exclude-result-prefixes="#all">
<xsl:mode on-no-match="shallow-copy"/>
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:param name="mapData"/>
<xsl:template match="/" name="main">
<RECORDS>
<xsl:variable name="file1" select="map:get($mapData, '1')" />
<xsl:variable name="input-doc1" as="document-node()" select="doc($file1)"/>
<xsl:variable name="file2" select="map:get($mapData, '2')" />
<xsl:variable name="input-doc2" as="document-node()" select="doc($file2)"/>
<xsl:merge>
<xsl:merge-source name="doc1" select="$input-doc1/RECORDS/RECORD">
<xsl:merge-key select="PROP[#NAME = 'Id']/PVAL"></xsl:merge-key>
</xsl:merge-source>
<xsl:merge-source name="doc2" select="$input-doc2/RECORDS/RECORD">
<xsl:merge-key select="PROP[#NAME = 'Id']/PVAL"></xsl:merge-key>
</xsl:merge-source>
<xsl:merge-action>
<xsl:if test="current-merge-group('doc1')">
<xsl:copy>
<xsl:apply-templates select="*, current-merge-group('doc2')/(* except PROP[#NAME = 'Id'])"/>
</xsl:copy>
</xsl:if>
</xsl:merge-action>
</xsl:merge>
</RECORDS>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:copy>
<xsl:apply-templates select="#*|node()" />
</xsl:copy>
</xsl:template>
<xsl:template match="RECORDS/RECORD/PROP[#NAME = 'START_DATE']/PVAL | RECORDS/RECORD/PROP[#NAME = 'END_DATE']/PVAL">
<xsl:element name="{name()}">
<xsl:call-template name="reformat-date">
<xsl:with-param name="date" select="." />
</xsl:call-template>
</xsl:element>
</xsl:template>
<xsl:template name="reformat-date">
<xsl:param name="date" />
<xsl:variable name="dd" select="substring($date,1,2)" />
<xsl:variable name="mmm" select="upper-case(substring($date,4,3))" />
<xsl:variable name="yyyy" select="substring($date,8,4)" />
<xsl:variable name="mmm">
<xsl:choose>
<xsl:when test="$mmm = 'JAN'">01</xsl:when>
<xsl:when test="$mmm = 'FEB'">02</xsl:when>
<xsl:when test="$mmm = 'MAR'">03</xsl:when>
<xsl:when test="$mmm = 'APR'">04</xsl:when>
<xsl:when test="$mmm = 'MAY'">05</xsl:when>
<xsl:when test="$mmm = 'JUN'">06</xsl:when>
<xsl:when test="$mmm = 'JUL'">07</xsl:when>
<xsl:when test="$mmm = 'AUG'">08</xsl:when>
<xsl:when test="$mmm = 'SEP'">09</xsl:when>
<xsl:when test="$mmm = 'OCT'">10</xsl:when>
<xsl:when test="$mmm = 'NOV'">11</xsl:when>
<xsl:when test="$mmm = 'DEC'">12</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:variable name="temp1" as="xs:string" select="string-join(($yyyy,$mmm,$dd),'-')" />
<xsl:value-of select="$temp1" />
</xsl:template>
</xsl:stylesheet>
The above XSL code works perfectly fine and the $temp1 gives proper date like 2018-08-16.
Now, when I try to convert this to dateTime variable I get the mentioned compilation error.
Code which gives error:
<xsl:variable name="temp2" as="xs:dateTime" select="xs:dateTime(concat($temp1,'T00:00:00'))"/>
and tried this as well :
<xsl:value-of select="floor((xs:dateTime(concat($temp1,'T00:00:00')) - xs:dateTime('1970-01-01T00:00:00')) div xs:dayTimeDuration('PT1S')) "/>
Requesting your help as this is my first XSL project.
Full Stacktrace:
Error at char 10 in expression in xsl:value-of/#select on line 60 column 105 of CASTransform.xsl:
FORG0001 Invalid dateTime value "/01--20T00:00:00" (Non-numeric year component)
at template reformat-date on line 55 of CASTransform.xsl:
invoked by xsl:call-template at file:/home/Merger/scripts/CASTransform.xsl#50
In template rule with match="element(Q{}RECORDS)/element(Q{}RECORD)/element(Q{}PROP)[(Q{http://www.w3.org/2001/XMLSchema}string(data(attribute::attribute(Q{}NAME)))) eq "START_DATE"]/element(Q{}PVAL)" on line 48 of CASTransform.xsl
invoked by xsl:apply-templates at file:/home/Merger/scripts/CASTransform.xsl#45
In template rule with match="(element()|(text()|(comment()|processing-instruction())))" on line 43 of CASTransform.xsl
invoked by xsl:apply-templates at file:/home/Merger/scripts/CASTransform.xsl#35
at template main on line 14 of CASTransform.xsl:
Exception in thread "main" net.sf.saxon.s9api.SaxonApiException: Invalid dateTime value "/01--20T00:00:00" (Non-numeric year component)
at net.sf.saxon.s9api.Xslt30Transformer.callTemplate(Xslt30Transformer.java:488)
at com.tracer.Merger.CASExportInput.trans(CASExportInput.java:38)
at com.tracer.Merger.CASExportInput.main(CASExportInput.java:58)
Caused by: ValidationException: Invalid dateTime value "/01--20T00:00:00" (Non-numeric year component)
at net.sf.saxon.type.ValidationFailure.makeException(ValidationFailure.java:406)
at net.sf.saxon.expr.CastExpression.doCast(CastExpression.java:385)
at net.sf.saxon.expr.CastExpression.evaluateItem(CastExpression.java:402)
at net.sf.saxon.expr.CastExpression.evaluateItem(CastExpression.java:30)
at net.sf.saxon.expr.Expression.iterate(Expression.java:872)
at net.sf.saxon.expr.AtomicSequenceConverter.iterate(AtomicSequenceConverter.java:304)
at net.sf.saxon.expr.Expression.process(Expression.java:949)
at net.sf.saxon.expr.instruct.ValueOf.processLeavingTail(ValueOf.java:340)
at net.sf.saxon.expr.LetExpression.processLeavingTail(LetExpression.java:746)
at net.sf.saxon.expr.instruct.Block.processLeavingTail(Block.java:752)
at net.sf.saxon.expr.instruct.NamedTemplate.expand(NamedTemplate.java:264)
at net.sf.saxon.expr.instruct.CallTemplate$CallTemplatePackage.processLeavingTail(CallTemplate.java:549)
at net.sf.saxon.trans.Mode.applyTemplates(Mode.java:478)
at net.sf.saxon.expr.instruct.ApplyTemplates.apply(ApplyTemplates.java:351)
at net.sf.saxon.expr.instruct.ApplyTemplates.process(ApplyTemplates.java:285)
at net.sf.saxon.expr.instruct.ElementCreator.processLeavingTail(ElementCreator.java:352)
at net.sf.saxon.expr.instruct.Copy.processLeavingTail(Copy.java:429)
at net.sf.saxon.expr.instruct.TemplateRule.applyLeavingTail(TemplateRule.java:384)
at net.sf.saxon.trans.Mode.applyTemplates(Mode.java:568)
at net.sf.saxon.expr.instruct.ApplyTemplates.apply(ApplyTemplates.java:351)
at net.sf.saxon.expr.instruct.ApplyTemplates.process(ApplyTemplates.java:285)
at net.sf.saxon.expr.instruct.ElementCreator.processLeavingTail(ElementCreator.java:352)
at net.sf.saxon.expr.instruct.Copy.processLeavingTail(Copy.java:429)
at net.sf.saxon.expr.instruct.Instruction.process(Instruction.java:142)
at net.sf.saxon.expr.parser.ExpressionTool.getIteratorFromProcessMethod(ExpressionTool.java:643)
at net.sf.saxon.expr.instruct.Instruction.iterate(Instruction.java:374)
at net.sf.saxon.expr.instruct.Choose.iterate(Choose.java:1019)
at net.sf.saxon.expr.sort.MergeInstr.lambda$iterate$0(MergeInstr.java:543)
at net.sf.saxon.expr.ContextMappingIterator.next(ContextMappingIterator.java:61)
at net.sf.saxon.om.SequenceIterator.forEachOrFail(SequenceIterator.java:135)
at net.sf.saxon.expr.sort.MergeInstr.processLeavingTail(MergeInstr.java:823)
at net.sf.saxon.expr.instruct.Instruction.process(Instruction.java:142)
at net.sf.saxon.expr.LetExpression.process(LetExpression.java:625)
at net.sf.saxon.expr.instruct.ElementCreator.processLeavingTail(ElementCreator.java:352)
at net.sf.saxon.expr.instruct.ElementCreator.processLeavingTail(ElementCreator.java:298)
at net.sf.saxon.expr.instruct.NamedTemplate.expand(NamedTemplate.java:264)
at net.sf.saxon.trans.XsltController.callTemplate(XsltController.java:850)
at net.sf.saxon.s9api.Xslt30Transformer.callTemplate(Xslt30Transformer.java:480)
... 2 more
Java code which serves the Input files :
package com.tracer.Merger.CASMerger;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
import javax.xml.transform.stream.StreamSource;
import net.sf.saxon.s9api. * ;
public class CASExportInput {
public static void trans(String XSLFile, String File1, String File2, String OutputFileName, String OutputFilePath) throws SaxonApiException {
String MergedFile = OutputFilePath.concat(OutputFileName);
Processor processor = new Processor(false);
XsltCompiler compiler = processor.newXsltCompiler();
XsltExecutable exp = compiler.compile(new StreamSource(new File(XSLFile)));
Serializer out = processor.newSerializer(new File(MergedFile));
out.setOutputProperty(Serializer.Property.METHOD, "xml");
out.setOutputProperty(Serializer.Property.INDENT, "yes");
Xslt30Transformer trans = exp.load30();
Map < String,
String > mapData = new HashMap < String,
String > ();
mapData.put("1", File1);
mapData.put("2", File2);
HashMap < QName,
XdmValue > parameters = new HashMap < >();
parameters.put(new QName("mapData"), XdmMap.makeMap(mapData));
trans.setStylesheetParameters(parameters);
trans.callTemplate(new QName("main"), out);
System.out.println("Output written to : " + MergedFile);
}
public static void main(String args[]) throws SaxonApiException {
if (args.length < 3) System.out.println("\nPlease check if you have entered arguments properly...");
String XSLFile = args[0].toString().trim();
String File1 = args[1].toString().trim();
String File2 = args[2].toString().trim();
String FileName1 = File1.substring(File1.lastIndexOf("/") + 1, File1.length() - 4);
String FileName2 = File2.substring(File2.lastIndexOf("/") + 1, File2.length() - 4);
String OutputFilePath = File1.substring(0, File1.lastIndexOf("/") + 1);
String OutputFileName = FileName1.concat("-" + FileName2 + ".xml");
trans(XSLFile, File1, File2, OutputFileName, OutputFilePath);
}
}
Not sure what the issue was. But, I uninstalled and re-installed Java 1.8 and the issue got fixed. No changes were done in the xsl or java code.

xslt multiple parameters into javascript

Im trying to create a function in javascript in xsl which accepts multiple in-parameters. I can not get it to work and get the following error:
Code: 0x80020009
Microsoft JScript runtime error
Wrong number of arguments or invalid property assignment
line = 37, col = 2 (line is offset from the start of the script block).
Error returned from property or method call.
The function looks as follows:
<msxsl:script language="JavaScript" implements-prefix="jsfuncs">
<![CDATA[
function getLineLength (x1,x2,y1,y2)
{
var xVector = x2 - x1;
var yVector = y2 - y1;
var output = Math.sqrt(raised2(xVector)+raised2(yVector));
return output;
}
]]>
</msxsl:script>
The calling code looks as follows:
<xsl:value-of select="jsfuncs:getLineLength($x1,$x2,$y1,$y2)"/>
x1,x2... are variables set earlier and they are correct. I can get everything to work when I post-process the values to one parameter. Is it at all possible to pass multiple parameters in xslt to Javascript? The engine in use is msxml 3.0.
Here is an example passing in four parameters:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ms="urn:schemas-microsoft-com:xslt"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="ms mf"
version="1.0">
<ms:script language="JScript" implements-prefix="mf">
function test(a, b, c, d) {
return a + ":" + b + ":" + c + ":" + d;
}
</ms:script>
<xsl:template match="/">
<xsl:value-of select="mf:test(1, 2, 3, 'foo')"/>
<br/>
<xsl:variable name="item" select="root/item"/>
<xsl:value-of select="mf:test(string($item/#a), string($item/#b), string($item/#c), string($item/#d))"/>
</xsl:template>
</xsl:stylesheet>
As you can see, when wanting to pass in a node to a function expecting a primitive value I would recommend to make sure to convert it on the XSLT side to a primitive value first, as done in mf:test(string($item/#a), string($item/#b), string($item/#c), string($item/#d)), as otherwise the function will get a node-set represented by some selection interface that you would need to iterate first.
The sample works fine for me with MSXML 3 and outputs 1:2:3:foo<br />1:2:3:whatever when run against an input sample with
<?xml version="1.0" encoding="UTF-8"?>
<root>
<item a="1" b="2" c="3" d="whatever"/>
</root>

XSLT 1.0 Idiom for ternary if?

This Java program makes use of a Ternary if, to map booleans to output strings: (a "*" for true, an empty string for false).
public class ternary {
public static void main(String[] args) {
boolean flags[]={true,false,true};
for (boolean f : flags) {
System.out.println(f?"*":"");
}
}
}
So the output is *, [empty], *.
I have an input XML document, something like:
<?xml version="1.0" ?>
<?xml-stylesheet type="text/xsl" href="change.xsl"?>
<root>
<change flag="true"/>
<change flag="false"/>
<change flag="true"/>
</root>
And I have the following XSLT template which maps true to '*' and false to '' (it works):
<xsl:template match="change">
<xsl:variable name="flag" select="translate(substring(#flag,1,1),'tf','*')"/>
<xsl:value-of select="$flag"/>
</xsl:template>
Is there is more concise version of this ?
a) Can I automatically get a value of boolean true|false directly from the string 'true|false' ?
b) Is there an (xpath?) construct to map the boolean true|false to '*', '' ?
a) Can I automatically get a value of boolean true|false directly from
the string 'true|false' ?
b) Is there an (xpath?) construct to map the boolean true|false to
'*', '' ?
This is actually an XPath question.
I. XPath 1.0
There are more than one XPath expressions the evaluation of which produces the wanted result:
substring('*', 2 -(#flag = 'true'))
This also answers b) -- notice that the strings 'true' and 'false' aren't boolean values! The only two boolean values are true() and false() and they are not strings.
In the above expression we use the fact that in XPath 1.0 if a boolean is in a context where a number is needed, it is automatically converted to number. By definition:
number(true()) is 1
and
number(false()) is 0
Thus the second argument of the call to substring() above:
2 - (#flag = 'true')
is evaluated as 1 when #flag = 'true' and to 2 otherwise.
A more general XPath 1.0 expression that produces the string $s1 if $val is "x" and produces the string $s2 if $val is "y" :
concat(substring($s1, 1 div ($val = "x")),
substring($s2, 1 div ($val = "y"))
)
This produces the string $s1 when $val = "x", the string $s2 when $val = "y", and the empty string -- if none of these two conditions holds.
The above XPath 1.0 expression can be generalized to produce N different string results $s1, $2, ..., $sN exactly when $val is one of the values $v1, $v2, ..., $vN because the function concat() can have any number of arguments.
II. XPath 2.0 (XSLT 2.0)
'*'[current()/#flag = 'true']
And more generally, given 2*N atomic values $s1, $s2, ... $sN and $v1, $v2, ..., $vN, such that all $vi values are different, the result of evaluating this XPath 2.0 expression:
($s1, $s2, ..., $sN)[index-of(($v1, $v2, ..., $vN), $v)]
is $sK exactly when $v eq $vK.
You could utilise simple pattern matching in templates for this.
<xsl:template match="change[#flag = 'true']">
<xsl:template match="change">
So, the first one matches true entries, and the other one matches all other cases (which in your cases is just false
So, given the following stylesheet
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" indent="yes"/>
<xsl:template match="change[#flag = 'true']">
<xsl:text>*
</xsl:text>
</xsl:template>
<xsl:template match="change">
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="#*|node()">
<xsl:apply-templates select="#*|node()"/>
</xsl:template>
</xsl:stylesheet>
When applied to your sample XML, the following is output
*
*

Is there an exclusive OR 'XOR' in XPath?

Is there an exclusive OR 'XOR' in XPath1.0 ?
Use this XPath 1.0 expression:
x and not(y) or y and not(x)
Always try to avoid the != operator, because it has an unexpected meaning/behavior when one or both of its arguments are node-sets.
In XSLT 2.0 or XQuery 1.0 one can write this as a function and then use just the function in any XPath expression. Below is an XSLT 2.0 function definition for xor and a small example of using this function:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:f="my:f">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:sequence select=
"for $x in (true(), false()),
$y in (true(), false())
return
('xor(', $x, ',', $y,') = ', f:xor($x, $y), '
')
"/>
</xsl:template>
<xsl:function name="f:xor">
<xsl:param name="pX" as="xs:boolean"/>
<xsl:param name="pY" as="xs:boolean"/>
<xsl:sequence select=
"$pX and not($pY) or $pY and not($pX)"/>
</xsl:function>
</xsl:stylesheet>
when this transformation is applied on any XML document (not used), the wanted, correct result is produced:
xor( true , true ) = false
xor( true , false ) = true
xor( false , true ) = true
xor( false , false ) = false
No but you can emulate it:
(a or b) and (a != b)
number($boolean_var) converts true() to 1 and false() to 0. (Note that true alone addresses a node!!)
boolean($numeric_var) converts 1 to true() and 0 to false().
Therefore, XOR can be accomplished by:
boolean((number($v1) + number($v2) + number($v3)) mod 2)
i.e. least-significant-bit addition using the mod 2 operator. Yes, XPATH is cumbersome.