I have XML form data coming in from a vendor app that allows me to select values for fields on the form by using this format.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsl:stylesheet version="1.0" xmlns="http://www.w3.org/1999/XSL/Transform" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<xsl:output method="text" /><xsl:template match="/"><xsl:apply-templates/></xsl:template>
<xsl:template match="DataSet/diffgr:diffgram/Forms">
<xsl:for-each select="Form">
<xsl:value-of select="FieldName1"/>
<xsl:value-of select="FieldName2"/>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I can't see the XML, but I can surmise from this that all of the fields are siblings within the <form> and looks something like this:
<form>
<CosignerName1 value="John Doe"/>
<CosignerDate1 value="1/1/1970"/>
<CosignerTime1 value="8:46 PM"/>
<CosignerName2 value="Jane Smith"/>
<CosignerDate2 value="2/2/1972"/>
<CosignerTime2 value="11:46 AM"/>
...
<CosignerName12 value="Will Hunting"/>
<CosignerDate12 value="12/12/1982"/>
<CosignerTime12 value="1:00 AM"/>
</form>
<form>
<CosignerName1 value="Bill Thomas"/>
<CosignerDate1 value="5/5/2020"/>
<CosignerTime1 value="8:46 PM"/>
<CosignerName2 value="Bev Poole"/>
<CosignerDate2 value="6/6/2022"/>
<CosignerTime2 value="11:46 AM"/>
...
<CosignerName12 value="Bob Ross"/>
<CosignerDate12 value="12/12/1982"/>
<CosignerTime12 value="1:00 AM"/>
</form>
Known factors:
Each form has 12 cosigners, each with numbered field names i.e. CosignerName1, CosignerName2, etc.
Each named cosigner also has a CosignerDate and a CosignerTime field associated by number. i.e. CosignerName1 has CosignerDate1 and CosignerTime1 fields that, when combined, show when that user signed the form.
I am using the word 'idnum' in my code and in this question to refer to the number at the end of each label that shows they are part of a set i.e CosignerName1, CosignerDate1, and CosignerTime1 all share idnum=1
Goal: Identify and select only the most recent CosignerName, based on CosignerDate and CosignerTime.
To do this, I figure I need to:
Find the most recent CosignerDate/CosignerTime combination
Read the label of either of the identified fields to extract the idnum from the end
Use the idnum to to identify the correct CosignerName and output it
I have some pieces of the solution, but no way to bring them together:
Create a Timestamp
I can combine a set of CosignerDate[idnum] and CosignerTime[idnum] fields, then translate them into a timestamp. If I could create an array of these timestamps, then I could sort and find the most recent one, but as I understand it XSLT 1.0 doesn't have arrays. So I am not sure what to do with this.
<xsl:variable name="mm" select="substring-before(CosignerDate1,'/')" />
<xsl:variable name="mmyyyy" select="substring-after(CosignerDate1,'/')"/>
<xsl:variable name="dd" select="substring-before($mmyyyy,'/')" />
<xsl:variable name="yyyy" select="substring-after($mmyyyy,'/')" />
<xsl:variable name="ampm" select="substring-after(CosignerTime1,' ')" />
<xsl:variable name="time">
<xsl:choose>
<xsl:when test="$ampm = 'AM'">
<xsl:value-of select="number(translate(substring-before(CosignerTime1,' '),':',''))" />
</xsl:when>
<xsl:when test="$ampm = 'PM'">
<xsl:value-of select="number(translate(substring-before(CosignerTime1,' '),':',''))+number('1200')" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:value-of select="concat('Timestamp:',$yyyy,$mm,$dd,$time)"/>
The code above outputs something like: Timestamp:202209191128
Get idnums
I can loop through all of the Cosigners and extract the identifying number shared by their labels.
<xsl:for-each select="*[starts-with(local-name(), 'CosignerName')]">
<xsl:variable name="idnum">
<xsl:value-of select="translate(local-name(),'CosignerName','')" />
</xsl:variable>
<xsl:value-of select="$idnum"/>
<xsl:value-of select="','"/>
</xsl:for-each>
This code above outputs something like: 1,4,7,12
But, this is where I get lost.
Failed Attempt
I tried something like the code below. The logic was to loop through each Cosigner name (12 iterations), get the idnum from the label, use that to get the CosignerDate and CosignerTime, create a timestamp, then sort the timestamps, if the timestamp we are looking at is the top one (most recent) then use the idnum to select the correct CosignerName and output it.
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<xsl:stylesheet version="1.0" xmlns="http://www.w3.org/1999/XSL/Transform" xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<xsl:output method="text" /><xsl:template match="/"><xsl:apply-templates/></xsl:template>
<xsl:template match="DataSet/diffgr:diffgram/Forms">
<xsl:for-each select="Form">
<xsl:for-each select="*[starts-with(local-name(), 'CosignerName')]">
<xsl:variable name="idnum">
<xsl:value-of select="translate(local-name(),'CosignerName','')" />
</xsl:variable>
<xsl:variable name="mm" select="substring-before(concat(CosignerDate,$idnum),'/')" />
<xsl:variable name="mmyyyy" select="substring-after(concat(CosignerDate,$idnum),'/')"/>
<xsl:variable name="dd" select="substring-before($mmyyyy,'/')" />
<xsl:variable name="yyyy" select="substring-after($mmyyyy,'/')" />
<xsl:variable name="ampm" select="substring-after(concat(CosignerTime,$idnum),' ')" />
<xsl:variable name="time">
<xsl:choose>
<xsl:when test="$ampm = 'AM'">
<xsl:value-of select="number(translate(substring-before(concat(CosignerTime,$idnum),' '),':',''))" />
</xsl:when>
<xsl:when test="$ampm = 'PM'">
<xsl:value-of select="number(translate(substring-before(concat(CosignerTime,$idnum),' '),':',''))+number('1200')" />
</xsl:when>
</xsl:choose>
</xsl:variable>
<xsl:variable name="timestamp" select="concat('Timestamp: ',$yyyy,$mm,$dd,$time)"/>
<xsl:sort select="$timestamp" data-type="number" order="descending"/>
<xsl:if test="position() = 1"><xsl:value-of select="."/></xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
This of course fails for several reasons:
the concat with the $idnum doesn't actually pull values from fields
the sort has to be the first thing in the for-each
the sort has to happen after all of the timestamps are created
the sorted list of timestamps still needs to reference which idnum they were generated from
Without being able to create an array of timestamps and idnums, I don't know how you can accomplish this.
Does anyone know how can I get at the CosignerName[idnum] that is correlated with the most recent CosignerDate[idnum]/CosignerTime[idnum]?
I believe you could just sort the CosignerNameX nodes by the individual components of their associated dates and times, as shown in the two other answers I linked to:
https://stackoverflow.com/a/59288030/3016153
https://stackoverflow.com/a/30631073/3016153
However, it might be more efficient - and certainly more readable - to do this in two steps:
Construct a dateTime value for each cosigner;
Find the cosigner with the most recent value.
Consider the following example:
XML
<DataSet xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1">
<diffgr:diffgram>
<Forms>
<form>
<CosignerName1 value="John Doe"/>
<CosignerDate1 value="1/1/1970"/>
<CosignerTime1 value="8:46 PM"/>
<CosignerName2 value="Jane Smith"/>
<CosignerDate2 value="2/2/1972"/>
<CosignerTime2 value="11:46 AM"/>
<CosignerName12 value="Will Hunting"/>
<CosignerDate12 value="12/12/1982"/>
<CosignerTime12 value="1:00 AM"/>
</form>
<form>
<CosignerName1 value="Bill Thomas"/>
<CosignerDate1 value="5/5/2020"/>
<CosignerTime1 value="8:46 PM"/>
<CosignerName2 value="Bev Poole"/>
<CosignerDate2 value="6/6/2022"/>
<CosignerTime2 value="11:46 AM"/>
<CosignerName12 value="Bob Ross"/>
<CosignerDate12 value="12/12/1982"/>
<CosignerTime12 value="1:00 PM"/>
</form>
</Forms>
</diffgr:diffgram>
</DataSet>
XSLT 1.0 + EXSLT node-set()
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:diffgr="urn:schemas-microsoft-com:xml-diffgram-v1"
xmlns:exsl="http://exslt.org/common"
exclude-result-prefixes="diffgr exsl">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/DataSet">
<output>
<xsl:for-each select="diffgr:diffgram/Forms/form">
<xsl:variable name="datetimes">
<xsl:for-each select="*[starts-with(local-name(), 'CosignerName')]">
<!-- identify values -->
<xsl:variable name="i" select="substring-after(local-name(), 'CosignerName')" />
<xsl:variable name="date" select="../*[local-name()=concat('CosignerDate', $i)]/#value" />
<xsl:variable name="time" select="../*[local-name()=concat('CosignerTime', $i)]/#value" />
<!-- extract date components -->
<xsl:variable name="year" select="substring-after(substring-after($date, '/'), '/')" />
<xsl:variable name="month" select="substring-before($date, '/')" />
<xsl:variable name="day" select="substring-before(substring-after($date, '/'), '/')" />
<!-- extract time components -->
<xsl:variable name="hour12" select="substring-before($time, ':')" />
<xsl:variable name="minute" select="substring-before(substring-after($time, ':'), ' ')" />
<xsl:variable name="pm" select="contains($time,'PM')" />
<xsl:variable name="hour" select="$hour12 mod 12 + 12*$pm"/>
<!-- construct dateTime -->
<datetime cosigner="{#value}" index="{$i}">
<xsl:value-of select="$year"/>
<xsl:value-of select="format-number($month, '-00')"/>
<xsl:value-of select="format-number($day, '-00')"/>
<xsl:value-of select="format-number($hour, 'T00')"/>
<xsl:value-of select="format-number($minute, ':00')"/>
<xsl:text>:00</xsl:text>
</datetime>
</xsl:for-each>
</xsl:variable>
<!-- output -->
<form>
<xsl:for-each select="exsl:node-set($datetimes)/datetime">
<xsl:sort select="." data-type="text" order="descending"/>
<xsl:if test="position()=1">
<last-cosigner index="{#index}" dateTime="{.}" >
<xsl:value-of select="#cosigner"/>
</last-cosigner>
</xsl:if>
</xsl:for-each>
</form>
</xsl:for-each>
</output>
</xsl:template>
</xsl:stylesheet>
Result
<?xml version="1.0" encoding="UTF-8"?>
<output>
<form>
<last-cosigner index="12" dateTime="1982-12-12T01:00:00">Will Hunting</last-cosigner>
</form>
<form>
<last-cosigner index="2" dateTime="2022-06-06T11:46:00">Bev Poole</last-cosigner>
</form>
</output>
P.S. With a Microsoft processor you may need to change the namespace declaration:
xmlns:exsl="http://exslt.org/common"
to:
xmlns:msxsl="urn:schemas-microsoft-com:xslt"
and then change the exsl prefix in lines #5 and #39 to msxsl.
I have an XML file for which I have to frame XSL so that my ETL job can process it. Here is what i need.
Input XML
<FS_Sub_Investment_Team>
<Report Name="REPORT">
<MeetTheTeamTableHeading Label="TEAM" />
<MeetTheTeamTable>
<Rows>
<Row ManagerData="ABC EFGHI XYZ" />
<Row ManagerData="ABC PQRST XYZ" />
</Rows>
</MeetTheTeamTable>
<MeetTheTeamNote></MeetTheTeamNote>
</Report>
</FS_Sub_Investment_Team>
Current XSL:
<xsl:template match="/">
<Bio>
<BioText>
<xsl:apply-templates select="//Row/#ManagerData" mode="concat"/>
</BioText>
</Bio>
</xsl:template>
Current Output :
<Bio><BioText>ABC EFGHI XYZABC PQRST XYZ</Bio></BioText>
My expected output is: With line break (or) with space between the two element values, please give me both solutions.
<Bio><BioText>ABC EFGHI XYZ
ABC PQRST XYZ</Bio></BioText>
Try:
<xsl:template match="/">
<Bio>
<BioText>
<xsl:for-each select="//Row">
<xsl:value-of select="#ManagerData" />
<xsl:if test="position()!=last()">
<xsl:text>
</xsl:text>
</xsl:if>
</xsl:for-each>
</BioText>
</Bio>
</xsl:template>
To insert a space instead of a new line, use:
<xsl:text> </xsl:text>
instead of:
<xsl:text>
</xsl:text>
I have a generic template I've designed with 2 params title and category.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.1"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:param name="category" />
<xsl:param name="title" />
<xsl:template name="test-group">
<fo:block>
<xsl:value=of select="$title" />
</fo:block>
<fo:block>
<xsl:value-of select="$category" />
</fo:block>
</xsl:template>
</xsl:stylesheet>
In the parent template I have the following:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.1" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:fo="http://www.w3.org/1999/XSL/Format">
<xsl:include href="templates/generic_template.xsl" />
...
<xsl:call-template name="test-group">
<xsl:with-param name="category" select="'animals'" />
<xsl:with-param name="title" select="'DOGS'" />
</xsl:call-template>
...
</xsl:stylesheet>
However, when the transform completes title and category are blank. I'm using FOP 2.0 so I'm not sure if this is a known shortcoming.
When defining a xsl:template that takes parameters, the parameter names used within the xsl:template should be declared using xls:param elements nested within.
<xsl:template name="test-group">
<xsl:param name="category" />
<xsl:param name="title" />
...
</xsl:template>
The parameters being attached to the xsl:template and not the xsl:stylesheet.
This is similar to when calling the template with xsl:call-template, except you are specifying the values instead using xsl:with-param.
I have sample xml like this:
User
name A
Id 8
User
name B
Id 5
User
name C
Id 16
User
name D
Id 10
...
This is what i expect, if input is
A only --> output should be A.
B only --> output should be B.
A and B only --> output should be B.
Any users other than A and B --> output should be name of user with lowest id.
A, B and other users --> user with lowest id except A and B.
I have tried different variants of below template
<xsl:template name="getPriorityName">
<xsl:for-each select="//*[local-name()='User']">
<xsl:sort select="./*[local-name()='Id']" data-type="number"/>
<xsl:variable name="name">
<xsl:value-of select="./*[local-name()='name']"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="$name!='A' and $name!='B'">
<xsl:if test="position()=1">
<xsl:value-of select="$name"/>
</xsl:if>
</xsl:when>
</xsl:choose>
</xsl:for-each>
</xsl:template>
But since i am unable to club name condition with for-each it does not work in case when A and B are there in input
Thanks Jirka :)
I modified the tempalte a little bit and now it looks ok:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/Users">
<xsl:variable name="UsersCount" select="count(User)" />
<xsl:variable name="UsersExceptAB" select="User[Name != 'A' and Name !='B']" />
<xsl:variable name="UsersAB" select="User[Name = 'A' or Name ='B']" />
<xsl:variable name="UserInABWithMinimumId" select="$UsersAB[not($UsersAB/Id < ./Id)]" />
<xsl:variable name="UserExceptABWithMinimumId" select="$UsersExceptAB[not($UsersExceptAB/Id < ./Id)]" />
<result>
<xsl:choose>
<xsl:when test="$UsersExceptAB">
<xsl:copy-of select="$UserExceptABWithMinimumId" />
</xsl:when>
<xsl:when test="$UsersAB">
<xsl:copy-of select="$UserInABWithMinimumId" />
</xsl:when>
<xsl:otherwise>
<xsl:comment>Oops, something is wrong.</xsl:comment>
</xsl:otherwise>
</xsl:choose>
</result>
</xsl:template>
</xsl:stylesheet>
Is there a way i can just assign the name to a variable; let's say; priorityName that i can use later, i have some other logic based on this priorityName in my application. I tried to access name from node, but since what we finally have is a node set(always of size 1 - either UserInABWithMinimumId or UserExceptABWithMinimumId). This is what i tried:
<xsl:variable name="priorityName">
<xsl:choose>
<xsl:when test="$UsersExceptAB">
<xsl:value-of select="$UserExceptABWithMinimumId/User/Name" />
</xsl:when>
<xsl:when test="$UsersAB">
<xsl:value-of select="$UserInABWithMinimumId/User/Name" />
</xsl:when>
<xsl:otherwise>
<xsl:comment>Oops, something is wrong.</xsl:comment>
</xsl:otherwise>
</xsl:choose>
</xsl:variable>
Thanks!!!
You could try following XSLT
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:template match="/Users">
<xsl:variable name="UsersCount" select="count(User)" />
<xsl:variable name="UsersExceptAB" select="User[Name != 'A' and Name !='B']" />
<xsl:variable name="UsersExceptABWithMinimumId" select="$UsersExceptAB[not($UsersExceptAB/Id < ./Id)]" />
<result>
<xsl:choose>
<!-- There exists some User with other name than A or B-->
<xsl:when test="$UsersExceptAB">
<xsl:copy-of select="$UsersExceptABWithMinimumId" />
</xsl:when>
<!-- There exists no User with other than A or B but there is a User with name B -->
<xsl:when test="User[Name = 'B']">
<xsl:copy-of select="User[Name = 'B']" />
</xsl:when>
<!-- There is no User with other than A -->
<xsl:when test="User[Name='A']">
<xsl:copy-of select="User[Name = 'A']" />
</xsl:when>
<!-- Probably there is no User -->
<xsl:otherwise>
<xsl:comment>Oops, something is wrong.</xsl:comment>
</xsl:otherwise>
</xsl:choose>
</result>
</xsl:template>
</xsl:stylesheet>
When I used this input
<?xml version="1.0" encoding="UTF-8"?>
<Users>
<User>
<Name>A</Name>
<Id>8</Id>
</User>
<User>
<Name>B</Name>
<Id>9</Id>
</User>
<User>
<Name>C</Name>
<Id>6</Id>
</User>
<User>
<Name>D</Name>
<Id>10</Id>
</User>
<User>
<Name>E</Name>
<Id>11</Id>
</User>
</Users>
(and commented different User for testing purpose) it returned me desired output.
Is it possible to have a better solution for the following:
Input xml
<product>
<text>
<languageId>en-us</languageId>
<description>some text en us</description>
</text>
<text>
<languageId>en-gb</languageId>
<description>some text en gb</description>
</text>
<text>
<languageId>en-us</languageId>
</text>
<product>
Output xml
<specifications>some text en us</specifications>
So, if there is a description with languageId = en-us and there is text present, then this text will be placed in the output xml, otherwise the element receives the attribute value xsi:nil=true
xslt version must be 1.0
XSLT
<ns0:specifications>
<!-- First loop, check if en-us is present, if so, check if there is a text! -->
<!-- If the 2 requirements are met, then this Txt element is used -->
<xsl:for-each select="s0:text">
<xsl:if test="translate(s0:LanguageId/text(),$smallcase,$uppercase)=translate('en-us',$smallcase,$uppercase)">
<xsl:if test="s0:Txt!=''">
<xsl:value-of select="s0:Txt/text()" />
</xsl:if>
</xsl:if>
</xsl:for-each>
<!-- Second loop, checks are the same. This loop is needed because xsl variables are immutable. If there is a better solution, just change the code!! -->
<!-- If the 2 requirements are met, then the variable is marked as true, else it's empty -->
<xsl:variable name="isEnUsPresent">
<xsl:for-each select="s0:text">
<xsl:if test="translate(s0:LanguageId/text(),$smallcase,$uppercase)=translate('en-us',$smallcase,$uppercase)">
<xsl:if test="s0:Txt!=''">
<xsl:value-of select="1" />
</xsl:if>
</xsl:if>
</xsl:for-each>
</xsl:variable>
<!-- if the variable is empty, set the attribute value xsi:nil=true like below -->
<xsl:choose>
<xsl:when test="$isEnUsPresent=''">
<xsl:attribute name="xsi:nil">
<xsl:value-of select="'true'" />
</xsl:attribute>
</xsl:when>
</xsl:choose>
</ns0:specifications>
Regards
As simple as this:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
exclude-result-prefixes="xsi">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:template match="/">
<specifications>
<xsl:apply-templates select="*"/>
</specifications>
</xsl:template>
<xsl:template match="text[languageId='en-us']/description">
<xsl:value-of select="."/>
</xsl:template>
<xsl:template match="/*[not(text[languageId='en-us']/description)]">
<xsl:attribute name="xsi:nil">true</xsl:attribute>
</xsl:template>
<xsl:template match="text()"/>
</xsl:stylesheet>
When this transformation is applied on the provided XML document:
<product>
<text>
<languageId>en-us</languageId>
<description>some text en us</description>
</text>
<text>
<languageId>en-gb</languageId>
<description>some text en gb</description>
</text>
<text>
<languageId>en-us</languageId>
</text>
</product>
the wanted, correct result is produced:
<specifications>some text en us</specifications>
When the same transformation is applied to this XML document:
<product>
<text>
<languageId>en-us-XXX</languageId>
<description>some text en us</description>
</text>
<text>
<languageId>en-gb</languageId>
<description>some text en gb</description>
</text>
<text>
<languageId>en-us</languageId>
</text>
</product>
again the wanted, correct result is produced:
<specifications xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true"/>