building an XSLT string using variable in a foreach loop - xslt

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=" - "/>

Related

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

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>

Why is this XSLT variable returning undefined?

I'm trying to write an XSLT function to select between some dates. I have parameters that are being converted to xs:date and then used to get the number of days between two of them, and format them appropriately. For some reason, the termDate variable is being reported as undefined, even though I can see in the Variables and Nodes/Values Set panels in Oxygen that the parameter references and element that does have a value.
<xsl:function name="my:getStatusDate">
<xsl:param name="rehire"/>
<xsl:param name="term"/>
<xsl:param name="hire"/>
<xsl:variable name="rehireDate" select="xs:date($rehire)" as="xs:date"/>
<xsl:variable name="termDate" select="xs:date($term)" as="xs:date"/>
<xsl:variable name="hireDate" select="xs:date($hire)" as="xs:date"/>
<xsl:variable name="dayDiffTermRehire" select="days-from-duration($termDate - $rehireDate)" as="xs:integer"/>
<xsl:choose>
<xsl:when test="$term != '' and not($term)">
<xsl:choose>
<xsl:when test="$dayDiffTermRehire > xs:integer(91)">
<xsl:value-of select="format-date($rehireDate, '[M01]/[D01]/[Y0001]')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="format-date($hireDate, '[M01]/[D01]/[Y0001]')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="format-date($hireDate, '[M01]/[D01]/[Y0001]')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:function>
The call site looks like this:
<ExpectedStatusEffectiveDate><xsl:value-of select="my:getStatusDate(RecentHireDate, TermDate, PreviousHireDate)"/></ExpectedStatusEffectiveDate>
A sample XML source looks like this:
<?xml version="1.0" encoding="UTF-8"?>
<Data>
<Entry>
<EmployeeFirstName>John</EmployeeFirstName>
<EmployeeLastName>Doe</EmployeeLastName>
<BirthDate>1940-01-01-01:00</BirthDate>
<RecentHireDate>1970-05-20-07:00</RecentHireDate>
<PreviousHireDate>1970-05-20-07:00</PreviousHireDate>
</Entry>
<Entry>
<EmployeeFirstName>Jane</EmployeeFirstName>
<EmployeeLastName>Doe</EmployeeLastName>
<BirthDate>1970-11-25-08:00</BirthDate>
<RecentHireDate>2003-12-22-08:00</RecentHireDate>
<PreviousHireDate>1970-06-19-07:00</PreviousHireDate>
<TermDate>2000-01-13-08:00</TermDate> <!-- NOTE: this entry HAS a TermDate -->
</Entry>
</Data>
Any idea why the termDate would be undefined in BOTH cases?
I'm going to chalk this up to an Oxygen bug. I'm not sure what changed, but after isolating the function into its own stylesheet, it started working. I then copied it back into the original, and everything seemed to work.

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.

XSLT - get sorted information extract from cdata

I’m new in the xslt topic and have a problem that can't solve on my own.
Here e excample of my xml file:
<node>
<failure><![CDATA[
some useless information.
CRS urn:ogc:def:crs:EPSG::25830 not defined.
CRS urn:ogc:def:crs:EPSG::25833 not possible.
CRS urn:ogc:def:crs:EPSG::25830 not defined.
some useless information.]]>
</failure>
</node>
The main problem is that the information stand in a CDATA block and many different informations are mixed up. I have found a way to get them out, but only as a string value not able to differentiate between the sort.
I need a way to extract elements that fit the pattern: "CRS [-unknown-] [id] not [result]"
What i want is something like this:
<failure>
<CRS>
<id> urn:ogc:def:crs:EPSG::25830 </id>
<result> not defined </result>
</CRS>
<CRS>
<id> urn:ogc:def:crs:EPSG::25833 </id>
<result> not posible </result>
</CRS>
<CRS>
<id> urn:ogc:def:crs:EPSG::25830 </id>
<result> not defined </result>
</CRS>
</failure>
Can somebody help me or made experience with simular problems?
XSLT 2.0 has xsl:analyze-string which is designed for precisely this task, so if at all possible I suggest you upgrade to a 2.0 processor such as Saxon:
<xsl:template match="node">
<failure>
<xsl:analyze-string select="failure"
regex="^\s*CRS\s*(\S+)\s*(not\s*.*)$" flags="m">
<xsl:matching-substring>
<CRS>
<id><xsl:value-of select="regex-group(1)" /></id>
<result><xsl:value-of select="normalize-space(regex-group(2))" /></result>
</CRS>
</xsl:matching-substring>
</xsl:analyze-string>
</failure>
</xsl:template>
String manipulation facilities in XSLT 1.0 are extremely limited in comparison, and since XSLT is a functional language without updateable variables you'd have to write some sort of hideously complex set of recursive call-template logic to split up the text into separate lines and then extract the relevant bits out of each line in turn using substring-before and substring-after.
<xsl:template name="each-line">
<xsl:param name="val" />
<!-- pull out everything before the first newline and normalize (trim leading
and trailing whitespace and squash internal whitespace to a single space
character -->
<xsl:variable name="firstLine"
select="normalize-space(substring-before($val, '
'))" />
<!-- pull out everything after the first newline -->
<xsl:variable name="rest" select="substring-after($val, '
')" />
<xsl:if test="$firstLine">
<xsl:call-template name="process-line">
<xsl:with-param name="line" select="$firstLine" />
</xsl:call-template>
</xsl:if>
<!-- if there are still some non-empty lines left then process them recursively -->
<xsl:if test="normalize-space($rest)">
<xsl:call-template name="each-line">
<xsl:with-param name="val" select="$rest" />
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="process-line">
<xsl:param name="line" />
<xsl:if test="starts-with($line, 'CRS ') and contains($line, ' not ')">
<!-- here $line will be something like
"CRS urn:ogc:def:crs:EPSG::25830 not defined." -->
<CRS>
<!-- ID is everything between the first and second spaces -->
<id><xsl:value-of select="substring-before(substring-after($line, ' '), ' ')" /></id>
<!-- result is everything after the second space -->
<result><xsl:value-of select="substring-after(substring-after($line, ' '), ' ')" /></result>
</CRS>
</xsl:if>
</xsl:template>
You would call this logic using a construct like
<xsl:template match="node">
<failure>
<xsl:call-template name="each-line">
<xsl:with-param name="val" select="failure" />
</xsl:call-template>
</failure>
</xsl:template>

xslt 1.0 - Trying to get nodes for a specific value

I've got an XML file that has many similarly named nodes but attributes within certain nodes are unique. I want to output into an HTML page only the nodes that fall under a certain attribute value.
Here is the xml:
<document>
<component>
<section>
<templateId value="temp_1" />
<entry>
<act>
<code displayName="temp_1:code_1" />
</act>
</entry>
<entry>
<act>
<code displayName="temp_1:code_2" />
</act>
</entry>
<entry>
<act>
<code displayName="temp_1:code_3" />
</act>
</entry>
</section>
<section>
<templateId value="temp_2" />
<entry>
<act>
<code displayName="temp_2:code_1" />
</act>
</entry>
<entry>
<act>
<code displayName="temp_2:code_2" />
</act>
</entry>
</section>
</component>
</document>
From this specific example, I want to only get the displayName value from the section that has the templateId value of temp_2. This is the XSL code that I'm using but it is getting everything, not just the section that I want. i know the first "when" is working because the right header (between the span tags) is displaying properly. It's just the for-each through the entries.
<xsl:tempalte match="/">
<xsl:choose>
<xsl:when test="//templateId/#value='temp_2'">
<div style="margin-bottom: 5px; padding: 5px; border-bottom: 1px solid #000000;">
<span style="font-weight: bold;">Template 2: </span>
<br />
<xsl:choose>
<xsl:when test="count(//section/entry) != 0">
<xsl:for-each select="//section/entry">
<xsl:choose>
<xsl:when test="position() = 1">
<xsl:value-of select="act/code/#displayName" />
</xsl:when>
<xsl:otherwise>
<br/>
<xsl:value-of select="act/code/#displayName" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
No codes to display
</xsl:otherwise>
</xsl:choose>
</div>
</xsl:when>
</xsl:choose>
</xsl:template>
It should display like so:
temp_2:code_1
<br>temp_2:code_2
Any help would be greatly appreciated.
I guess you want to completely restudy XSLT and its philosophy. Don't program it like it was BASIC. The basic pattern, at least for your case, is that an XSLT program is a collection of templates to handle matching elements. Instead of littering your code with if and choose, write templates with the proper matching conditions. Instead of BASIC's FOR I=1 TO 10, use <xsl:apply-templates/> to "iterate" over the children. Here's the basic idea:
<xsl:template match="/">
<html>
<xsl:apply-templates/>
</html>
</xsl:template>
<xsl:template match="templateId"/> <!-- skip templateID elements by default -->
<xsl:template match="templateId[#value='temp_2']">
<div style="margin-bottom: 5px; padding: 5px; border-bottom: 1px solid #000000;">
<span style="font-weight: bold;">Template 2: </span>
<xsl:apply-templates/>
</div>
</xsl:template>
<xsl:template match="code">
<xsl:value-of select="#displayName"/>
<xsl:if test="position() != 1"><br/></xsl:if>
</xsl:template>
<xsl:template match="section[count(entry)=0]">
No codes to display
</xsl:template>
Why no template for act elements? Well, by default XSLT will provide you with a template which does a <xsl:apply-templates/>.
Based on your description it sounds like you only want the temp_2 values in your for-each.
That being the case you can just update your select to the following:
<xsl:for-each select="//section[templateId/#value = 'temp_2']/entry">
This says to grab any entry under section that has a templateId with an attribute of value that equals 'temp_2'.