I have a long XML file from which I ned to pull out book titles and other information, then sort it alphabetically, with a separator for each letter. I also need a section for items that don't begin with a letter, say a number or symbol. Something like:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
…
I also need to create a separate list of authors, created from the same data but showing different kinds of information.
How I'm currently doing it
This is simplified, but I basically have this same code twice, once for titles and once for authors. The author version of the template works with different elements and does different things with the data, so I can't use the same template.
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'#'" />
</xsl:call-template>
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'A'" />
</xsl:call-template>
…
<xsl:call-template name="BIP-letter">
<xsl:with-param name="letter" select="'Z'" />
</xsl:call-template>
<xsl:template name="BIP-letter">
<xsl:param name="letter" />
<xsl:choose>
<xsl:when test="$letter = '#'">
<xsl:text>#</xsl:text>
<xsl:for-each select="//Book[
not(substring(Title,1,1) = 'A') and
not(substring(Title,1,1) = 'B') and
…
not(substring(Title/,1,1) = 'Z')
]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$letter" />
<xsl:for-each select="//Book[substring(Title,1,1) = $letter]">
<xsl:sort select="Title" />
<xsl:appy-templates select="Title" />
<!-- Add other relevant data here -->
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
My questions
The code above works just fine, but:
Manually cycling through each letter gets very long, especially having to do it twice. Is there a way to simplify that? Something like a <xsl:for-each select="[A-Z]"> that I could use to set the parameter when calling the template?
Is there a simpler way to select all titles that don't begin with a letter? Something like //Book[not(substring(Title,1,1) = [A-Z])?
There may be cases where the title or author name starts with a lowercase letter. In the code above, they would get grouped with under the # heading, rather than with the actual letter. The only way I can think to accommodate that—doing it manually—would significantly bloat up the code.
This solution answers all questions asked:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:variable name="vLowercase" select="'abcdefghijklmnopqrstuvuxyz'"/>
<xsl:variable name="vUppercase" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"/>
<xsl:variable name="vDigits" select="'0123456789'"/>
<xsl:key name="kBookBy1stChar" match="Book"
use="translate(substring(Title, 1, 1),
'abcdefghijklmnopqrstuvuxyz0123456789',
'ABCDEFGHIJKLMNOPQRSTUVWXYZ##########'
)"/>
<xsl:template match="/*">
<xsl:apply-templates mode="firstInGroup" select=
"Book[generate-id()
= generate-id(key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)[1]
)
]">
<xsl:sort select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book" mode="firstInGroup">
<xsl:value-of select="'
'"/>
<xsl:value-of select="translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)"/>
<xsl:apply-templates select=
"key('kBookBy1stChar',
translate(substring(Title, 1, 1),
concat($vLowercase, $vDigits),
concat($vUppercase, '##########')
)
)">
<xsl:sort select="Title"/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="Book">
<xsl:value-of select="'
'"/>
<xsl:value-of select="concat(Title, ' - ', Binding, ', $', price)"/>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following xml document (none provided in the question!):
<Books>
<Book>
<Title>After the Sands</Title>
<Binding>paperback</Binding>
<price>24.95</price>
</Book>
<Book>
<Title>Cats Galore: A Compendium of Cultured Cats</Title>
<Binding>hardcover</Binding>
<price>5.00</price>
</Book>
<Book>
<Title>Arctic Spirit</Title>
<Binding>hardcover</Binding>
<price>65.00</price>
</Book>
<Book>
<Title>1494</Title>
<Binding>hardcover</Binding>
<price>9.99</price>
</Book>
<Book>
<Title>Back to the Front</Title>
<Binding>paperback</Binding>
<price>18.95</price>
</Book>
</Books>
the wanted, correct result is produced:
#
1494 - hardcover, $9.99
A
After the Sands - paperback, $24.95
Arctic Spirit - hardcover, $65.00
B
Back to the Front - paperback, $18.95
C
Cats Galore: A Compendium of Cultured Cats - hardcover, $5.00
Explanation:
Use of the Muenchian method for grouping
Use of the standard XPath translate() function
Using mode to process the first book in a group of books starting with the same (case-insensitive) character
Using <xsl:sort> to sort the books in alphabetical orser
The most problematic part is this:
I also need a section for items that don't begin with a letter, say a number or symbol.
If you have a list of all possible symbols that an item can begin with, then you can simply use translate() to convert them all to the # character. Otherwise it gets more complicated. I would try something like:
XSLT 1.0 (+ EXSLT node-set())
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:exsl="http://exslt.org/common"
extension-element-prefixes="exsl">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:key name="book" match="Book" use="index" />
<xsl:template match="/Books">
<!-- first-pass: add index char -->
<xsl:variable name="books-rtf">
<xsl:for-each select="Book">
<xsl:copy>
<xsl:copy-of select="*"/>
<index>
<xsl:variable name="index" select="translate(substring(Title, 1, 1), 'abcdefghijklmnopqrstuvwxyz', 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')" />
<xsl:choose>
<xsl:when test="contains('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $index)">
<xsl:value-of select="$index"/>
</xsl:when>
<xsl:otherwise>#</xsl:otherwise>
</xsl:choose>
</index>
</xsl:copy>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="books" select="exsl:node-set($books-rtf)/Book" />
<!-- group by index char -->
<xsl:for-each select="$books[count(. | key('book', index)[1]) = 1]">
<xsl:sort select="index"/>
<xsl:value-of select="index"/>
<xsl:text>
</xsl:text>
<!-- list books -->
<xsl:for-each select="key('book', index)">
<xsl:sort select="Title"/>
<xsl:value-of select="Title"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="Binding"/>
<xsl:text>, </xsl:text>
<xsl:value-of select="Price"/>
<xsl:text>
</xsl:text>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
However, this still leaves the problem of items that begin with a diacritic, e.g. "Österreich" or say a Greek letter. Under this method they too will be clumped under #.
Unfortunately, the only good solution for this is to move to XSLT 2.0.
Demo: https://xsltfiddle.liberty-development.net/jyRYYjj/2
Related
I have gathered bits and pieces of this XSLT from these forums. I'm trying to put them altogether to create a single, generic XSLT that can be used to convert XML to CSV by specifying the path to the nodes that should be included in the CSV file.
I have three things that I still can't figure out after about 10 hours of messing with it.
I want to iterate over each column named in csv:columns. During each iteration, I need to extract and store the text() of the column. I think this is the way to iterate, but want to make sure:
<xsl:for-each select="document('')/*/csv:columns/*">
Once I have the text() from the column, I need to put that into the columnname variable in such a way that it works when it is used with getNodeValue.
I was unable to set columnname using variable. If I didn't hard-code the value (surrounded by apostrophes), I could not get it to work. This is why I have the following line in the code:
<xsl:variable name="columnname" select="'location/city'" />
I want to pass the result of getNodeValue into quotevalue so that the result is properly quoted.
The XSLT:
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://nowhere/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<xsl:variable name="delimiter" select="','" />
<csv:columns>
<column>title</column>
<column>location/city</column>
</csv:columns>
<xsl:template match="job">
<xsl:value-of select="concat(#id, ',')"/>
<!-- #1 I WANT TO LOOP THROUGH ALL OF THE CSV COLUMNS HERE -->
<!-- #2 How do I put the text into the variable 'columnname' variable so that it works with getNodeValue? -->
<xsl:variable name="columnname" select="'location/city'" />
<xsl:variable name="vXpathExpression" select="$columnname"/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
<!-- #3 After getNodeValue gets the value, I want to send that value into 'quotevalue' -->
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:value-of select="$pCurrentNode/*[name()=$pExpression]"/>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pExpression"
select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select=
"$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<!-- Quote the value if required -->
<xsl:when test="contains($value, '"')">
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:when test="contains($value, $delimiter)">
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$value"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
Sample XML
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://nowhere/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="2006-04">
<job id="2830302">
<employer>Acme</employer>
<title>Manager</title>
<description>Full time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Los Angeles</city>
<state>California</state>
</location>
</job>
<job id="2830303">
<employer>Acme</employer>
<title>Clerk, evenings</title>
<description>Part time</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<location>
<city>Albany</city>
<state>New York</state>
</location>
</job>
</positionfeed>
The current output using the XSLT I provided
2830302,Los Angeles
2830303,Albany
The output if the XSLT works as desired
2830302,Manager,Los Angeles
2830303,"Clerk, evenings",Albany
Solution (many thanks to Tim's help below)
<?xml version="1.0"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:csv="csv:csv" xpath-default-namespace="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/" >
<xsl:output method="text" encoding="utf-8" />
<xsl:strip-space elements="*" />
<!-- Set the value of the delimiter character -->
<xsl:variable name="delimiter" select="','" />
<!-- The name of the node that contains the column values -->
<xsl:param name="containerNodeName" select="'job'"/>
<!-- All nodes that should be ignored during processing -->
<xsl:template match="source|feeddate"/>
<!-- The names of the nodes to be included in the CSV file -->
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="Category">category</column>
<column header="Description">description</column>
<column header="PostingDate">postingdate</column>
<column header="URL">joburl</column>
<column header="City">location/city</column>
<column header="State">location/state</column>
</xsl:variable>
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- ************** DO NOT TOUCH BELOW **************** -->
<!-- Warn about unmatched nodes -->
<xsl:template match="*">
<xsl:message terminate="no">
<xsl:text>WARNING: Unmatched element: </xsl:text>
<xsl:value-of select="name()"/>
</xsl:message>
<xsl:apply-templates/>
</xsl:template>
<!-- Generate the column headers -->
<xsl:template match="//*[*[local-name()=$containerNodeName]]">
<xsl:value-of select="'Id'"/>
<xsl:value-of select="$delimiter"/>
<xsl:for-each select="$columns/#header">
<xsl:variable name="colname" select="." />
<xsl:value-of select="$colname"/>
<xsl:if test="position() != last()">
<xsl:value-of select="$delimiter"/>
</xsl:if>
</xsl:for-each>
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
<!-- Generate the rows of column data -->
<xsl:template match="//*[local-name()=$containerNodeName]">
<!-- TODO: Handle attributes generically -->
<xsl:value-of select="#id"/>
<xsl:variable name="container" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pCurrentNode" select="$container"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template name="getQuotedNodeValue">
<xsl:param name="pExpression"/>
<xsl:param name="pCurrentNode" select="."/>
<xsl:choose>
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:variable name="result" select="$pCurrentNode/*[name()=$pExpression]"/>
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$result"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="getQuotedNodeValue">
<xsl:with-param name="pExpression" select="substring-after($pExpression, '/')"/>
<xsl:with-param name="pCurrentNode" select= "$pCurrentNode/*[name()=substring-before($pExpression, '/')]"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="quotevalue">
<xsl:param name="value"/>
<xsl:choose>
<xsl:when test="contains($value, '"')">
<!-- Quote the value and escape the double-quotes -->
<xsl:variable name="x" select="replace($value, '"', '""')"/>
<xsl:value-of select="concat('"', $x, '"')"/>
</xsl:when>
<xsl:otherwise>
<!-- Quote the value -->
<xsl:value-of select="concat('"', $value, '"')"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
Sample data to demonstrate solution
<?xml version="1.0" encoding="utf-8"?>
<positionfeed
xmlns="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.job-search-engine.com/add-jobs/positionfeed-namespace/ http://www.job-search-engine.com/add-jobs/positionfeed.xsd"
version="2006-04">
<source>Casting360</source>
<feeddate>2016-11-11T21:48:34Z</feeddate><job id="1363612">
<employer>Casting360</employer>
<title>The Robert Irvine Show Is Seeking Guests</title>
<category>Reality TV</category>
<description>TV personality ROBERT IRVINE (Restaurant Impossible) is seeking guests looking for solutions to their unique problems to share their stories on his show!
Our next show is Thursday, September 22nd in LA. If you're not in LA we will provide your airfare, hotel, car service, and per diem.
Please note: WE ARE NOT LOOKING FOR RESUMES; THIS IS NOT AN ACTING GIG. We are looking for real people to share their stories!
*appearance fee (TBD)
If you or someone you know has a conflict that they need help resolving, WE WANT TO HEAR FROM YOU.
Please email tvgal.ri#gmail.com the following information:
Name
Phone number
Your story in 2-3 paragraphs
1-3 photos of yourself.</description>
<postingdate>2016-09-15T23:12:13Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1363612&city=Los+Angeles&state=CA</joburl>
<location>
<nation>USA</nation>
<city>Los Angeles</city>
<state>California</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370302">
<employer>Casting360</employer>
<title>Photoshoot for Publication</title>
<category>Modeling</category>
<description>6 FEMALE Models are wanted for publication photoshoot.
If you're not in the NYC Vicinity (NY, Pa, Ct,) DO NOT REPLY because your response will be summarily ignored.
Chosen models will be given a 5 look photo shoot. The shoot will occur on location (outdoors) in highly public locations chosen both for it's convenience and scenery.
The 5 looks (outfits) will be pre-determined by our staff of items most outfits within a model's wardrobe.
THIS IS A TF (UNPAID) SHOOT. After the release of the magazine, the photos agreed upon from the shoot shall be given to the model (in digital format) for her to build her portfolio.
Chosen models will receive a 5 outfit photo shoot at no cost to them by a NY Fashion Photographer.As a result, chosen models not only receive a free photo shoot, but also become PUBLISHED MODELS featured in a magazine.
The model (Janeykay) centered in the photo attached (Please look at the attached photo) is a Casting360 member who not only received her photo shoot, not only is being featured in a magazine, but also made the cover becoming a Cover Model from her shoot with us.</description>
<postingdate>2016-10-03T00:34:43Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370302&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job><job id="1370962">
<employer>Casting360</employer>
<title>Actresses Needed for "Red Shore", Action Film</title>
<category>Acting</category>
<description>CASTING (non-union)
We are a New Independent company looking to shoot our first feature. We are currently looking to fill two Major roles.
Female/African American, Hispanic, Asian, Pacific Islander/ 5'5-5'10/ Age Late 30's-Early 40's.
Project description: A long standing feud between two best friends turned enemies escalates over a valuable Diamond on display in a New York City Museum. With the stakes high they each seek the help of both friends and strangers to settle their feud once and for all.
Please note this is a non-paid project.
Fight training will be provided for free.
Please email including age and height in your e-mail.
Those selected will be invited to our audition.</description>
<postingdate>2016-10-03T14:18:20Z</postingdate>
<joburl>http://casting360.com/lgj/8886644624?jobid=1370962&city=New+York&state=NY</joburl>
<location>
<nation>USA</nation>
<city>New York</city>
<state>New York</state>
</location>
<jobsource>Casting360</jobsource>
</job>
</positionfeed>
As you are using XSLT 2.0, you could define your columns in a variable like so:
<xsl:variable name="columns" as="element()*">
<column>title</column>
<column>location/city</column>
</xsl:variable>
Then you can just iterate over them with a simple statement
<xsl:for-each select="$columns">
But the problem you may be having is that within this xsl:for-each you have changed context. You are no longer positioned on a job element, but the column element, and you don't want your expression to be relative to that. You really need to swap back to being on the job element, which you can do simply by setting a variable reference to the job element before the xsl:for-each and then using that as a parameter to the named template:
<xsl:template match="job">
<xsl:value-of select="#id"/>
<xsl:variable name="job" select="." />
<xsl:for-each select="$columns">
<xsl:value-of select="$delimiter"/>
<xsl:variable name="vXpathExpression" select="."/>
<xsl:call-template name="getNodeValue">
<xsl:with-param name="pCurrentNode" select="$job"/>
<xsl:with-param name="pExpression" select="$vXpathExpression"/>
</xsl:call-template>
</xsl:for-each>
<xsl:text>
</xsl:text>
</xsl:template>
As for quoting the result; instead of doing just xsl:value-of simply call the quote template with the value as a parameter
<xsl:when test="not(contains($pExpression, '/'))">
<xsl:call-template name="quotevalue">
<xsl:with-param name="value" select="$pCurrentNode/*[name()=$pExpression]" />
</xsl:call-template>
</xsl:when>
EDIT: If you want a header row of column names, you would have to match the parent of the job node, and then just output the values of the $column variable
<xsl:template match="*[job]">
<xsl:value-of select="$columns" separator="," />
<xsl:text>
</xsl:text>
<xsl:apply-templates />
</xsl:template>
Or maybe this if you didn't want the full path
<xsl:value-of select="$columns/(tokenize(., '/')[last()])" separator="," />
Or you could extend your columns variable to have the header text
<xsl:variable name="columns" as="element()*">
<column header="Title">title</column>
<column header="City">location/city</column>
</xsl:variable>
Then you would do this...
<xsl:value-of select="$columns/#header" separator="," />
I read lot of articles but did not find a conclusive help to my problem.
I have an XML document to which I apply an xslt to get a csv file as output.
I send a parameter to my xsl transformation to filter the target nodes to apply the templates.
The xml document looks like that (I removed some unuseful nodes for comprehension):
<GetMOTransactionsResponse xmlns="http://www.exane.com/pott" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.exane.com/pott PoTTMOTransaction.xsd">
<MOTransaction>
<Transaction VersionNumber="2" TradeDate="2013-11-20">
<TransactionId Type="Risque">32164597</TransactionId>
<InternalTransaction Type="Switch">
<BookCounterparty>
<Id Type="Risque">94</Id>
</BookCounterparty>
</InternalTransaction>
<SalesPerson>
<Id Type="Risque">-1</Id>
</SalesPerson>
</Transaction>
<GrossPrice>58.92</GrossPrice>
<MOAccount Account="TO1E" />
<Entity>0021</Entity>
</MOTransaction>
<MOTransaction>
<Transaction VersionNumber="1" TradeDate="2013-11-20">
<TransactionId Type="Risque">32164598</TransactionId>
<SalesPerson>
<Id Type="Risque">-1</Id>
</SalesPerson>
</Transaction>
<GrossPrice>58.92</GrossPrice>
<MOAccount Account="TO3E" />
<Entity>0021</Entity>
</MOTransaction>
</GetMOTransactionsResponse>
My xslt is below (sorry it's quite long, and I write it more simple than it really is):
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:pott="http://www.exane.com/pott">
<xsl:output method="text" omit-xml-declaration="no" indent="no" />
<xsl:param name="instrumentalSystem"></xsl:param>
<xsl:template name="abs">
<xsl:param name="n" />
<xsl:choose>
<xsl:when test="$n = 0">
<xsl:text>0</xsl:text>
</xsl:when>
<xsl:when test="$n > 0">
<xsl:value-of select="format-number($n, '#')" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="format-number(0 - $n, '#')" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="outputFormat">
<!--Declaration of variables-->
<xsl:variable name="GrossPrice" select="pott:GrossPrice" />
<xsl:variable name="TransactionId" select="pott:Transaction/pott:TransactionId[#Type='Risque']" />
<xsl:variable name="VersionNumber" select="pott:Transaction/#VersionNumber" />
<!--Set tags values-->
<xsl:value-of select="$Entity" />
<xsl:text>;</xsl:text>
<xsl:value-of select="concat('0000000', pott:MOAccount/#Account) "/>
<xsl:text>;</xsl:text>
<xsl:text>;</xsl:text>
<xsl:value-of select="$TransactionId" />
<xsl:text>;</xsl:text>
<xsl:value-of select="$VersionNumber" />
<xsl:text>
</xsl:text>
</xsl:template>
<xsl:template match="/">
<xsl:choose>
<!-- BB -->
<xsl:when test="$instrumentalSystem = 'BB'">
<!--xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction]"-->
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction]">
<xsl:call-template name="outputFormat"></xsl:call-template>
</xsl:for-each>
</xsl:when>
<!-- CP -->
<xsl:when test="$instrumentalSystem = 'CP'">
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[not(pott:InternalTransaction)]">
<xsl:call-template name="outputFormat"></xsl:call-template>
</xsl:for-each>
</xsl:when>
<xsl:otherwise>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
If parameter = BB, I want to select MOTransaction nodes that have a child Transaction that contains a InternalTransaction node.
If parameter = CP, I want to select MOTransaction nodes that don't have a child Transaction that contains a InternalTransaction node
When I write
pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction], I get the Transaction nodes and not the MOTransaction nodes
I think I am not very far from the expected result, but despite all my attempts, I fail.
If anyone can help me.
I hope being clear, otherwise I can give more information.
Looking at one of xsl:for-each statements, you are doing this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction/pott:Transaction[pott:InternalTransaction]">
You say you want to select MOTransaction elements, but it is actually selecting the child Transaction elements. To match the logic you require, it should be this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction[pott:InternalTransaction]]">
In fact, this should also work
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction/pott:InternalTransaction]">
Similarly, for the second statement (in the case of the parameter being "CP"), it could look like this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction[not(pott:InternalTransaction)]]">
Alternatively, it could look like this
<xsl:for-each select="pott:GetMOTransactionsResponse/pott:MOTransaction[not(pott:Transaction/pott:InternalTransaction)]">
They are not quite the same though, as the first will only include MOTransaction elements that have Transaction child elements, whereas the second will include MOTransaction that don't have any Transaction childs at all.
As a slight aside, you don't really need to use an xsl:for-each and xsl:call-template here. It might be better to use template matching.
Firstly, try changing the named template <xsl:template name="outputFormat"> to this
<xsl:template match="pott:MOTransaction">
Then, you can re-write you merge the xsl:for-each and xsl:call-template into a single xsl:apply-templates call.
<xsl:apply-template select="pott:GetMOTransactionsResponse/pott:MOTransaction[pott:Transaction/pott:InternalTransaction]" />
I am in need to transform the below coding using XSLT 1.0 based on the separators attributes given. The text should be separated based on the separators given:
Input:
<chapter xmlns="http://www.w3.org/1998/Math/MathML">
<math display="inline"><mfenced separators=", : . ;"><mn>1</mn><mtext>b</mtext><mo>%</mo><mi>d</mi><mi>e</mi></mfenced></math>
<math display="inline"><mfenced separators=", ;"><mi>a</mi><mi>b</mi><mi>c</mi><mi>d</mi><mi>e</mi></mfenced></math>
<math display="inline"><mfenced separators=", : . ; ; : . ;"><mi>a</mi><mi>b</mi><mi>c</mi><mi>d</mi><mi>e</mi></mfenced></math>
</chapter>
output required:
1,b:%.d;e
a,b;c;d;e
a,b:c.d;e
Also please note that if there are too many separator characters, the extra ones are ignored. If separator characters are given, but there are too few, the last one is repeated as necessary
I could not able to get the output only if the separator characters are lesser than the child elements.
XSLT 1.0 tried:
<?xml version='1.0'?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:m="http://www.w3.org/1998/Math/MathML">
<xsl:template match="m:mfenced">
<xsl:variable name="text" select="#separators"/>
<xsl:for-each select="child::*">
<xsl:apply-templates/>
<xsl:choose>
<xsl:when test="contains($text,' ')">
<xsl:variable name="attr" select="string-length(translate($text, ' ', ''))"/>
<xsl:variable name="ch" select="count(parent::*/child::*)-1"/>
<xsl:if test="$ch=$attr"><xsl:value-of select="substring($text,count(preceding-sibling::*)+position(),1)"/></xsl:if>
<xsl:if test="$ch gt $attr">
<xsl:if test="not(substring($text,count(preceding-sibling::*)+position(),1)='')"><xsl:value-of select="substring($text,count(preceding-sibling::*)+position(),1)"/></xsl:if>
<xsl:if test="(substring($text,count(preceding-sibling::*)+position(),1)='')"><xsl:value-of select="substring($text,count(preceding-sibling::*)+1,1)"/></xsl:if>
</xsl:if>
<xsl:if test="$ch lt $attr and count(following-sibling::*)>0"><xsl:value-of select="substring($text,count(preceding-sibling::*)+position(),1)"/></xsl:if>
</xsl:when>
<xsl:otherwise><xsl:if test="count(following-sibling::*)>0"><xsl:value-of select="$text"/></xsl:if></xsl:otherwise></xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
The following solution is based on obtaining the position of each <m:mi> within the <m:fenced> elements to obtain the next operator to be outputted.
Note. I am assuming that the string length used to represent each operator is 1.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:m="http://www.w3.org/1998/Math/MathML">
<xsl:output method="text" />
<!-- Ignore all text nodes (just for demo) -->
<xsl:template match="text()" />
<xsl:template match="m:mfenced">
<!-- Print children values and operators -->
<xsl:apply-templates select="*" mode="list-op">
<xsl:with-param name="separator-str" select="#separators" />
<xsl:with-param name="separator-len" select="string-length(#separators)" />
</xsl:apply-templates>
<!-- Print new line -->
<xsl:text>
</xsl:text>
</xsl:template>
<!-- Last m:mi elements for each m:mfenced are just printed -->
<xsl:template match="*[last()]" mode="list-op">
<xsl:value-of select="."/>
</xsl:template>
<!-- In this template we use the position() function to calculate the next
operator that is going to be outputted -->
<xsl:template match="*" mode="list-op">
<xsl:param name="separator-str" />
<!-- This parameter is not required, but allows us to cache
the length of the separators string instead of calculating it
for each m:mi element -->
<xsl:param name="separator-len" />
<!-- Print current value -->
<xsl:value-of select="." />
<!-- Calculate the separator position within the string -->
<xsl:variable name="string-position" select="2*position() - 1" />
<!-- Check if the position oveflows the position in the array, and
if it does, print the last separator in the string. -->
<xsl:choose>
<xsl:when test="$separator-len >= $string-position">
<xsl:value-of select="substring($separator-str, $string-position, 1)" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="substring($separator-str, $separator-len)" />
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
I'm trying to think functional, in XSLT terms, as much as possible, but in this case, I really don't see how to do it without tweaking. I have roughly this data structure:
<transactions>
<trx>
<text>abc</text>
<text>def</text>
<detail>
<text>xxx</text>
<text>yyy</text>
<text>zzz</text>
</detail>
</trx>
</transactions>
Which I roughly want to flatten into this form
<row>abc</row>
<row>def</row>
<row>xxx</row>
<row>yyy</row>
<row>zzz</row>
But the tricky thing is: I want to create chunks of 40 text-rows and transactions mustn't be split across chunks. I.e. if my current chunk already has 38 rows, the above transaction would have to go into the next chunk. The current chunk would need to be filled with two empty rows to complete the 40:
<row/>
<row/>
In imperative/procedural programming, it's very easy. Just create a global iterator variable counting to multiples of 40, and insert empty rows if needed (I have provided an answer showing how to tweak XSLT/Xalan to allow for such variables). But how to do it with XSLT? N.B: I'm afraid recursion is not possible considering the size of data I'm processing... But maybe I'm wrong on that
I. Here is an XSLT 1.0 solution (the XSLT 2.0 solution is much easier):
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:ext="http://exslt.org/common" exclude-result-prefixes="ext">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pChunkSize" select="8"/>
<xsl:param name="vChunkSize" select="$pChunkSize+1"/>
<xsl:variable name="vSheet" select="document('')"/>
<xsl:variable name="vrtfEmptyChunk">
<xsl:for-each select=
"($vSheet//node())[not(position() > $pChunkSize)]">
<row/>
</xsl:for-each>
</xsl:variable>
<xsl:variable name="vEmptyChunk" select=
"ext:node-set($vrtfEmptyChunk)/*"/>
<xsl:variable name="vrtfDummy">
<delete/>
</xsl:variable>
<xsl:variable name="vDummy" select="ext:node-set($vrtfDummy)/*"/>
<xsl:template match="/*">
<chunks>
<xsl:call-template name="fillChunks">
<xsl:with-param name="pNodes" select="trx"/>
<xsl:with-param name="pCurChunk" select="$vDummy"/>
</xsl:call-template>
</chunks>
</xsl:template>
<xsl:template name="fillChunks">
<xsl:param name="pNodes"/>
<xsl:param name="pCurChunk"/>
<xsl:choose>
<xsl:when test="not($pNodes)">
<chunk>
<xsl:apply-templates mode="rename" select="$pCurChunk[self::text]"/>
<xsl:copy-of select=
"$vEmptyChunk[not(position() > $vChunkSize - count($pCurChunk))]"/>
</chunk>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="vAvailable" select=
"$vChunkSize - count($pCurChunk)"/>
<xsl:variable name="vcurNode" select="$pNodes[1]"/>
<xsl:variable name="vTrans" select="$vcurNode//text"/>
<xsl:variable name="vNumNewNodes" select="count($vTrans)"/>
<xsl:choose>
<xsl:when test="not($vNumNewNodes > $vAvailable)">
<xsl:variable name="vNewChunk"
select="$pCurChunk | $vTrans"/>
<xsl:call-template name="fillChunks">
<xsl:with-param name="pNodes" select="$pNodes[position() > 1]"/>
<xsl:with-param name="pCurChunk" select="$vNewChunk"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<chunk>
<xsl:apply-templates mode="rename" select="$pCurChunk[self::text]"/>
<xsl:copy-of select=
"$vEmptyChunk[not(position() > $vAvailable)]"/>
</chunk>
<xsl:call-template name="fillChunks">
<xsl:with-param name="pNodes" select="$pNodes"/>
<xsl:with-param name="pCurChunk" select="$vDummy"/>
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="text" mode="rename">
<row>
<xsl:value-of select="."/>
</row>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the following XML document (based on the provided one, but with three trxelements):
<transactions>
<trx>
<text>abc</text>
<text>def</text>
<detail>
<text>xxx</text>
<text>yyy</text>
<text>zzz</text>
</detail>
</trx>
<trx>
<text>abc2</text>
<text>def2</text>
</trx>
<trx>
<text>abc3</text>
<text>def3</text>
<detail>
<text>xxx3</text>
<text>yyy3</text>
<text>zzz3</text>
</detail>
</trx>
</transactions>
the wanted, correct result (two chunks with size 8) is produced:
<chunks>
<chunk>
<row>abc</row>
<row>def</row>
<row>xxx</row>
<row>yyy</row>
<row>zzz</row>
<row>abc2</row>
<row>def2</row>
<row/>
</chunk>
<chunk>
<row>abc3</row>
<row>def3</row>
<row>xxx3</row>
<row>yyy3</row>
<row>zzz3</row>
<row/>
<row/>
<row/>
</chunk>
</chunks>
Do note:
The first two transactions' text elements total number is 7 and they fit in one 8-place chunk.
The third transaction has 5 text elements and doesn't fit in the remaining space of the first chunk -- it is put in a new chunk.
II. XSLT 2.0 Solution (using FXSL)
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:f="http://fxsl.sf.net/"
xmlns:dvc-foldl-func="dvc-foldl-func"
exclude-result-prefixes="f dvc-foldl-func"
>
<xsl:import href="../f/func-dvc-foldl.xsl"/>
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:param name="pChunkSize" select="8"/>
<dvc-foldl-func:dvc-foldl-func/>
<xsl:variable name="vPadding">
<row/>
</xsl:variable>
<xsl:variable name="vFoldlFun" select="document('')/*/dvc-foldl-func:*[1]"/>
<xsl:template match="/">
<xsl:variable name="vpaddingChunk" select=
"for $i in 1 to $pChunkSize
return ' '
"/>
<xsl:variable name="vfoldlResult" select=
"f:foldl($vFoldlFun, (), /*/trx),
$vpaddingChunk
"/>
<xsl:variable name="vresultCount"
select="count($vfoldlResult)"/>
<xsl:variable name="vFinalResult"
select="subsequence($vfoldlResult, 1,
$vresultCount - $vresultCount mod $pChunkSize
)"/>
<result>
<xsl:for-each select="$vFinalResult">
<row>
<xsl:value-of select="."/>
</row>
</xsl:for-each>
<xsl:text>
</xsl:text>
</result>
</xsl:template>
<xsl:template match="dvc-foldl-func:*" mode="f:FXSL">
<xsl:param name="arg1"/>
<xsl:param name="arg2"/>
<xsl:variable name="vCurCount" select="count($arg1)"/>
<xsl:variable name="vNewCount" select="count($arg2//text)"/>
<xsl:variable name="vAvailable" select=
"$pChunkSize - $vCurCount mod $pChunkSize"/>
<xsl:choose>
<xsl:when test="$vNewCount le $vAvailable">
<xsl:sequence select="$arg1, $arg2//text"/>
</xsl:when>
<xsl:otherwise>
<xsl:sequence select="$arg1"/>
<xsl:for-each select="1 to $vAvailable">
<xsl:sequence select="$vPadding/*"/>
</xsl:for-each>
<xsl:sequence select="$arg2//text"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
When this transformation is applied on the same XML document (above), the same correct, wanted result is produced:
<result>
<row>abc</row>
<row>def</row>
<row>xxx</row>
<row>yyy</row>
<row>zzz</row>
<row>abc2</row>
<row>def2</row>
<row/>
<row>abc3</row>
<row>def3</row>
<row>xxx3</row>
<row>yyy3</row>
<row>zzz3</row>
<row> </row>
<row> </row>
<row> </row>
</result>
Do note:
The use of the f:foldl() function.
A special DVC (Divide and Conquer) variant of f:foldl() so that recursion stack overflow is avoided for all practical purposes -- for example, the maximum recursion stack depth for 1000000 (1M) trx elements is just 19.
Build the complete XML data structure as you need in Java. Then, do the simple iteration in XSL over prepared XML.
You might save a lot of effort and provide a maintainable solution.
As promised a simplified example answer showing how Xalan can be tweaked to allow for incrementing such global iterators:
<xsl:stylesheet version="1.0" xmlns:f="xalan://com.example.Functions">
<!-- the global row counter variable -->
<xsl:variable name="row" select="0"/>
<xsl:template match="trx">
<!-- wherever needed, the $row variable can be globally incremented -->
<xsl:variable name="iteration" value="f:increment('row')"/>
<!-- based upon this variable, calculations can be made -->
<xsl:variable name="remaining-rows-in-chunk"
value="40 - (($iteration - 1) mod 40) "/>
<xsl:if test="count(.//text) > $remaining-rows-in-chunk">
<xsl:call-template name="empty-row">
<xsl:with-param name="rows" select="$remaining-rows-in-chunk"/>
</xsl:call-template>
</xsl:if>
<!-- process transaction now, that previous chunk has been filled [...] -->
</xsl:template>
<xsl:template name="empty-row">
<xsl:param name="rows"/>
<xsl:if test="$rows > 0">
<row/>
<xsl:variable name="dummy" select="f:increment('row')"/>
<xsl:call-template name="empty-row">
<xsl:with-param name="rows" select="$rows - 1"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
And the contents of com.example.Functions:
public class Functions {
public static String increment(ExpressionContext context, String nodeName) {
XNumber n = null;
try {
// Access the $row variable
n = ((XNumber) context.getVariableOrParam(new QName(nodeName)));
// Make it "mutable" using this tweak. I feel horrible about
// doing this, though ;-)
Field m_val = XNumber.class.getDeclaredField("m_val");
m_val.setAccessible(true);
// Increment it
m_val.setDouble(n, m_val.getDouble(n) + 1.0);
} catch (Exception e) {
log.error("Error", e);
}
return n == null ? null : n.str();
}
}
I have a template with a parameter. How can I insert a tab character n times?
n is the value of the parameter.
In XSLT 2.0:
<xsl:for-each select="1 to $count"> </xsl:for-each>
(Sadly though, I suspect that if you were using XSLT 2.0 you wouldn't need to ask the question).
Another technique often used with XSLT 1.0 is the hack:
<xsl:for-each select="//*[position() <= $count]"> </xsl:for-each>
which works provided the number of elements in your source document is greater than the number of tab characters you want to output.
Just call it recursively; output a tab, then call the same template again with n-1 passed in, if n > 1.
<xsl:template name="repeat">
<xsl:param name="output" />
<xsl:param name="count" />
<xsl:if test="$count > 0">
<xsl:value-of select="$output" />
<xsl:call-template name="repeat">
<xsl:with-param name="output" select="$output" />
<xsl:with-param name="count" select="$count - 1" />
</xsl:call-template>
</xsl:if>
</xsl:template>
As has been pointed out, this example will actually output a minimum of one. In my experience where the output is whitespace, it's usually needed. You can adapt the principle of a recursive template like this any way you see fit.
This seems the simplest and most flexible to me.
For XSLT 1.0 (or perhaps 1.1).
<xsl:variable name="count">10</xsl:variable>
<xsl:variable name="repeat"><xsl:text> </xsl:text></xsl:variable>
<xsl:sequence select="string-join((for $i in 1 to $count return $repeat),'')"/>
Of course the count variable is where you assign your n parameter.
I used the variable repeat to hold the tab character, but you could just replace the $repeat with the tab character in single quotes in the sequence element. Note: This variable can be of a length greater than 1, which creates a whole bunch of possibilities.
It does not use recursion, so it won't run into a recursion limit.
I don't know the maximum value you can use for count, but I tested it up to 10,000.
Globally define a long enough array of tabs:
<xsl:variable name="TABS" select="' '" />
Then use like this:
<xsl:value-of select="fn:substring($TABS, 1, fn:number($COUNT))" />
(XSLT 1.0)
<xsl:template name="tabs">
<xsl:param name="n"/>
<xsl:if test="$n > 0"> <!-- When n = 0, output nothing. -->
<xsl:call-template name="tabs"> <!-- Recursive call: call same template... -->
<xsl:with-param name="n" select="$n - 1"/> <!-- ... for writing n - 1 tabs. -->
</xsl:call-template>
<xsl:text> </xsl:text> <!-- Add one tab character. -->
</xsl:if>
</xsl:template>
Example usage:
<xsl:call-template name="tabs">
<xsl:with-param name="n" select="3"/>
</xsl:call-template>
I've discovered an LGPL-licensed library for doing this called functx, as I was sure someone had to have already done this... This is a "standard library" type XSLT library, which contains a function called repeat-string. From the docs:
The functx:repeat-string function returns a string consisting of a given number of copies of $stringToRepeat concatenated together.
Where I use it like this in my code:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="2.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:functx="http://www.functx.com">
<xsl:import href="../buildlib/functx-1.0.xsl"/>
<xsl:output omit-xml-declaration="yes" />
<xsl:variable name="INDENT" select="' '" />
....
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*" />
</xsl:copy>
</xsl:template>
<xsl:template match="data-pusher-properties">
<xsl:for-each select="property">
<xsl:choose>
...
<xsl:when test="boolean(#value = '${pusher.notifications.server}')">
<xsl:value-of select="functx:repeat-string($INDENT, #indent)" />
<xsl:text>"</xsl:text>
<xsl:value-of select="#name" />
<xsl:text>": </xsl:text>
<xsl:text>"</xsl:text>
<xsl:value-of select="$pusher.notifications.email.server" />
<xsl:text>"\
</xsl:text>
</xsl:when>
...
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
So for printing a tab character n times, call it like this:
<xsl:value-of select="functx:repeat-string(' ', n)" />
I know this question is old, but I hope this can still help someone.
Documentation for the repeat-string function