Calculate between two dates in XSL - xslt

I have these two nodes in XML:
<firstregistrationdate>01.10.2016.</firstregistrationdate>
<currentdate>14.10.2021.</currentdate>
And I would like to calculate days or years between them so if anybody has an idea how to do that it would be very helpful.
Thanks!

If you only want the difference between the two years, you could do simply:
<xsl:variable name="startyear" select="substring(firstregistrationdate, 7, 4)"/>
<xsl:variable name="endyear" select="substring(currentdate, 7, 4)"/>
<result>
<xsl:value-of select="xs:integer($endyear) - xs:integer($startyear)"/>
</result>
Demo: https://xsltfiddle.liberty-development.net/nbsuwEG
Added:
Calculating the difference in days between two dates is also fairly trivial in XSLT 2.0. The complication in your case is that the input dates are not in the expected YYYY-MM-DD format, so it is necessary to convert them first:
<xsl:variable name="startdate" select="replace(firstregistrationdate, '(.{2})\.(.{2})\.(.{4})\.', '$3-$2-$1')"/>
<xsl:variable name="enddate" select="replace(currentdate, '(.{2})\.(.{2})\.(.{4})\.', '$3-$2-$1')"/>
<result>
<xsl:value-of select="days-from-duration(xs:date($enddate)-xs:date($startdate))"/>
</result>
Demo: https://xsltfiddle.liberty-development.net/nbsuwEG/1

Related

Incrementer in Nested for-each-group Calls

First post here after looking at tons of awesome suggestions from the community.
I have three fields in XSLT 2.0, all at the same level (shoulders, knees, and toes). I am needing to output sums of toes based on unique combinations of shoulders and knees, so I have created two nested for-each-groups. On each output, I'm also needing to output an incrementer from from 1 to number of unique combinations of shoulders and knees.
This incrementer is where I'm having issues. The closest I've come is by calling position(), but if I call it in the innermost group, the counter resets at each unique shoulder. If I call it in the outermost group, every knee inside of a unique shoulder gets the same value, then it resets at each unique shoulder. If I call it outside of the groups completely, it never gets past 1. I've also tried to use xsl:number , keys, etc., to no avail. In those cases, the correct number of rows are still being printed, but the incrementer values are looking at the individual, non-grouped values.
I read one suggestion about "tunneling" values between templates, but I haven't been able to get that to work, mostly because I don't think I'm invoking the templates correctly (with these fields being same-level and not parent-child). Any thoughts on making this work with for-each-group or otherwise? Many thanks in advance.
Sample XML:
<bodies>
<parts>
<shoulders>shoulders1</shoulders>
<knees>knees1</knees>
<toes>1</toes>
</parts>
<parts>
<shoulders>shoulders2</shoulders>
<knees>knees2</knees>
<toes>2</toes>
</parts>
<parts>
<shoulders>shoulders1</shoulders>
<knees>knees2</knees>
<toes>10</toes>
</parts>
<parts>
<shoulders>shoulders2</shoulders>
<knees>knees1</knees>
<toes>10</toes>
</parts>
<parts>
<shoulders>shoulders1</shoulders>
<knees>knees1</knees>
<toes>9</toes>
</parts>
<parts>
<shoulders>shoulders2</shoulders>
<knees>knees2</knees>
<toes>8</toes>
</parts>
</bodies>
Sample XSLT:
<xsl:stylesheet exclude-result-prefixes="xsl" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0" xmlns:this="urn:this-stylesheet" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:template match="/">
<xsl:for-each-group select="bodies/parts" group-by="shoulders">
<xsl:for-each-group select="current-group()" group-by="knees">
<xsl:value-of select="shoulders"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="knees"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="sum(current-group()/toes)"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="position()"/>
<xsl:text>. </xsl:text>
</xsl:for-each-group>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
Resulting Output:
shoulders1, knees1, 10, 1. shoulders1, knees2, 10, 2. shoulders2, knees2, 10, 1. shoulders2, knees1, 10, 2.
Desired Output:
shoulders1, knees1, 10, 1. shoulders1, knees2, 10, 2. shoulders2, knees2, 10, 3. shoulders2, knees1, 10, 4.
When you've got two nested xsl:for-each-group instructions like this, then an alternative is to do single level grouping on a composite key, like this:
<xsl:for-each-group select="bodies/parts" group-by="concat(shoulders, '~', knees)">
The position() will then increment the way you are looking for, if I've understood the requirement correctly.
This doesn't work, of course, if you actually want to produce hierarchically structured output in which the outer group is significant.
I've tried this three times now and I think you have to process your results and then count them. I've run out of time to try and figure a way to do the counting inline, because I think it can't be done.
<xsl:stylesheet exclude-result-prefixes="xsl" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0" xmlns:this="urn:this-stylesheet" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsl:template match="/">
<xsl:variable name="results" as="xsd:string*">
<xsl:for-each-group select="bodies/parts" group-by="shoulders">
<xsl:for-each-group select="current-group()" group-by="knees">
<xsl:value-of>
<xsl:value-of select="shoulders"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="knees"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="sum(current-group()/toes)"/>
<xsl:text>,</xsl:text>
</xsl:value-of>
</xsl:for-each-group>
</xsl:for-each-group>
</xsl:variable>
<xsl:for-each select="$results">
<xsl:value-of select=".,position()"/>
<xsl:text>. </xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>

XSLT Transformation (help)

I newby to XSLT and having some trouble to solve this problem.
The input is coming from an XML Excel document and has this format :
<Row>
<Cell><Data ss:Type="String">ToE.3</Data></Cell>
<Cell ss:Index="15"><Data ss:Type="String">Maintain</Data></Cell>
<Cell><Data ss:Type="Number">3</Data></Cell>
<Cell><Data ss:Type="String">Other</Data></Cell>
<Cell ss:Index="131"><Data ss:Type="String">Windows 2003</Data></Cell>
<Cell><Data >Microsoft SQL Server 2005</Data></Cell>
</Row>
..more rows (note the excel sheet has 132 columns)
I need to convert this to a standard text file, something like (with the right column) separator :
Col1 Col2 Col3 ..To.. Col15 Col16 ..To.. Col131
ToE.3 Maintain 3 Windows 2003
The problem is how to insert the empty row values that are skipt with the Index attribute.
The transformation without the empty, index handling looks like :
<xsl:for-each select="Row">
<xsl:for-each select="Cell/Data">
<xsl:value-of select="current()"/>
<xsl:text>\</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
Some help would be warmly appreciated
step1: you need to declare output format, ie, "text" and not "xml"..
step2: you need to get rid of additional whitespace. use Strip-space with element='*', that means 'all'!
step3: you need to write header row first ie, col1, col2 etc..
so using template match select an element row that is first in your XML.. assuming that all the rows have same number of columns, you need to write "COL+ NUMBER" .. column numbers = no of cells you have in first row.
step4: if the cell is last then insert 'enter character'..
step5: call the generic function
step6: explaining generic function:
this function copies data under each cells separated by \. Only for the first row, we would be calling it manually, otherwise template match will take care of it.
Here is the code:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template name="Header" match="Row[not(preceding-sibling::Row)]">
<xsl:for-each select="Cell">
<xsl:value-of select="'Col'"/>
<xsl:value-of select="position()"/>
<xsl:if test="position()!=last()">
<xsl:value-of select="'\'"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
<xsl:call-template name="CopyData"/>
</xsl:template>
<xsl:template name="CopyData" match="Row">
<xsl:for-each select="Cell">
<xsl:for-each select="Data">
<xsl:apply-templates select="."/>
</xsl:for-each>
<xsl:if test="position()!=last()">
<xsl:value-of select="'\'"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
</xsl:stylesheet>
corresponding sample output:
Col1\Col2\Col3\Col4\Col5\Col6
ToE.3\Maintain\3\Other\Windows 2003\Microsoft SQL Server 2005
ToE.3\Maintain\3\Other\Windows 2003\Microsoft SQL Server 2005
This is tricky because as you are seeing Excel skips columns in which no data appears, then provides an ss:Index attribute for the subsequent non-blank column. You have to reconstruct the "missing" cell positions on your own. That is, if you wish to retain the original column position like "15" or "131" in your example, with intervening blanks.
Agreeing with InfantProgrammer above, but suggest you'd add some logic to the "CopyData" template above to (a) determine the number of missing cells, then (b) call a recursive named template to write 'em to output.
<xsl:template name="WriteBlanks">
<xsl:param name="Count" select="0"/>
<xsl:if test="Count > 0">
<xsl:value-of select="'\'"/>
<xsl:call-template name="WriteBlanks">
<xsl:with-param name="Count" select="$Count - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
You could do something similar to generate the first row of column headers.
Given the simplicity of your need to just write backslash characters as column separator, a more succinct approach of just creating a long string of them, then lopping off however many are needed with XPath substring() could be in reach. However a recursive template may be suitable for more complex outputs.

Retain number of digits during processing input->number->output during XSLT processing

For this XSLT:
<xsl:variable name="source0" select="number(num2)"/>
<xsl:variable name="source1" select="number(num3)"/>
s0 plain: <xsl:value-of select="$source0"/>
s1 plain: <xsl:value-of select="$source1"/>
test11: <xsl:value-of select="format-number($source0, '#.#')"/>
test12: <xsl:value-of select="format-number($source0, '#.###############')"/>
test21: <xsl:value-of select="format-number($source1, '#.#')"/>
test22: <xsl:value-of select="format-number($source1, '#.###############')"/>
For XML:
<num2>123456.1234</num2>
<num3>1234567.1234</num3>
I get this output (using Saxon 9.2, XSLT 2.0)
s0 plain: 123456.1234
s1 plain: 1.2345671234E6
test11: 123456.1
test12: 123456.123399999996764
test21: 1234567.1
test22: 1234567.123399999924004
First off... I'm curious why does it suddenly switch between standard and scientific notation when it exceeds 6 digits to the left of decimal place? This is my problem, I want to avoid scientific notation. After various other questions, I discover apparently I'm stuck with putting format-number everywhere.
But format-number doesn't appear to work either. In spite of the fact that the output of "s1 plain" proves that the number of significant digits is known to the processor (I understand about converting to double and back can lose precision, but there is the correct number after such a conversion, so...?), there appears to be no way to output that value in standard non-scientific notation. Is there?
This is my problem, I want to avoid
scientific notation. After various
other questions, I discover apparently
I'm stuck with putting format-number
everywhere.
But format-number doesn't appear to
work either.
Apparently you are not using the appropriate features of XSLT 2.0/XPath 2.0.
This transformation:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:output method="text"/>
<xsl:template match="/*">
<xsl:variable name="vs0" as="xs:decimal"
select="xs:decimal(num2)"/>
<xsl:variable name="vs1" as="xs:decimal"
select="xs:decimal(num3)"/>
s0 plain: <xsl:value-of select="$vs0"/>
s1 plain: <xsl:value-of select="$vs1"/>
test11: <xsl:value-of select="format-number($vs0, '#.#')"/>
test12: <xsl:value-of select="format-number($vs0, '#.###############')"/>
test21: <xsl:value-of select="format-number($vs1, '#.#')"/>
test22: <xsl:value-of select="format-number($vs1, '#.###############')"/>
</xsl:template>
</xsl:stylesheet>
when applied on this XML document:
<nums>
<num2>123456.1234</num2>
<num3>1234567.1234</num3>
</nums>
Produces exact results:
s0 plain: 123456.1234
s1 plain: 1234567.1234
test11: 123456.1
test12: 123456.1234
test21: 1234567.1
test22: 1234567.1234
Conclusion: When in need of good precision, always try to use the xs:decimal data-type.

Finding max of three numbers in XSL

How can find max of three numbers in XSL ?
More Information : I have three numbers say 1.0, 2.0, 5.0....I don't have any nodes set... I would like to find maximum of 1.0, 2.0 and 5.0.....
Example :
AIM : TO know which type of node <MYNODE>, <DOC>, <PIC>
is having maximum count and whant's the count number ?
<ROOT>
<MYNODES>
<MYNODE>A</MYNODE>
<MYNODE>B</MYNODE>
<MYNODE>C</MYNODE>
<MYNODE>D</MYNODE>
</MYNODES>
<DOCS>
<DOC>1</DOC>
<DOC>2</DOC>
<DOC>3</DOC>
</DOC>
<PICS>
<PIC>a.jpeg</PIC>
<PIC>b.jpeg</PIC>
<PIC>c.jpeg</PIC>
<PIC>d.jpeg</PIC>
<PIC>e.jpeg</PIC>
</PICS>
</ROOT>
With your input XML, you would find the maximum count you are looking for like this:
<xsl:variable name="vMaxChildren">
<xsl:for-each select="/ROOT/*">
<xsl:sort select="count(*)" data-type="number" order="descending" />
<xsl:if test="position() = 1">
<xsl:value-of select="concat(name(), ': ', count(*))" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:value-of select="$vMaxChildren" />
Which would produce:
PICS: 5
This question is incorrectly formulated and the provided "XML document' is not well-formed!
Do note that it is generally meaningless to ask about the maximum of a set of numbers. There can be more than one number with the highest value. Therefore, the solutions below show just the first item with the maximum value.
This is one possible XSLT 1.0 solution:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="vNameMaxCount">
<xsl:for-each select="*/*">
<xsl:sort select="count(*)" data-type="number"
order="descending"/>
<xsl:if test="position() = 1">
<xsl:value-of select="concat(name(),'+', count(*))"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
One element with maximum children is: <xsl:text/>
<xsl:value-of select="substring-before($vNameMaxCount, '+')"/>
Maximum number of children: <xsl:text/>
<xsl:value-of select="substring-after($vNameMaxCount, '+')"/>
</xsl:template>
</xsl:stylesheet>
when the above transformation is applied on the following XML document (produced from the one provided after spending 10 minutes to make it well-formed!):
<ROOT>
<MYNODES>
<MYNODE>A</MYNODE>
<MYNODE>B</MYNODE>
<MYNODE>C</MYNODE>
<MYNODE>D</MYNODE>
</MYNODES>
<DOCS>
<DOC>1</DOC>
<DOC>2</DOC>
<DOC>3</DOC>
</DOCS>
<PICS>
<PIC>a.jpeg</PIC>
<PIC>b.jpeg</PIC>
<PIC>c.jpeg</PIC>
<PIC>d.jpeg</PIC>
<PIC>e.jpeg</PIC>
</PICS>
</ROOT>
the wanted result is produced
One element with maximum children is: PICS
Maximum number of children: 5
An XSLT 2.0 solution (actually just an XPath 2.0 soulution):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
>
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:sequence select=
"for $vmaxChildrein in max(/*/*/count(*)),
$vmaxNode in */*[count(*) = $vmaxChildrein][1]
return
(name($vmaxNode),
'has the max no of children:',
$vmaxChildrein
)
"/>
</xsl:template>
</xsl:stylesheet>
when this transformation is applied on the above document, the wanted result is produced:
PICS has the max no of children: 5
For finding the maximum of more tricky properties that cannot be immediately expressed as an XPath expression and used in <xsl:sort>, do use the f:maximum() function of FXSL.
5.0 is the largest of the three numbers. Hardcode it and be done with it. :-)
Seriously though, you may wish to take another path. Logic like this is trivial in other languages, but can be a pain in XSLT. You should consider writing a simple extension for the XSLT engine rather than messing around trying to make XSLT do what you want it to.

XSLT - Sorting multiple values

I've already created my XSLT but id like to be able to sort the data, also add some kind of index so i can group the items together, the difficulty Im having is the the node i want to sort by contains multiple values - values id like to sort by.
For example here is my XML:
<item>
<title>Item 1</title>
<subjects>English,Maths,Science,</subjects>
<description>Blah Blah Bah...</description>
</item>
<item>
<title>Item 2</title>
<subjects>Geography,Physical Education</subjects>
<description>Blah Blah Bah...</description>
</item>
<item>
<title>Item 3</title>
<subjects>History, Technology</subjects>
<description>Blah Blah Bah...</description>
</item>
<item>
<title>Item 4</title>
<subjects>Maths</subjects>
<description>Blah Blah Bah...</description>
</item>
So if i sort by <subjects> I get this order:
English,Maths,Science,
Geography,Physical Education
History, Technology
Maths
But I would like this kind of output:
English
Geography
History
Maths
Maths
Physical Education
Science
Technology
Outputting the XML for each subject contained in <subjects>, so Item1 contains subjects Maths, English & Science so I want to output that Title and Description 3 times because its relevant to all 3 subjects.
Whats the best way in XSLT to do this?
I think one way to do this would be by using the node-set extenstion function to do multi-pass processing. Firstly you would loop through the existing subject nodes, splitting them by commas, to create a new set of item nodes; one per subject.
Next, you would loop through this new node set in subject order.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:exsl="urn:schemas-microsoft-com:xslt" extension-element-prefixes="exsl" version="1.0">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:variable name="newitems">
<xsl:for-each select="items/item">
<xsl:call-template name="splititems">
<xsl:with-param name="itemtext" select="subjects"/>
</xsl:call-template>
</xsl:for-each>
</xsl:variable>
<xsl:for-each select="exsl:node-set($newitems)/item">
<xsl:sort select="text()"/>
<xsl:value-of select="text()"/>
<xsl:text> </xsl:text>
</xsl:for-each>
</xsl:template>
<xsl:template name="splititems">
<xsl:param name="itemtext"/>
<xsl:choose>
<xsl:when test="contains($itemtext, ',')">
<item>
<xsl:value-of select="substring-before($itemtext, ',')"/>
</item>
<xsl:call-template name="splititems">
<xsl:with-param name="itemtext" select="substring-after($itemtext, ',')"/>
</xsl:call-template>
</xsl:when>
<xsl:when test="string-length($itemtext) > 0">
<item>
<xsl:value-of select="$itemtext"/>
</item>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Note that the above example uses Microsoft's Extension functions. Depending on what XSLT processor you are using, you may have to specify another namespace for the processor.
You may also need to do some 'trimming' of the subjects, because in your XML sample above, there is a space before one of the subjects (Technology) in the comma-delimited list.
Well, processing the contents of text nodes isn't really the mandate of XSLT. If you can, you should probably change the representation to add some more XML structure into the subjects elements. Otherwise you'll have to write some really clever string processing code using XPath string functions, or perhaps use a Java-based XSLT processor and hand off the string processing to a Java method. It's not straightforward.