How to get the grand total in an aggregate sum in XSLT? - xslt

First, I want to thank you for your time to help. I'm new to XSLT and am stuck for days trying to get the grand total of quantity. I am able to get the aggregate sum for each report_id by type and discard the negative aggregate sum (see report_id 222 and 444).
<?xml version="1.0" encoding="UTF-8"?>
<root>
<report>
<report_id>111</report_id>
<group>
<type>A</type>
<quantity>2</quantity>
</group>
<group>
<type>B</type>
<quantity>4</quantity>
</group>
<group>
<type>A</type>
<quantity>6</quantity>
</group>
</report>
<report>
<report_id>222</report_id>
<group>
<type>A</type>
<quantity>-5</quantity>
</group>
<group>
<type>A</type>
<quantity>2</quantity>
</group>
</report>
<report>
<report_id>333</report_id>
<group>
<type>B</type>
<quantity>7</quantity>
</group>
</report>
<report>
<report_id>444</report_id>
<group>
<type>B</type>
<quantity>-12</quantity>
</group>
</report>
</root>
My xsl output is below, and the problem lies at the last value 0.
111 A=8; 111 B=4; 333 B=7; 0
The output should be this
111 A=8; 111 B=4; 333 B=7; 19
I tried call-template to do the $grand_qty and failed. I prefer XSLT 2.0 but version is find too. Thanks in advance.
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet exclude-result-prefixes="xsl"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:output method="text" />
<xsl:template match="/root">
<xsl:variable name="total_qty" select="0"/>
<xsl:variable name="grand_qty" select="0"/>
<xsl:for-each-group select="report/group" group-by="concat(../report_id, '|', type)">
<xsl:variable name="qty" select="sum(current-group()/quantity)"/>
<xsl:if test="$qty > 0">
<xsl:variable name="total_qty" select="$qty + $total_qty"/>
<xsl:value-of select="concat(../report_id, ' ', type, '=', $total_qty, '; ')"/>
<xsl:variable name="grand_qty" select="$grand_qty + $total_qty"/>
</xsl:if>
</xsl:for-each-group>
<xsl:value-of select="$grand_qty"/>
</xsl:template>
</xsl:stylesheet>

Variables in XSLT are immutable, and limited in scope to their parent element (or more precisely, to all following siblings and their descendants).
Try a different approach:
XSLT 2.0
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/root">
<xsl:variable name="groups">
<xsl:for-each-group select="report/group" group-by="concat(../report_id, ' ', type)">
<xsl:variable name="qty" select="sum(current-group()/quantity)"/>
<xsl:if test="$qty gt 0">
<group name="{current-grouping-key()}">
<xsl:value-of select="$qty"/>
</group>
</xsl:if>
</xsl:for-each-group>
</xsl:variable>
<xsl:value-of select="$groups/group/concat(#name, '=', ., '; ')" separator=""/>
<xsl:value-of select="sum($groups/group)"/>
</xsl:template>
</xsl:stylesheet>

As your code uses version="3.0" I would guess XSLT 3 also works; there you have some ways to store values with maps or arrays, xsl:iterate or fold-left; together with grouping it is possible but a bit convoluted to keep a total parameter as follows:
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
xmlns:mf="http://example.com/mf"
expand-text="yes">
<xsl:function name="mf:group" as="map(*)*">
<xsl:param name="group-elements" as="element(group)*"/>
<xsl:for-each-group select="$group-elements" composite="yes" group-by="../report_id, type">
<xsl:variable name="qty" select="sum(current-group()/quantity)"/>
<xsl:sequence select="map { 'qty' : $qty, 'value' : current-grouping-key()[1] || ' ' || current-grouping-key()[2] || ' = ' || $qty }[$qty gt 0]"/>
</xsl:for-each-group>
</xsl:function>
<xsl:output method="text" item-separator="; "/>
<xsl:template match="/" name="xsl:initial-template">
<xsl:iterate select="root/mf:group(report/group)">
<xsl:param name="total" select="0"/>
<xsl:on-completion>
<xsl:sequence select="$total"/>
</xsl:on-completion>
<xsl:sequence select="?value"/>
<xsl:next-iteration>
<xsl:with-param name="total" select="?qty + $total"/>
</xsl:next-iteration>
</xsl:iterate>
</xsl:template>
</xsl:stylesheet>
Alternative with fold-left instead of xsl:iterate:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="3.0"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
xmlns:mf="http://example.com/mf"
expand-text="yes">
<xsl:function name="mf:group" as="map(*)*">
<xsl:param name="group-elements" as="element(group)*"/>
<xsl:for-each-group select="$group-elements" composite="yes" group-by="../report_id, type">
<xsl:variable name="qty" select="sum(current-group()/quantity)"/>
<xsl:sequence select="map { 'qty' : $qty, 'value' : current-grouping-key()[1] || ' ' || current-grouping-key()[2] || ' = ' || $qty }[$qty gt 0]"/>
</xsl:for-each-group>
</xsl:function>
<xsl:output method="text" item-separator="; "/>
<xsl:template match="/" name="xsl:initial-template">
<xsl:sequence
select="fold-left(
root!mf:group(report!group),
0,
function($a, $g) { $a[position() lt last()], $g?value, $a[last()] + $g?qty }
)"/>
</xsl:template>
</xsl:stylesheet>

Related

How to partition dates with XSLT

I have a group of dates and I'd like to create partitions with a criterion such as "exactly 7 days apart" For example this is my source xml:
<root>
<entry date="2019-05-12" />
<entry date="2019-05-19" />
<entry date="2019-05-26" />
<entry date="2019-06-16" />
<entry date="2019-06-23" />
</root>
The result should be like this:
<root>
<group>
<val>12.5.</val>
<val>19.5.</val>
<val>26.5.</val>
</group>
<group>
<val>16.6.</val>
<val>23.6.</val>
</group>
</root>
since the first three and the last two dates are all on a Sunday without a gap.
What I have so far is this:
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:sd="urn:someprefix"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
>
<xsl:output indent="yes"/>
<xsl:template match="root">
<root>
<xsl:copy-of select="sd:partition(distinct-values(for $i in entry/#date return $i cast as xs:date))"/>
</root>
</xsl:template>
<xsl:function name="sd:partition">
<xsl:param name="dates" as="xs:date*"/>
<xsl:for-each-group select="$dates" group-adjacent="format-date(., '[F]')">
<group>
<xsl:for-each select="current-group()">
<val>
<xsl:value-of select="format-date(.,'[D].[M].')"/>
</val>
</xsl:for-each>
</group>
</xsl:for-each-group>
</xsl:function>
</xsl:stylesheet>
Which only generates one group.
How can I ask for the previous element to be 7 days apart? I know of duration (xs:dayTimeDuration('P1D')), but I don't know how to compare it to a previous value.
I use Saxon 9.8 HE.
I think you can also do it using group-adjacent:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
exclude-result-prefixes="#all"
expand-text="yes"
version="3.0">
<xsl:output method="xml" indent="yes"/>
<xsl:template match="root">
<xsl:copy>
<xsl:for-each-group select="entry/#date/xs:date(.)"
group-adjacent=". - (position() - 1) * xs:dayTimeDuration('P7D')">
<group>
<xsl:apply-templates select="current-group()"/>
</group>
</xsl:for-each-group>
</xsl:copy>
</xsl:template>
<xsl:template match=".[. instance of xs:date]">
<val>{format-date(.,'[D].[M].')}</val>
</xsl:template>
</xsl:stylesheet>
https://xsltfiddle.liberty-development.net/ncdD7mM
To do your grouping, you really need to know the difference in days with the previous element, then you can group starting with dates where the difference is not 7 days. So, you can declare a variable where you build up some new XML with the dates and differences, and then use that to group.
Try this function in your XSLT instead.
<xsl:function name="sd:partition">
<xsl:param name="dates" as="xs:date*"/>
<xsl:variable name="datesWithDiff" as="element()*">
<xsl:for-each select="$dates">
<xsl:variable name="pos" select="position()" />
<date diff="{(. - $dates[$pos - 1]) div xs:dayTimeDuration('P1D')}">
<xsl:value-of select="." />
</date>
</xsl:for-each>
</xsl:variable>
<xsl:for-each-group select="$datesWithDiff" group-starting-with="date[#diff = '' or xs:int(#diff) gt 7]">
<group>
<xsl:for-each select="current-group()">
<val>
<xsl:value-of select="format-date(.,'[D].[M].')"/>
</val>
</xsl:for-each>
</group>
</xsl:for-each-group>
</xsl:function>

Group by consecutive dates and sum by similar dates in xslt

Gurus
I am try to print data rows by consecutive dates and taking a sum of all the time off units in each row into by one summarized row with start date being the first day of the time off and end date being the last time off in the consecutive group. I used below xslt and it worked excellently however there could be case when a time off is cancelled in the system then for the same date I can get a negative value for the unit in which case my xslt below is not working as I am unable to handle similar dates and similar type scenarios. Any advise please.
XML
<?xml version='1.0' encoding='UTF-8'?>
<Data>
<Worker>
<Worker_ID>12</Worker_ID>
<Time_Off>
<Type>Compassionate Leave</Type>
<Date>2018-02-09-08:00</Date>
<Units>1</Units>
</Time_Off>
<Time_Off>
<Type>Compassionate Leave</Type>
<Date>2018-02-08-08:00</Date>
<Units>1</Units>
</Time_Off>
<Time_Off>
<Type>Compassionate Leave</Type>
<Date>2018-02-08-08:00</Date>
<Units>-1</Units>
</Time_Off>
<Time_Off>
<Type>Compassionate Leave</Type>
<Date>2018-02-01-08:00</Date>
<Units>1</Units>
</Time_Off>
<Time_Off>
<Type>Statutory Holiday</Type>
<Date>2018-02-07-08:00</Date>
<Units>1</Units>
</Time_Off>
<Time_Off>
<Type>Statutory Holiday</Type>
<Date>2018-02-06-08:00</Date>
<Units>1</Units>
</Time_Off>
</Worker>
</Data>
Xslt:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf"
version="3.0">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:function name="mf:date" as="xs:date">
<xsl:param name="input" as="xs:string"/>
<xsl:sequence select="xs:date(substring($input, 1, 10))"/>
</xsl:function>
<xsl:function name="mf:line" as="xs:string">
<xsl:param name="group" as="element(Time_Off)*"/>
<xsl:value-of
select="$group[1]/../Worker_ID,
$group[1]/Type,
mf:date($group[1]/Date),
mf:date($group[last()]/Date),
sum($group/Units)"
separator=","/>
</xsl:function>
<xsl:template match="Worker">
<xsl:for-each-group select="Time_Off" group-by="Type">
<xsl:variable name="sorted-times" as="element(Time_Off)*">
<xsl:perform-sort select="current-group()">
<xsl:sort select="mf:date(Date)"/>
</xsl:perform-sort>
</xsl:variable>
<xsl:for-each-group select="$sorted-times" group-by="mf:date(Date) - xs:dayTimeDuration('P1D') * position()">
<xsl:value-of select="mf:line(current-group()) || '
'"/>
</xsl:for-each-group>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
The output I get is below:
12,Compassionate Leave,2018-02-01,2018-02-01,1
12,Compassionate Leave,2018-02-08,2018-02-08,1
12,Compassionate Leave,2018-02-08,2018-02-09,0
12,Statutory Holiday,2018-02-06,2018-02-07,2
My desired output is below:
12,Compassionate Leave,2018-02-01,2018-02-01,1
12,Compassionate Leave,2018-02-08,2018-02-09,1
12,Statutory Holiday,2018-02-06,2018-02-07,2
You have posted an XSLT 3 stylesheet so I guess although you tagged the question as XSLT 2 it is fine to use XSLT 3, therefore here is a suggestion using xsl:iterate:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf"
version="3.0">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:function name="mf:date" as="xs:date">
<xsl:param name="input" as="xs:string"/>
<xsl:sequence select="xs:date(substring($input, 1, 10))"/>
</xsl:function>
<xsl:function name="mf:line" as="xs:string">
<xsl:param name="group" as="element(Time_Off)*"/>
<xsl:value-of
select="$group[1]/../Worker_ID,
$group[1]/Type,
mf:date($group[1]/Date),
mf:date($group[last()]/Date),
sum($group/Units)"
separator=","/>
</xsl:function>
<xsl:template match="Worker">
<xsl:for-each-group select="Time_Off" group-by="Type">
<xsl:variable name="sorted-times" as="element(Time_Off)*">
<xsl:perform-sort select="current-group()">
<xsl:sort select="mf:date(Date)"/>
</xsl:perform-sort>
</xsl:variable>
<xsl:iterate select="$sorted-times">
<xsl:param name="group" as="element(Time_Off)*" select="()"/>
<xsl:on-completion>
<xsl:value-of select="mf:line($group), ''" separator="
"/>
</xsl:on-completion>
<xsl:variable name="new-group" as="xs:boolean"
select="$group
and mf:date(Date) - mf:date($group[last()]/Date) gt xs:dayTimeDuration('P1D')"/>
<xsl:if test="$new-group">
<xsl:value-of select="mf:line($group), ''" separator="
"/>
</xsl:if>
<xsl:next-iteration>
<xsl:with-param name="group" select="if ($new-group) then . else ($group, .)"/>
</xsl:next-iteration>
</xsl:iterate>
</xsl:for-each-group>
</xsl:template>
</xsl:stylesheet>
Online at https://xsltfiddle.liberty-development.net/pPgCcov/2.

XSLT - extract all text but last subsection

Looking to parse out a namespace from a full class name in xml.
Data example:
<results>
<test-case name="Co.Module.Class.X">
</results>
End result (going to csv format):
,Co.Module.Class
Stylesheet:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxsl="urn:schemas-microsoft-com:xslt">
<xsl:output method="text" indent="yes" encoding="ISO-8859-1"/>
<xsl:param name="delim" select="','" />
<xsl:param name="quote" select="'"'" />
<xsl:param name="break" select="'
'" />
<xsl:template match="/">
FullTestName, Namespace
<xsl:apply-templates select="//test-case" />
</xsl:template>
<xsl:template match="test-case">
<xsl:apply-templates />
<xsl:value-of select="#name" />
<xsl:value-of select="$delim" />
<xsl:value-of select="function to go here for nameWithJustNamespace" />
<xsl:value-of select="$break" />
</xsl:template>
I understand the process would need a last index of "." to be called once, yet I'm not finding XSLT to have that function. How to best accomplish this?
To do this in pure XSLT 1.0, you need to call a named recursive template, e.g.:
XSLT 1.0
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text" encoding="UTF-8"/>
<xsl:template match="/results">
<xsl:call-template name="remove-last-token">
<xsl:with-param name="text" select="test-case/#name"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="remove-last-token">
<xsl:param name="text"/>
<xsl:param name="delimiter" select="'.'"/>
<xsl:value-of select="substring-before($text, $delimiter)"/>
<xsl:if test="contains(substring-after($text, $delimiter), $delimiter)">
<xsl:value-of select="$delimiter"/>
<xsl:call-template name="remove-last-token">
<xsl:with-param name="text" select="substring-after($text, $delimiter)"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
This pure XSLT 1.0 transformation (shorter, no conditional XSLT operations, single template):
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="test-case[contains(#name, '.')]">
<xsl:param name="pDotIndex" select="0"/>
<xsl:variable name="vNextToken"
select="substring-before(substring(#name, $pDotIndex+1), '.')"/>
<xsl:value-of select="concat(substring('.', 2 - ($pDotIndex > 0)),$vNextToken)"/>
<xsl:variable name="vNewDotIndex" select="$pDotIndex+string-length($vNextToken)+1"/>
<xsl:apply-templates
select="self::node()[contains(substring(#name,$vNewDotIndex+1), '.')]">
<xsl:with-param name="pDotIndex" select="$vNewDotIndex"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<results>
<test-case name="Co.Module.Class.X"/>
</results>
produces the wanted, correct result:
Co.Module.Class
Part 2
With a slight modification the following transformation produces the complete CSV:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<xsl:template match="test-case[contains(#name, '.')]">
<xsl:param name="pDotIndex" select="0"/>
<xsl:variable name="vNextToken"
select="substring-before(substring(#name, $pDotIndex+1), '.')"/>
<xsl:value-of select="concat(substring(',', 2 - (position() > 1)),
substring('.', 2 - ($pDotIndex > 0)), $vNextToken)"/>
<xsl:variable name="vNewDotIndex" select="$pDotIndex+string-length($vNextToken)+1"/>
<xsl:apply-templates
select="self::node()[contains(substring(#name,$vNewDotIndex+1), '.')]">
<xsl:with-param name="pDotIndex" select="$vNewDotIndex"/>
</xsl:apply-templates>
</xsl:template>
</xsl:stylesheet>
When applied on this XML document:
<results>
<test-case name="Co.Module.Class.X"/>
<test-case name="Co2.Module2.Class2.Y"/>
<test-case name="Co3.Module3.Class3.Z"/>
</results>
the wanted, correct (CSV) result is produced:
Co.Module.Class,Co2.Module2.Class2,Co3.Module3.Class3

Processing a list in XSLT

I have list of elements in variable
|ELEMENT1|ELEMENT2|ELEMENT3|ELEMENT4|ELEMENT5|
If any of request elements matches this , I should display local name and its value.
Request XML :
<Root>
<element1>Test1</element1>
<child>
<element2>222</element2>
</child>
<secondChild>
<element2>234</element2>
</secondChild>
<thirdchild>
<element3>5w2</element3>
</thirdchild>
</Root>
XSL:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
version="1.0">
<xsl:output method="text"/>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'"/>
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'"></xsl:variable>
<xsl:variable name="list"><xsl:value-of select="'|ELEMENT1|ELEMENT2|ELEMENT3|ELEMENT4|ELEMENT5|'"/></xsl:variable>
<xsl:template match="/">
<xsl:for-each select="//*[contains(translate($list,$lower,$upper),concat('|',translate(local-name(),$lower,$upper),'|'))]">
<xsl:value-of select="concat(local-name(),':',.,'|')"></xsl:value-of>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Expected Output :
element1:Test1|element2:222|element3:5w2|
But I am getting
element1:Test1|element2:222|element2:234|element3:5w2|
This is because I have element2 in two places in XML. I should not read second element2 while processing.
Can you please help on this
Filter out any elements that have a preceding element of the same name.
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="text"/>
<xsl:variable name="lower" select="'abcdefghijklmnopqrstuvwxyz'" />
<xsl:variable name="upper" select="'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" />
<xsl:variable name="list" select="'|ELEMENT1|ELEMENT2|ELEMENT3|ELEMENT4|ELEMENT5|'" />
<xsl:template match="/">
<xsl:for-each select="//*[
contains(
concat('|', translate($list, $lower, $upper), '|'),
concat('|', translate(local-name(), $lower, $upper), '|')
)
]">
<xsl:if test="not(preceding::*[local-name() = local-name(current())])">
<xsl:value-of select="concat(local-name(), ':', ., '|')" />
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
You can define a key
<xsl:key name="name" match="*" use="local-name()"/>
and then in your condition you check
<xsl:for-each select="//*[generate-id() = generate-id(key('name', local-name())[1])][contains(translate($list,$lower,$upper),concat('|',translate(local-name(),$lower,$upper),'|'))]">

how to insert xml nodet at a defined points

xslt have function(like substring vice versa) or how to solve it? I had xml:
<document>
<Line>
<Line-Item>
<LineNumber>10</LineNumber>
<EAN>111</EAN>
<BIC>123123</BIC>
<SIC>AVD091</SIC>
</Line-Item>
</Line>
<Line>
<Line-Item>
<LineNumber>20</LineNumber>
<EAN>22222</EAN>
<BIC>3232332</BIC>
<SIC>AVD25482</SIC>
</Line-Item>
</Line>
</document>
needed output:
10 111 123123 AVD091
20 22222 3232332 AVD25482
Field line number start from 1 column position, EAN start from 11 column position, BIC start from 19 and SIC from 31.
Try this XSLT 1.0 style-sheet. The pad template is an XSLT 1.0 version of Martin's mf:pad function.
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="text"/>
<xsl:template match="/">
<xsl:apply-templates select="document/Line/Line-Item"/>
</xsl:template>
<xsl:template name="pad">
<xsl:param name="value" />
<xsl:param name="width" />
<xsl:variable name="col-max" select="' '"/>
<xsl:value-of select="substring( concat($value,$col-max), 1, $width)" />
</xsl:template>
<xsl:template match="Line-Item" >
<xsl:call-template name="pad" >
<xsl:with-param name="value" select="LineNumber"/>
<xsl:with-param name="width" select="10" />
</xsl:call-template>
<xsl:call-template name="pad" >
<xsl:with-param name="value" select="EAN"/>
<xsl:with-param name="width" select="8" />
</xsl:call-template>
<xsl:call-template name="pad" >
<xsl:with-param name="value" select="BIC"/>
<xsl:with-param name="width" select="12" />
</xsl:call-template>
<xsl:value-of select="SIC" />
<xsl:value-of select="'
'" />
</xsl:template>
<xsl:template match="*" />
</xsl:stylesheet>
This short and generic transformation:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output method="text"/>
<xsl:strip-space elements="*"/>
<my:fields>
<fieldset name="LineNumber" width="10"/>
<fieldset name="EAN" width="8"/>
<fieldset name="BIC" width="12"/>
</my:fields>
<xsl:variable name="vSpaces" select="' '"/>
<xsl:variable name="vFields" select="document('')/*/my:fields/*"/>
<xsl:template match="Line-Item">
<xsl:text>
</xsl:text>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="Line-Item/*">
<xsl:value-of select=
"concat(.,
substring($vSpaces,
1,
$vFields[#name = name(current())]/#width
-
string-length()
)
)"/>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<document>
<Line>
<Line-Item>
<LineNumber>10</LineNumber>
<EAN>111</EAN>
<BIC>123123</BIC>
<SIC>AVD091</SIC>
</Line-Item>
</Line>
<Line>
<Line-Item>
<LineNumber>20</LineNumber>
<EAN>22222</EAN>
<BIC>3232332</BIC>
<SIC>AVD25482</SIC>
</Line-Item>
</Line>
</document>
produces the wanted, correct result:
10 111 123123 AVD091
20 22222 3232332 AVD25482
Do note:
The element my:fields can be put in its own XML document. Thus, no modifications would be required to the XSLT code if some fields widths need to be modified.
Here is a sample stylesheet (XSLT 2.0, sorry, started writing before your comment indicated a request for 1.0):
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:mf="http://example.com/mf"
exclude-result-prefixes="xs mf">
<xsl:param name="col-max" as="xs:string" select="' '"/>
<xsl:strip-space elements="*"/>
<xsl:output method="text"/>
<xsl:function name="mf:pad" as="xs:string">
<xsl:param name="input" as="xs:string"/>
<xsl:param name="col-length" as="xs:integer"/>
<xsl:sequence select="concat($input, substring($col-max, 1, $col-length - string-length($input)))"/>
</xsl:function>
<xsl:template match="Line">
<xsl:if test="position() > 1">
<xsl:text>
</xsl:text>
</xsl:if>
<xsl:apply-templates/>
</xsl:template>
<xsl:template match="LineNumber">
<xsl:sequence select="mf:pad(., 10)"/>
</xsl:template>
<xsl:template match="EAN">
<xsl:sequence select="mf:pad(., 9)"/>
</xsl:template>
<xsl:template match="BIC">
<xsl:sequence select="mf:pad(., 12)"/>
</xsl:template>
<xsl:template match="SIC">
<xsl:sequence select="mf:pad(., string-length())"/>
</xsl:template>
</xsl:stylesheet>