How to output values from 2 different groups onto a single line? - xslt

I have a file of employees that receive different types of earnings per pay period. Each employee can have from 1 to 3 different earning flag types (A, B, C) and these earnings can be for different dates.
For each employee I would like to sum the amounts together if the record has the same earning flag and date. If there is not an earning for say B then there should be a 0 in the B place.
I would like the output to be on the same line if possible to make readability and verification easier. This is the part I am having trouble with.
I have grouped by EmployeeID, EarningFlag, and Date. Then I summed the amounts for each grouping. The output though goes on separate lines and is a mess. I have no idea how to put a zero in place if there is no such earning flag on that date for the employee.
Example XML:
<Entry>
<EmployeeName>Bob Stevens</EmployeeName>
<EmployeeID>123</EmployeeID>
<EarningFlag>A</EarningFlag>
<Date>2019-04-01</Date>
<Amount>2031.54</Amount>
</Entry>
<Entry>
<EmployeeName>Bob Stevens</EmployeeName>
<EmployeeID>123</EmployeeID>
<EarningFlag>A</EarningFlag>
<Date>2019-04-01</Date>
<Amount>30.74</Amount>
</Entry>
<Entry>
<EmployeeName>Bob Stevens</EmployeeName>
<EmployeeID>123</EmployeeID>
<EarningFlag>B</EarningFlag>
<Date>2019-04-01</Date>
<Amount>1.63</Amount>
</Entry>
<Entry>
<EmployeeName>Samantha Philips</EmployeeName>
<EmployeeID>036</EmployeeID>
<EarningFlag>C</EarningFlag>
<Date>2019-04-01</Date>
<Amount>631.54</Amount>
</Entry>
<Entry>
<EmployeeName>Samantha Philips</EmployeeName>
<EmployeeID>036</EmployeeID>
<EarningFlag>C</EarningFlag>
<Date>2019-04-01</Date>
<Amount>3771.33</Amount>
</Entry>
<Entry>
<EmployeeName>Samantha Philips</EmployeeName>
<EmployeeID>036</EmployeeID>
<EarningFlag>A</EarningFlag>
<Date>2019-04-01</Date>
<Amount>631.54</Amount>
</Entry>
<Entry>
<EmployeeName>Samantha Philips</EmployeeName>
<EmployeeID>036</EmployeeID>
<EarningFlag>B</EarningFlag>
<Date>2019-03-07</Date>
<Amount>3771.33</Amount>
</Entry>
<xsl:for-each-group select="Entry" group-by="concat(EmployeeID,Date,EarningFlag)">
<xsl:sort select="current-grouping-key()"/>
<xsl:value-of select="EmployeeID"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="EmployeeName"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Date"/>
<xsl:text>,</xsl:text>
<xsl:if test="EarningFlag='A'">
<xsl:value-of select="sum(current-group()/Amount)"/>
</xsl:if>
<xsl:text>,</xsl:text>
<xsl:if test="EarningFlag='B'">
<xsl:value-of select="sum(current-group()/Amount)"/>
</xsl:if>
<xsl:text>,</xsl:text>
<xsl:if test="EarningFlag='C'">
<xsl:value-of select="sum(current-group()/Amount)"/>
</xsl:if>
</xsl:for-each-group>
My actual results are:
123,Bob Stevens,2019-04-04,2062.28123,Bob Stevens,2019-04-04,1.63,
036,Samantha Philips,2019-03-07,3771.33,
036,Samantha Philips,2019-04-01,631.54,036,Samantha Philips,2019-04-01,3771.33
The expected output would be in the order of Employee ID, Employee Name, Date, A, B, C.
123,Bob Stevens,2019-04-01,2062.28,1.63,0
036,Samantha Philips,2019-03-07,0,3771.33,0
036,Samantha Philips,2019-04-01,631.54,0,3771.33
Is this even possible???
Thank you for any help.

Following on from the answer from #michael.hor257k, you can also write
<xsl:for-each-group select="Entry" group-by="concat(EmployeeID,Date)">
<xsl:sort select="EmployeeID"/>
<xsl:sort select="Date"/>
<xsl:value-of select="EmployeeID, EmployeeName, Date,
sum(current-group([EarningFlag='A']/Amount),
sum(current-group()[EarningFlag='B']/Amount,
sum(current-group()[EarningFlag='C']/Amount"
separator=","/>
<xsl:text>
</xsl:text>
</xsl:for-each-group>

I guess you want to do:
<xsl:for-each-group select="Entry" group-by="concat(EmployeeID,Date)">
<xsl:sort select="EmployeeID"/>
<xsl:sort select="Date"/>
<xsl:value-of select="EmployeeID"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="EmployeeName"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="Date"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="sum(current-group()[EarningFlag='A']/Amount)"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="sum(current-group()[EarningFlag='B']/Amount)"/>
<xsl:text>,</xsl:text>
<xsl:value-of select="sum(current-group()[EarningFlag='C']/Amount)"/>
<xsl:text>
</xsl:text>
</xsl:for-each-group>

Related

XSL: test if a node value exist in another node

I apologize for the basic question since I am a bit new to XSLT.
<?xml version="1.0" encoding="UTF-8"?>
<ike>
<gateway>
<entry name="VPN1-IKEV1-128">
<protocol>
<ikev1>
<ike-crypto-profile>Suite-B-GCM-128</ike-crypto-profile>
</ikev1>
<version>ikev1</version>
</protocol>
</entry>
</gateway>
<crypto-profiles>
<ike-crypto-profiles>
<entry name="Suite-B-GCM-128">
<encryption>
<member>aes-128-gcm</member>
</encryption>
</entry>
</ike-crypto-profiles>
</crypto-profiles>
</ike>
What I need to implement:
If version is ikev1
1). get its configured ike-crypto-profile name(Suite-B-GCM-128)
2). Look up the crypto profile name (Suite-B-GCM-128) in crypto-profiles.
3). If there is a member named "aes-128-gcm or aes-256-gcm" in this crypto profile, send an error message to user because we do not support AES-GCM for IKEv1.
Here is my xsl. It does not work as expected.
<xsl:template match="/config/global/network/ike/gateway/entry">
<xsl:variable name="name" select="#name"/>
<xsl:variable name="gwname" select="#gwname"/>
<xsl:variable name="ver" select="protocol/version" />
<xsl:variable name="v1crypto" select="protocol/ikev1/ike-crypto-profile"/>
<xsl:if test="$ver='ikev1'">
<xsl:for-each match="/config/global/network/ike/crypto-profiles/ike-crypto-profiles/entry">
<xsl:if test="#name=$v1crypto">
<xsl:if test="encryption/member='aes-128-gcm' or encryption/member='aes-256-gcm'">
<error> AES-GCM <xsl:value-of select="#name"/>is not supported for IKEv1 gateway <xsl:value-of select="$gwname" /> </error>
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:template>
Post the worked XSL here just in case anyone is interested in this topic.
<xsl:template match="/config/global/network/ike/gateway/entry">
<xsl:variable name="gwname" select="#name"/>
<xsl:variable name="ver" select="protocol/version"/>
<xsl:variable name="v1crypto" select="protocol/ikev1/ike-crypto-profile"/>
<xsl:if test="$ver='ikev1'">
<xsl:for-each select="/config/global/network/ike/crypto-profiles/ike-crypto-profiles/entry">
<xsl:if test="#name=$v1crypto">
<xsl:for-each select="encryption/member">
<xsl:if test=".='aes-128-gcm' or .='aes-256-gcm' " >
<error>Not support: <xsl:value-of select="."/> is selected in <xsl:value-of select="#name"/> which is attched to IKEv1 gateway <xsl:value-of select="$gwname"/></error> <xsl:text>
</xsl:text>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:for-each>
</xsl:if>
</xsl:template>

building an XSLT string using variable in a foreach loop

The problem I'm facing perhaps simple for others, but being a beginner XSL - I'm yet to find a proper solution. What I want to do is build a string by concatenating the results of variables define in for-each loop.
Sorry it should be this:
<linked-hash-map>
<entry>
<string>properties</string>
<list>
<linked-hash-map>
<entry>
<string>property_id</string>
<int>123</int>
</entry>
<entry>
<string>type</string>
<string>H</string>
</entry>
<entry>
<string>status</string>
<string>CURRENT</string>
</entry>
<entry>
<string>description</string>
<string>Test</string>
</entry>
<entry>
<string>lots</string>
<list>
<linked-hash-map>
<entry>
<string>lot_id</string>
<int>123</int>
</entry>
<entry>
<string>lot_number</string>
<int>11</int>
</entry>
<entry>
<string>plan_number</string>
<int>100</int>
</entry>
<entry>
<string>plan_type</string>
<string>CC</string>
</entry>
<entry>
<string>plan_id</string>
<int>1</int>
</entry>
</linked-hash-map>
</list>
</entry>
</linked-hash-map>
</list>
</entry>
Desired output: SP - 31 - 108661
So basically I want to concatenate the plan_type, lot_number, and plan_number together into one string.
I've tried this:
<xsl:for-each select="linked-hash-map/entry/linked-hash-map/entry/list/linked-hash-map/entry" >
<xsl:choose>
<!-- LOT NUMBER -->
<xsl:when test="string[1] = 'lot_number'">
<xsl:variable name="lot_number" select="int" />
</xsl:when>
<!-- PLAN NUMBER -->
<xsl:when test="string[1] = 'plan_number'">
<xsl:variable name="plan_number" select="int" />
</xsl:when>
<!-- PLAN TYPE -->
<xsl:when test="string[1] = 'plan_type'">
<xsl:variable name="plan_type" select="string[2]" />
</xsl:when>
</xsl:choose>
</xsl:for-each>
<!-- LOT DETAIL -->
// concatenate string in here.
<xsl:value-of select="concat($plan_type, '-', $lot_number, '-', $plan_number)" />
But it doesn't work because of the variable being out of scope.
How can I achieve that desired output?
Thank you.
To change as little as possible, try the following XSLT-1.0 code:
<xsl:template match="linked-hash-map">
<xsl:variable name="plan_type" select="entry[string='plan_type']/string[2]/text()" />
<xsl:variable name="lot_number" select="entry[string='lot_number']/int/text()" />
<xsl:variable name="plan_number" select="entry[string='plan_number']/int/text()" />
<xsl:value-of select="concat($plan_type, ' - ', $lot_number, ' - ', $plan_number)" />
</xsl:template>
Its result is:
SP - 31 - 108661
Note that variables are local in scope to the block in which they are declared, so the each variable in this case would only be accessible within the xsl:when
But you don't need xsl:for-each or variables here. You can just do something like this (assuming you were in a template matching linked-hash-map
<xsl:value-of select="concat(entry[string[1]='plan_type']/string[2], '-', entry[string[1]='lot_number']/int, '-', entry[string[1]='plan_number']/int)" />
Or maybe, to make it a bit more readable, separate out the lines
<xsl:value-of select="entry[string[1]='plan_type']/string[2]" />
<xsl:text>-</xsl:text>
<xsl:value-of select="entry[string[1]='lot_number']/int" />
<xsl:text>-</xsl:text>
<xsl:value-of select="entry[string[1]='plan_number']/int" />
See http://xsltfiddle.liberty-development.net/eiZQaGs/2
We don't have an XSLT version number on the question, so I wondered what I would write in 3.0. Perhaps:
<xsl:value-of select="for $n in ('plan_type', 'lot_number', 'plan_number')
return entry[string[1]=$n]/*[2]"
separator=" - "/>

Get Parent Attribute in the Current Position with XSLT

I have the following data:
<books>
<entry id="8">
<author name="tony-blair">Tony Blair</author>
</entry>
<entry id="9">
<author name="william-campbell">William Campbell</author>
</entry>
</books>
And use the following template
<xsl:template match="books/entry">
<xsl:value-of select="author"/>
<xsl:value-of select="ancestor::books/entry/#id"/>
</xsl:template>
I try to use ancestor::books/entry/#id but it results only the first id.
How to get the parent entry id while we are in the current position entry?
<xsl:template match="books/entry">
<xsl:value-of select="author"/>
<xsl:value-of select="#id"/>
</xsl:template>
Within a
<xsl:template match="books/entry">
the current context node is the entry element, so you can just use
<xsl:value-of select="#id" />
You don't need to go up to the books element and back down again.
<xsl:template match="entry">
<xsl:value-of select="author"/>
<xsl:value-of select="#id"/>
</xsl:template>
As others have said, you don't need to go up and down the tree.
books/entry is a tiny performance optimisation but it seems you're not there yet, so keep things simple and you're probably not processing massive documents anyway.

XSL fetch min/max of modified attributes into variables

I would like to find a "nicer" solution to get the minimum and maximum values of attributes and save them into accesible variables. I would love to get away from the for-each-loop too. How is that possible?
My XML:
<Rows>
<Entry value1="16,423" value2="18,123" />
<Entry value1="423" value2="11,588" />
<Entry value1="1,168" value2="521" />
</Rows>
And my XSL:
<xsl:for-each select="Rows/Entry/#value1|Rows/Entry/#value2">
<xsl:sort select="." data-type="number" />
<xsl:choose>
<xsl:when test="position() = 1">
<xsl:variable name="min" select="format-number(translate(.,',',''),'#')" />
</xsl:when>
<xsl:when test="position() = last()">
<xsl:variable name="max" select="format-number(translate(.,',',''),'#')" />
</xsl:when>
</xsl:choose>
</xsl:for-each>
The desired output should be $min=423 and $max=18123 as numbers and accesible outside the for-each-loop
Well there is XSLT 2.0 since 2007 (implemented by XSLT processors like Saxon 9, AltovaXML, XmlPrime) where you can simply do (assuming you have the declaration xmlns:xs="http://www.w3.org/2001/XMLSchema" on your xsl:stylesheet element):
<xsl:variable name="min" select="min(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', ''))"/>
<xsl:variable name="max" select="max(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', ''))"/>
If you really want to store a formatted string in a variable you can of course do that as well with e.g.
<xsl:variable name="min" select="format-number(min(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', '')), '#')"/>
<xsl:variable name="max" select="format-number(max(Rows/Entry/(#value1, #value2)/xs:decimal(translate(., ',', '')), '#')"/>
As for XSLT 1.0, there I think the sorting with for-each is the right approach but you would need to pull the xsl:variable outside the for-each e.g.
<xsl:variable name="min">
<xsl:for-each select="Rows/Entry/#value1|Rows/Entry/#value2">
<xsl:sort select="translate(., ',', '')" data-type="number"/>
<xsl:if test="position() = 1">
<xsl:value-of select="format-number(., '#')"/>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="max">
<xsl:for-each select="Rows/Entry/#value1|Rows/Entry/#value2">
<xsl:sort select="translate(., ',', '')" data-type="number"/>
<xsl:if test="position() = last()">
<xsl:value-of select="format-number(.,'#')" />
</xsl:if>
</xsl:for-each>
</xsl:variable>
As an alternative you could replace the for-each with apply-templates and then write a template matching #value1 | #value2 but while I think most tasks to transform nodes are better done using push style in XSLT I think for finding a minimum or maximum value the for-each is fine.
I'm not sure if it is absolutely correct but I tried this for min
(/Rows/Entry/#value1|/Rows/Entry/#value2)[not((/Rows/Entry/#value1|/Rows/Entry/#value2) < .)]
and this for max
(/Rows/Entry/#value1|/Rows/Entry/#value2)[not((/Rows/Entry/#value1|/Rows/Entry/#value2) > .)]
and it gave me values you mentioned. But for simplification I worked with xml with values without ",".

How to check whether a node is present in the file or not

I have xml which could have sometimes node DSD_G22 but not always. SO if that node is present then get the value of the elements otherwise assign empty:
input 1 scenario:
<root>
<G_83>
<G_8301/>
<G_8302/>
</G_83>
<DSD_G22>
<DSD_G22_G2201>Value</DSD_G22_G2201>
<DSD_G22_G2202>Value1</DSD_G22_G2202>
</DSD_G22>
</root>
Scenario Input 2:
<root>
<G_83>
<G_8301/>
<G_8302/>
</G_83>
</root>
The output for scenario 2 should be:
<G_83>
<G_8301/>
<G_8302/>
</G_83>
<DSD_G22>
<DSD_G22_G2201/>
<DSD_G22_G2202/>
<DSD_G22/>
</root>
I tried to do this but doesn't work. Please HelP:
<xsl:variable name="emptySpace" select="' '" />
<xsl:if test="#DSD_G22">
<xsl:if test="#DSD_G22_G2201">
<xsl:attribute name="DSD_G22_G2201">
<xsl:value-of select="#DSD_G22_G2201" />
</xsl:attribute>
</xsl:if>
<xsl:if test="#DSD_G22_G2202">
<xsl:attribute name="DSD_G22_G2202">
<xsl:value-of select="#DSD_G22_G2202" />
</xsl:attribute>
</xsl:if>
</xsl:if>
<xsl:if test="not(#DSD_G22)">
<xsl:attribute name="DSD_G22_G2201">
<xsl:value-of select="#emptySpace " />
</xsl:attribute>
<xsl:attribute name="DSD_G22_G2202">
<xsl:value-of select="#emptySpace" />
</xsl:attribute>
</xls:if>
This would do it for you:
<DSD_G22>
<DSD_G22_G2201><xsl:value-of select="DSD_G22/DSD_G22_G2201"/></DSD_G22_G2201>
<DSD_G22_G2202><xsl:value-of select="DSD_G22/DSD_G22_G2202"/></DSD_G22_G2202>
</DSD_G22>
<xsl:if test="count(DSD_G22) > 0">
Exists!
</xsl:if>