XSLT 3.0 Nested grouping and counting string values - xslt-2.0

I have jobs and multiple job tests for which I have to get 'pass' and 'fail' count by gender.My current code is working if there is one jobtest for each job.If any job has multiple job tests the counts are incorrect.
Here is my fiddle with with xml and xslt code.job 1 counts are correct as it has only one test.Job 2 counts are incorrect as it has multiple jobtests.
https://xsltfiddle.liberty-development.net/a9GPfH/1
Expected Totals
Job, job_Test, Ma_Pass, Ma_Fail, Fem_Pass, Fem_Fail
Job 1, Application Evaluation, 5, 3, 9, 2
Job 2, Application Evaluation, 304, 2, 131, 0
Job 2, Suitability Test, 127, 118, 53, 54
Anyhelp is hightly appreciated.

You need to group the nested Job_Test in the inner grouping:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:r="urn:com.workday.report/Test_by_Gender"
xmlns:this="urn:this"
xmlns:is="java:com.workday.esb.intsys.xpath.ParsedIntegrationSystemFunctions"
exclude-result-prefixes="#all"
version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="r:Report_Data">
<Root>
<xsl:variable name="linefeed" select="'
'"/>
<xsl:for-each-group select="r:Report_Entry" group-by="r:Job/#r:Descriptor">
<xsl:variable name="entry" select="."/>
<xsl:for-each-group select="current-group()/r:Test_Result_group/r:Job_Test" group-by="#r:Descriptor">
<Job><xsl:value-of select = "$entry/r:Job/#r:Descriptor"/></Job>
<Job_Test><xsl:value-of select="current-grouping-key()"/></Job_Test>
<xsl:variable name="Females" select="current-group()[ancestor::r:Report_Entry/r:Gender = 'Female']"/>
<xsl:variable name="FemalePass" select="this:CountResultByStatus($Females,'Pass')"/>
<xsl:variable name="FemaleFail" select="this:CountResultByStatus($Females, 'Fail')"/>
<xsl:variable name="Males" select="current-group()[ancestor::r:Report_Entry/r:Gender = 'Male']"/>
<xsl:variable name="MalePass" select="this:CountResultByStatus($Males,'Pass')"/>
<xsl:variable name="MaleFail" select="this:CountResultByStatus($Males,'Fail')"/>
<Total_Male><xsl:value-of select="count($Males)"/></Total_Male>
<Male_Pass><xsl:value-of select= "$MalePass"/></Male_Pass>
<Male_Fail><xsl:value-of select= "$MaleFail"/></Male_Fail>
<Total_Female><xsl:value-of select="count($Females)"/></Total_Female>
<Female_Pass><xsl:value-of select="$FemalePass"/> </Female_Pass>
<Female_Fail><xsl:value-of select="$FemaleFail"/></Female_Fail>
<xsl:value-of select="$linefeed"/>
</xsl:for-each-group>
</xsl:for-each-group>
</Root>
</xsl:template>
<xsl:function name="this:CountResultByStatus" as="xs:integer">
<xsl:param name="People" as="element()*"/>
<xsl:param name="Status" as="xs:string"/>
<xsl:sequence select="count($People[../r:Test_Status = $Status])"/>
</xsl:function>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/a9GPfH/2

Related

How to create dynamic columns (same number of columns in header & row) using XSLT 2.0

I have an XML data source:
<ws:Report_Data xmlns:ws="urn:com.ws.report/Expense_Data">
<ws:Report_Entry>
<ws:uID>
<ws:id>1</ws:id>
</ws:uID>
<ws:Journal_Entry_Group>
<ws:Ledger_Accounts ws:Descriptor="Q1: TEST1">
<ws:ID ws:type="Ledger_Account_ID" ws:parent_type="Account_Set_ID"
ws:parent_id="Standard">Q1</ws:ID>
</ws:Ledger_Accounts>
</ws:Journal_Entry_Group>
<ws:line>
<ws:Number>000123</ws:Number>
</ws:line>
<ws:line>
<ws:Number>000124</ws:Number>
</ws:line>
</ws:Report_Entry>
<ws:Report_Entry>
<ws:uID>
<ws:id>2</ws:id>
</ws:uID>
<ws:Journal_Entry_Group>
<ws:Ledger_Accounts ws:Descriptor="Q1: TEST1">
<ws:ID ws:type="Ledger_Account_ID" ws:parent_type="Account_Set_ID"
ws:parent_id="Standard">Q1</ws:ID>
</ws:Ledger_Accounts>
<ws:Ledger_Accounts ws:Descriptor="Q2: TEST2">
<ws:ID ws:type="Ledger_Account_ID" ws:parent_type="Account_Set_ID"
ws:parent_id="Standard">Q2</ws:ID>
</ws:Ledger_Accounts>
</ws:Journal_Entry_Group>
<ws:line>
<ws:Number>000596</ws:Number> </ws:line>
</ws:Report_Entry>
</ws:Report_Data>
Based on the number of ws:ledger_accounts returned, I want to create the columns of the ledger account id and the description. The column header is generated dynamically as well (adding number _1, _2 and so on in the Ledger_Account_ID and Ledger_Account_Descheader name).
Based on the sample data source above, the second report entry has the highest number of ws:Journal_Entry_Group/ws:Ledger_Accounts returned, and so my header column should have 2 and the data row should have exact number of columns as well. The delimiter is <|> while the data row is enclosed with "".
Example expected Output:
uID<|>Ledger_Account_ID_1<|>Ledger_Account_ID_2<|>Ledger_Account_Desc_1<|>Ledger_Account_Desc_2|Number
"1"<|>"Q1"<|>""<|>"Q1: TEST1"<|>""<|>"000123"
"1"<|>"Q1"<|>""<|>"Q1: TEST1"<|>""<|>"000124"
"2"<|>"Q1"<|>"Q2"<|>"Q1: TEST1"<|>"Q2: TEST2"<|>"000596"
The XSLT code:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet exclude-result-prefixes="xsl" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="2.0" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:ws="urn:com.ws.report/Expense_Data">
<xsl:output method="text"/>
<xsl:variable name="linefeed" select="'
'"/>
<xsl:variable name="delimiter"><![CDATA["<|>"]]></xsl:variable>
<xsl:variable name="delimiter1"><![CDATA[<|>]]></xsl:variable>
<xsl:template match="/ws:Report_Data">
<xsl:text>uID</xsl:text>
<xsl:value-of select="$delimiter1"/>
<xsl:value-of select="ws:Report_Entry[count(.//ws:Ledger_Accounts) = max(../ws:Report_Entry/count(.//ws:Ledger_Accounts))]//ws:Ledger_Accounts/concat('Ledger_Account_ID',$delimiter1)" separator=""/>
<xsl:value-of select="ws:Report_Entry[count(.//ws:Ledger_Accounts) = max(../ws:Report_Entry/count(.//ws:Ledger_Accounts))]//ws:Ledger_Accounts/concat('Ledger_Account_Desc',$delimiter1)" separator=""/>
<xsl:text>Number</xsl:text>
<xsl:value-of select="$linefeed"/>
<xsl:apply-templates select="/ws:Report_Data/ws:Report_Entry/ws:line"/>
</xsl:template>
<xsl:template match="/ws:Report_Data/ws:Report_Entry/ws:line ">
<xsl:text>"</xsl:text>
<xsl:value-of select="../ws:uID/ws:id"/>
<xsl:value-of select="$delimiter"/>
<!-- Ledger_Account_IDs -->
<xsl:if test="count(../ws:Journal_Entry_Group/ws:Ledger_Accounts) > 1">
<xsl:for-each select="../ws:Journal_Entry_Group/ws:Ledger_Accounts">
<xsl:value-of select="normalize-space(ws:ID[#ws:type='Ledger_Account_ID'])"/>
<xsl:if test="position()!=last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
</xsl:if>
<xsl:value-of select="$delimiter"/>
<!-- Ledger_Account_Desc -->
<xsl:if test="count(../ws:Journal_Entry_Group/ws:Ledger_Accounts) > 1">
<xsl:for-each select="../ws:Journal_Entry_Group/ws:Ledger_Accounts">
<xsl:value-of select="normalize-space(#ws:Descriptor)"/>
<xsl:if test="position()!=last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
</xsl:if>
<xsl:value-of select="$delimiter"/>
<xsl:value-of select="ws:Number"/>
<xsl:text>"</xsl:text>
<xsl:value-of select="$linefeed"/>
</xsl:template>
</xsl:stylesheet>
But the output is incorrect, the data row is not sync up with the number of columns in the header. For the first two lines it only creates 1 ledger account id column, should have at least another ""<|> before it goes to Ledger_Account_Desc column and the header can’t make it incremental.
Incorrect Output:
uID<|>Ledger_Account_ID<|>Ledger_Account_ID<|>Ledger_Account_Desc<|>Ledger_Account_Desc|Number
"1”<|>"Q1"<|>"Q1: TEST1"<|>"000123"
"1”<|>"Q1"<|>"Q1: TEST1"<|>"000124"
"2"<|>"Q1"<|>"Q2"<|>"Q1: TEST1"<|>"Q2: TEST2"<|>"000596"
I would use the separator attribute of value-of and then use a function to wrap each value in quotes; XSLT 3 would be
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xpath-default-namespace="urn:com.ws.report/Expense_Data"
xmlns:ws="urn:com.ws.report/Expense_Data"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="#all"
version="3.0">
<xsl:param name="delimiter" as="xs:string"><![CDATA[<|>]]></xsl:param>
<xsl:param name="linefeed" as="xs:string" select="'
'"/>
<xsl:output method="text"/>
<xsl:function name="mf:quote" as="xs:string">
<xsl:param name="input" as="xs:string"/>
<xsl:sequence select="'"' || $input || '"'"/>
</xsl:function>
<xsl:variable name="max-accounts" select="max(//Report_Entry/count(.//Ledger_Accounts))"/>
<xsl:template match="/">
<xsl:value-of
select="'uID',
(1 to $max-accounts)!('Ledger_Account_ID_' || .),
(1 to $max-accounts)!('Ledger_Account_Desc_1' || .),
'Number'"
separator="{$delimiter}"/>
<xsl:value-of select="$linefeed"/>
<xsl:apply-templates select="//line"/>
</xsl:template>
<xsl:template match="line">
<xsl:value-of
select="(../uID/id,
for $i in 1 to $max-accounts return string(../descendant::Ledger_Accounts[$i]/ID),
for $i in 1 to $max-accounts return string(../descendant::Ledger_Accounts[$i]/#ws:Descriptor),
Number) ! mf:quote(.)"
separator="{$delimiter}"/>
<xsl:value-of select="$linefeed"/>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/pNvtBGj

How to check if given comma separated values is ascending numerical sequence

I would like to check if given comma separated value is ascending numeric sequence.
Input XML:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<isSequence>
<seq name="a" value="1, 2, 3"/>
<seq name="b" value="3, 1, 5, 6"/>
<seq name="c" value="15, 16, 18, 0"/>
<seq name="d" value="21, 22, 23, 24, 25"/>
<seq name="e" value="A, B, C"/>
</isSequence>
</root>
I have tried below code
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="xs"
version="2.0">
<xsl:template match="/">
<xsl:for-each select="root/isSequence/seq">
<xsl:if test="subsequence(#value, 0)">
<xsl:value-of select="#name"/> is an ascending numeric sequence
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Expected Result
a is an ascending numeric sequence
d is an ascending numeric sequence
Assuming you are looking for an integer sequence then I think (in XSLT 3) this can be expressed as
<xsl:template match="seq[let $items := tokenize(#value, ',\s+')!xs:integer(.) return every $p in 1 to count($items) satisfies $items[$p] = $items[1] + ($p - 1)]">
<xsl:value-of select="#name"/><xsl:text> is an ascending numeric sequence
</xsl:text>
</xsl:template>
Full example (https://xsltfiddle.liberty-development.net/6r5Gh2H)
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes"
version="3.0">
<xsl:mode on-no-match="shallow-skip"/>
<xsl:strip-space elements="*"/>
<xsl:output method="text"/>
<xsl:template match="seq[let $items := tokenize(#value, ',\s+')!xs:integer(.) return every $p in 1 to count($items) satisfies $items[$p] = $items[1] + ($p - 1)]">
<xsl:value-of select="#name"/><xsl:text> is an ascending numeric sequence
</xsl:text>
</xsl:template>
</xsl:stylesheet>
In XSLT 2 with XPath 2 I don't think you have let to express that compactly in a match pattern but you can of course use a variable at XSLT level:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
version="2.0">
<xsl:strip-space elements="*"/>
<xsl:output method="text"/>
<xsl:template match="seq[every $i in tokenize(#value, ',\s+') satisfies $i castable as xs:integer]">
<xsl:variable name="items" select="for $item in tokenize(#value, ',\s+') return xs:integer($item)"/>
<xsl:if test="every $p in 1 to count($items) satisfies $items[$p] = $items[1] + ($p - 1)">
<xsl:value-of select="#name"/><xsl:text> is an ascending numeric sequence
</xsl:text>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
http://xsltransform.hikmatu.com/bFDb2BU

XSLT 2.0 xsl:result-document every n elements

I have a XML as follows:
<list>
<element>1</element>
<element>2</element>
<element>3</element>
<element>4</element>
<element>5</element>
<element>6</element>
<element>7</element>
<element>8</element>
<element>9</element>
<element>10</element>
<element>11</element>
<element>12</element>
<element>13</element>
<element>14</element>
<element>15</element>
<element>16</element>
<element>17</element>
<element>18</element>
<element>19</element>
<element>20</element>
Then I use this template to write the content to a file
<xsl:template match="list">
<xsl:result-document href="file:///c:/temp/bic.txt">
<xsl:for-each select="element">
<xsl:value-of select="."/>
</xsl:for-each>
</xsl:result-document>
</xsl:template>
But now I want to split the output so each five elements go to a different file. How can I do that?
One way is to use positional grouping:
<xsl:template match="list">
<xsl:for-each-group select="element" group-adjacent="(position() - 1) idiv 5">
<xsl:result-document href="bic-{position()}.txt" method="text">
<xsl:value-of select="current-group()" separator=""/>
</xsl:result-document>
</xsl:for-each-group>
</xsl:template>

Separating data into groups

I think I'm missing something simple here. I have a source XML file
<Inventory Division="B" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<StackGroup name="Warehouse">
<Stack>
<Mainstack name="PRIMARY">
<MainstackGroup name="GROUP_PRIMARY">
<MainstackLayer sequence="1">
<StackLayerRef id="LAYER_1"/>
</MainstackLayer>
</MainstackGroup>
</Mainstack>
<Mainstack name="SECONDARY">
<MainstackGroup name="GROUP_SECONDARY">
<MainstackLayer sequence="2">
<StackLayerRef id="LAYER_2"/>
</MainstackLayer>
</MainstackGroup>
</Mainstack>
</Stack>
</StackGroup>
</Inventory>
I'm applying a stylesheet:
<?xml version="1.0" encoding="iso-8859-1"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="/">
<Mainstack>
<NumberOfStacks>
<xsl:value-of select="count(/Inventory/StackGroup/Stack/Mainstack)"/>
</NumberOfStacks>
<StackDisplayOrder>
<xsl:apply-templates select="/Inventory/StackGroup/Stack/Mainstack" mode="order"/>
</StackDisplayOrder>
<xsl:apply-templates select="/Inventory/StackGroup/Stack/Mainstack" mode="stacklist"/>
</Mainstack>
</xsl:template>
<xsl:template match="/Inventory/StackGroup/Stack/Mainstack" mode="order">
<Index><xsl:value-of select="position() - 1" /></Index>
</xsl:template>
<xsl:template match="/Inventory/StackGroup/Stack/Mainstack" mode="stacklist">
<Stack>
<Index><xsl:value-of select="position() - 1" /></Index>
<Name>
<xsl:value-of select="/Inventory/StackGroup/Stack/Mainstack/#name"/>
</Name>
<GroupName>
<xsl:value-of select="/Inventory/StackGroup/Stack/Mainstack/MainstackGroup/#name"/>
</GroupName>
<SequenceNo>
<xsl:value-of select="/Inventory/StackGroup/Stack/Mainstack/MainstackGroup/MainstackLayer/#sequence"/>
</SequenceNo>
<LayerNo>
<xsl:value-of select="/Inventory/StackGroup/Stack/Mainstack/MainstackGroup/MainstackLayer/StackLayerRef/#id"/>
</LayerNo>
</Stack>
</xsl:template>
</xsl:stylesheet>
I get the following output:
<?xml version="1.0" encoding="UTF-8"?>
<Mainstack>
<NumberOfStacks>2</NumberOfStacks>
<StackDisplayOrder>
<Index>0</I`enter code here`ndex>
<Index>1</Index>
</StackDisplayOrder>
<Stack>
<Index>0</Index>
<Name>PRIMARY SECONDARY</Name>
<GroupName>GROUP_PRIMARY GROUP_SECONDARY</GroupName>
<SequenceNo>1 2</SequenceNo>
<LayerNo>LAYER_1 LAYER_2</LayerNo>
</Stack>
<Stack>
<Index>1</Index>
<Name>PRIMARY SECONDARY</Name>
<GroupName>GROUP_PRIMARY GROUP_SECONDARY</GroupName>
<SequenceNo>1 2</SequenceNo>
<LayerNo>LAYER_1 LAYER_2</LayerNo>
</Stack>
</Mainstack>
The template obviously finds two matches and concatenates the data from both. How can I separate them?
Thanks in advance!
Use relative paths so change e.g.
<Name>
<xsl:value-of select="/Inventory/StackGroup/Stack/Mainstack/#name"/>
</Name>
to
<Name>
<xsl:value-of select="#name"/>
</Name>
and so on for all the paths in the template.

Saxon XSLT2.0 Extracting Numbers from the String

I am trying to Extract Integer from a String using Xslt2.0
For Example consider the string "designa80000dd5424d" and i need the two integers inside the string i.e "8000" and "5424"
I tried using translate function as below
select="translate($term,translate($term, '0123456789', ''), '')"
But it combines both the integers and gives the output as "80005424"
i need something which separates them
I tried using translate function as below
select="translate($term,translate($term, '0123456789', ''), '')"
But it combines both the numbers and gives the output as "80005424" i
need something which separates them
I. Here is a complete 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="vSpaces">
<xsl:call-template name="makeSpaces"/>
</xsl:variable>
<xsl:variable name="vtheNumbers"
select="normalize-space(translate(., translate(.,'0123456789',''), $vSpaces))"/>
<xsl:call-template name="tokenize">
<xsl:with-param name="pStr" select="$vtheNumbers"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="tokenize">
<xsl:param name="pStr"/>
<xsl:param name="pInd" select="1"/>
<xsl:if test="string-length($pStr)">
<xsl:value-of select=
"concat($pInd, ': ',substring-before(concat($pStr, ' '), ' '), '
')"/>
<xsl:call-template name="tokenize">
<xsl:with-param name="pStr" select="substring-after($pStr, ' ')"/>
<xsl:with-param name="pInd" select="$pInd +1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="makeSpaces">
<xsl:param name="pLen" select="string-length(.)"/>
<xsl:choose>
<xsl:when test="$pLen = 1">
<xsl:value-of select="' '"/>
</xsl:when>
<xsl:when test="$pLen > 1">
<xsl:variable name="vHalfLen" select="floor($pLen div 2)"/>
<xsl:call-template name="makeSpaces">
<xsl:with-param name="pLen" select="$vHalfLen"/>
</xsl:call-template>
<xsl:call-template name="makeSpaces">
<xsl:with-param name="pLen" select="$pLen -$vHalfLen"/>
</xsl:call-template>
</xsl:when>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document:
<t>designa80000dd5424dan1733g122</t>
the wanted, correct result is produced:
1: 80000
2: 5424
3: 1733
4: 122
Do note:
The last argument of the outer translate() is a string having the same number of characters as that of the input string, and each of these characters is a space.
II. XPath 2.0 shorter and simpler
This XPath 2.0 expression when evaluated produces the wanted sequence of numbers:
tokenize(., '[^\d]+')[.]
Here is an XSLT - based verification:
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:template match="/*">
<xsl:variable name="vNumbers"
select="tokenize(., '[^\d]+')[.]"/>
<xsl:for-each select="$vNumbers">
<xsl:value-of select="concat(position(), ': ', ., '
')"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document:
<t>designa80000dd5424dan1733g122</t>
the same correct result is produced:
1: 80000
2: 5424
3: 1733
4: 122
You could try it using tokenize with any non-digit sequences as the separator, i.e. using XPath 3.0 tokenize('designa80000dd5424d', '[^0-9]+')[normalize-space()]!number() or in XSLT/XPath 2.0 as for $t in tokenize('designa80000dd5424d', '[^0-9]+')[normalize-space()] return number($t) or you could use xsl:analyze-string (XSLT 2.0) or the analyze-string function (XSLT/XPath 3.0, but available with Saxon 9.7 HE).

Resources