generate dynamic table rows using XQuery - xslt

I have a function which returns phone numbers. It returns in this format
<td>#phone</td>
The function may return any number of values.
I want to display them in a grid like structure.
i.e a table which has say 10 columns in it. (So if the function returns 26 records, then I will have 3 rows: two rows with 10 columns and third row with 6 columns.)
I am unable to understand the counter logic in for statement in XQuery (for statement with keyword at). Any help would be appreciated.
The function call is like this (any modification to the function call is also highly appreciated):
The actual code is this:
declare function local:table-construct(
$areacode as xs:string,
$uniquekey as $xs:string,
$doc as xs:element) as xs:element?
{
for $phno in $doc/users[$areacode eq $code and $uniquekey eq $thiskey]
return <td>{$phno/phone}</td>
}
let $doc := <an xml doc from a database>
let $areacode := "somestring"
let $uniquekey := "somekey"
return
<html>
<body>
<table>
<tr>{local:table-construct($areacode, $uniquekey, $doc)}</tr>
</table>
</body>
</html>
The present format gives me all the phone numbers in the same row. I want the table to show only 10 rows. and the remaining data in the next rows.

Because you didn't provide any code, I can only guess you trapped into the "functional programming" trap. XQuery is a functional language without variables as known in imperative languages, better think of them as constants.
let $x := 1 to 10
let $sum := 0
for $i in $x
let $sum := $sum+$i (: here we cover $sum from line 2, do not change it :)
return $sum
The output of this code is 1 2 3 4 5 6 7 8 9 10 which might be unexpected. In line 4, we always add $i (1 to 10) and $sum (0), but we do not update $sum but cover it. For the next $i, $sum will be 0 again.
If this is the problem, think about using some pattern like this:
let $seq := 1 to 15
let $dividor := 4
for $i in 1 to ceiling(count($seq) div $dividor) cast as xs:integer
return <tr>{
for $td in subsequence($seq, ($i -1)*$dividor + 1, $dividor)
return <td>{$td}</td>
}</tr>
You might have to fit it to your code, but the idea should be fine.
EDIT: If your query processor supported it, you also could use a sliding window. As of version 5.0.2, Marklogic still doesn't.

mabe this can help, u can do the grouping as per your request inside the xslt!
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:variable name="Columns" select="10"/>
<xsl:template match="/*">
<xsl:copy>
<xsl:apply-templates select="phone[position() mod $Columns= 1]"/>
</xsl:copy>
</xsl:template>
<xsl:template match="phone">
<tr>
<xsl:apply-templates mode="copy" select=". | following-sibling::phone[position() < $Columns]"/>
</tr>
</xsl:template>
<xsl:template match="line" mode="copy">
<td><xsl:copy-of select="."/><td>
</xsl:template>
</xsl:stylesheet>

The at keyword of the FLWOR statement won't help you much here. You need to take 10 columns at a time and put that in its own <tr>. The most straight-forward way of doing this in MarkLogic is as follows:
declare function local:wrap-columns($columns, $width) {
let $nrrows := ceiling(count($columns) div $width)
for $row in 1 to $nrrows
let $start := ($row - 1) * $width + 1
let $end := $row * $width
return
<tr>{$columns[$start to $end]}</tr>
};
let $columns :=
for $i in 1 to 26
return <td>{$i}</td>
return
local:wrap-columns($columns, 10)
On second glance, it resembles the solution by Ranon (coincidence, honestly), though wrapped in a function to make integration and reuse easier.
A solution in XSLT would work too, but if you are not already using XSLT, then using XQuery makes more sense here I guess.
HTH!

Related

Control start and stop position during a for-each

I am looking to find a way to control both start and end numbering that are added as suffix number to text "period". It seems the logic of my code makes for-each count 3 search matches (index 1-3), and due to using if to find se:Bank it reduces printout to 2 prints.
I know I can use position() -2 to force the numbering to start at 0, but that will only work if I have exact same amount of data. As soon as data growths the position() -2 will assume to step 2 positions minus and will not return zero as start.
I do understand that XSLT does what I actually ask for and returns presumable correct answer.
The interval of number I will be using is between 0 and 3. Most of the time in sequence.
It will be unknown in advance exact interval amounts.
I know there is a start-at using <xsl:number> but it did not solve my problem. Using start-at prints out same number twice.
The JSON file is aligned with a certain standard so I am not allowed to change the structure of the JSON file.
Suspected problem:
The "foreach" spans over more loops than wanted. In this case I need just for the system to loop twice for "se:Bank" and therefor return "period0" and "period1".
Observation:
I suspect that it probably would be better that I learn to extract when xbrl:concept = se:Bank. That would reduce the planned loop down to 2 search, thus being able iterate over them.
Here you find the xsltfiddle.
Below you find same code as in xsltfiddle:
JSON data:
<data>
{
"report": {
"facts": [
{
"xbrl:concept": "se:CompanyName",
"value": "Great Company Ltd"
},
{
"xbrl:concept": "se:Bank",
"numericValue": 1000
},
{
"xbrl:concept": "se:Bank",
"numericValue": 3000
}
]
}
}
</data>
XSL:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="3.0">
<xsl:output method="xhtml" indent="yes" html-version="5"/>
<xsl:mode on-no-match="shallow-skip"/>
<!-- Parse JSON to XML -->
<xsl:template match="data">
<xsl:apply-templates select="json-to-xml(.)/*"/>
</xsl:template>
<!-- Printout periods -->
<xsl:template match="//*[#key='facts']">
<xsl:for-each select="//*[#key='xbrl:concept']">
<xsl:if test=". = 'se:Bank'">
period<xsl:number value="position()"/>
</xsl:if>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
Result:
<?xml version="1.0" encoding="UTF-8"?>period2period3
Expected result:
<?xml version="1.0" encoding="UTF-8"?>period0period1
If you want to output position() or position() - 1 then <xsl:value-of select="position()"/> or <xsl:value-of select="position() -1 "/>, respectively, suffice, there is no need to feed position() to xsl:number.
Furthermore, I am not sure I understand your requirements correctly, but using a predicate <xsl:for-each select=".//*[#key='xbrl:concept'][. = 'se:Bank']"> instead of the of the nested xsl:if should help to get the result you want, namely to process the two elements in the input sample meeting the condition in the predicate: https://xsltfiddle.liberty-development.net/93wniUS/1

Unable to split and merge in XSLT

I have an Xml
<input Inputxml="&ltOrder..<LinePPlineNO="1#quot;Line/> >" >
How do i remove some part of string using xsl .. for eg I need to remove a whole string from < to > for a PPline 1
I need to splie the string in 3 parts remove the string from lt to gt and merge the part 1na dpart 3 of string
<Test Attrib1="b" Attrib2="C" Inputxml="
<OrderLine OrderedQty="1" PrimeLineNo="1" ShipNode="ABC" $gt;
</OrderLine $gt;
<OrderLine OrderedQty="1" PrimeLineNo="2" ShipNode="ABC" $gt;
</OrderLine $gt;" />
For example I may have 100 Order lines but I need to find the one with Prime line 1 and remove it .. So if I have to remeove a line I have to remove from lt; to gt;
Your example is confusing. If you have an XML input such as:
<input Inputxml="<order><Line PPlineNO="1">Bingo</Line></order>"/>
where the Inputxml attribute holds the escaped XML:
<order><Line PPlineNO="1">Bingo</Line></order>
you can use:
<xsl:template match="input">
<result>
<xsl:value-of select="substring-before(substring-after(#Inputxml, 'PPlineNO="1">'), '</Line>')" />
</result>
</xsl:template>
to get:
<result>Bingo</result>
Note that is not a good way to parse XML (or rather what used to be XML). It would be much smarter to unescape it first, then parse it as XML. In XSLT 3.0, you can use the parse-xml() function for this. In XSLT 1.0/2.0, you can do:
<xsl:value-of select="#Inputxml" disable-output-escaping="yes"/>
save the result to a file, and process the resulting file using another XSLT stylesheet.

Count the number of elements which match a certain criteria using XSLT

I need to count the number of students who have failed an exam or those with some wrong data. This if loop condition is working properly, but the count of the number of students is wrong.
<xsl:if test ="$StudentFinalmark > 100 or $StudentFinalmark < 49">
<xsl:value-of select = "count(StudentFinalmark)"/>
</xsl:if>
For example, if there are four failed students and students with two wrong dates, it's coming as 111111
Please help me with the solution.
Assuming that your xml looked like this (as per your comment below):
<Students>
<StudentResults>
<Marks>
<StudentAssign1>40</StudentAssign1>
<StudentAssign2>40</StudentAssign2>
<StudentExam>40</StudentExam>
</Marks>
</StudentResults>
<StudentResults>
<Marks>
<StudentAssign1>1</StudentAssign1>
<StudentAssign2>2</StudentAssign2>
<StudentExam>3</StudentExam>
</Marks>
</StudentResults>
<StudentResults>
<Marks>
<StudentAssign1>33</StudentAssign1>
<StudentAssign2>33</StudentAssign2>
<StudentExam>33</StudentExam>
</Marks>
</StudentResults>
</Students>
an XSLT like this will return the results you are after:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">
<xsl:element name="Output">
<xsl:value-of select="count(Students/StudentResults[sum(Marks/*) > 100 or sum(Marks/*) < 49])" />
</xsl:element>
</xsl:template>
</xsl:stylesheet>
This returns the result:
<Output>2</Output>
It returns two because, in the example XML above, the first StudentResults has a sum of 120 (which fails because it is bigger than 100) and the second has a sum of 6 (which fails because it is less than 49)
The trick is to output a single item which is a count of the elements which you select via xpath, not via an "if" and "for" combination. There isn't a need to use a variable either.

Is there a DRY way to convert values prior to the transformation?

Lets say I have the following xml.
<root>
<data>
<a>ATTITUDE_ANNOYED</a>
<b>ATTITUDE_CAUTIOUS</b>
<c>25</c>
<d>30</d>
</data>
</root>
Ignoring the schema of my output, I want my output to present A as "Cautious" (one level up from annoyed), B as "
Pleased" (one level up from Cautious") and I want to perform some maths on C and D to convert the value into something slightly different.
I've had a look at a bunch of similar questions here (and I'm new to XSLT so maybe I don't quite get it) but a lot of the solutions appear to be "in-line", i.e. you modify the result as you transform it. This is okay but in my real example there are a lot of these values and I don't want to be performing the exact same conversion in multiple places (DRY). I just want to more or less pre-process the entire document and convert a bunch of values into other values (using just a few formulas) before I start the transformation.
What would be the best way to achieve this? I'm not particularly interested in performance so is there a way that I can run a prior transformation to easily transform specific values without modifying the structure?
UPDATE: (DevNull asked for my output desires) The output isn't exactly finalised. I'm trying to help a group at CivFanatics produce a guide on the differences between the AIs whose values derive from an xml file. There are a ton of leaders and a ton of values that need converting, by hand its taking them 2 hours per leader at the moment and the final formatting hasn't been decided on so I thought we'd all save time by using something like XLST.
Here is a rough example of a demo I'm working on.
<xsl:template match="data">
<h3>Attitude Thresholds</h3>
<table border="1">
<tr><td>Will open borders</td><td><xsl:value-of select="a"/></td></tr>
<tr><td>Will trade techs</td><td><xsl:value-of select="b"/></td></tr>
</table>
</xsl:template>
To clarify A and B are OpenBordersRefuseAttitudeThreshold and TechRefuseAttitudeThreshold. The guide is more readable if these are WillOpenBordersAt and WillTradeTechAt as opposed to the original values in the xml file so I need to nudge them up a value for the final output.
There are additional conversions that have been discussed:
iWonderConstructRand -> Builds Wonders
0 -> 0/10
5 -> 1/10
10 -> 2/10
15 -> 3/10
20 -> 4/10
25 -> 5/10
30 -> 6/10
35 -> 7/10
40 -> 8/10
45 -> 9/10
50 -> 10/10
So that's values such as C or D. They range between 0-50 and should be put into a 0-10 format for readability. There are a fair few of these values too.
Also there are conversions like "GoodieBaddie" which vary from 0 to 10. Which we wish to convert to something like:
0-3 -> Bad(x)
4-6 -> Neutral(x)
7-10 -> Good(x)
Where (x) is the original value.
Am I even using the right tool for the job here or is it borderline? I figured XLST would be a good choice to enable other contributors to not have to rely on the devs to make changes to the formatting/layout (as xlst is easier to edit then say C# or Python).
This transformation:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:my="my:my">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<my:attitudes>
<a val="1">ANNOYED</a>
<a val="2">CAUTIOUS</a>
<a val="3">PLEASED</a>
</my:attitudes>
<xsl:variable name="vAttitudes" select="document('')/*/my:attitudes/*"/>
<xsl:template match="node()|#*">
<xsl:copy>
<xsl:apply-templates select="node()|#*"/>
</xsl:copy>
</xsl:template>
<xsl:template match="*[starts-with(., 'ATTITUDE_')]/text()">
<xsl:variable name="vVal" select=
"$vAttitudes[. = substring-after(current(), '_')]/#val"/>
<xsl:value-of select="concat('ATTITUDE_', $vAttitudes[#val = $vVal+1])"/>
</xsl:template>
<xsl:template match="*[floor(.) = .]/text()">
<xsl:value-of select="concat(round(. div 5), '/10')"/>
</xsl:template>
</xsl:stylesheet>
when applied on the provided XML document:
<root>
<data>
<a>ATTITUDE_ANNOYED</a>
<b>ATTITUDE_CAUTIOUS</b>
<c>25</c>
<d>30</d>
</data>
</root>
produces the wanted, correct result:
<root>
<data>
<a>ATTITUDE_CAUTIOUS</a>
<b>ATTITUDE_PLEASED</b>
<c>5/10</c>
<d>6/10</d>
</data>
</root>

date minus another date in xslt

Hope someone can help. I am trying to compare 2 dates inside an XML file and using a XSLT to do some calculation:
For example I have 2 dates in XML: 2011-05-23 and 2011-04-29. I want to do calculation inside XSLT like below:
('2011-05-23'-'2011-04-29')*30 = 24*30= 720
Can anyone shed any light?
An XSLT 2.0 solution
<?xml version="1.0"?>
<xsl:stylesheet version="2.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xsl:template match="/">
<xsl:value-of select="days-from-duration(
xs:date('2011-05-23')
- xs:date(xs:date('2011-04-29'))
)*30"/>
</xsl:template>
</xsl:stylesheet>
Yields: 720
xs:date() function evaluates the dates, which can be used to perform date operations
subtracting the second date from the first yields the xdt:dayTimeDuration P24D (24 days)
days-from-duration() extracts the days component from the xdt:dayTimeDuration (24)
then you can use that number to perform normal arethmatic (e.g. 24*30=720)
It might be worth looking at the EXSLT - date:difference solution. I believe that should do what you want, and there's even a straight XSLT implementation available.
Be aware though that the returned value is in duration format as specified in the XML Schema Part 2: Datatypes Second Edition, so it's likely you will need to process the result to derive the unit you wish to use in calculation (for instance in your example above you're expecting a result detailing the number of days difference - so you would need to pull out the relevant unit you want to work with, the result from date:difference in that case would most likely be "P24D").
Here's two templates I sometimes use for date calculations:
<xsl:template name="calcseconds">
<xsl:param name="date" />
<xsl:value-of select="(((substring($date,1,4) - 1970) * 365)+floor((substring($date,1,4) - 1970) div 4)+substring('000,031,059,090,120,151,181,212,243,273,304,334,365',substring($date,6,2)*4-3,3)+(substring($date,9,2)-1)+(1-floor(((substring($date,1,4) mod 4) + 2) div 3))*floor((substring($date,6,2)+17) div 20))*86400+(substring($date,12,2)*3600)+(substring($date,15,2)*60)+substring($date,18,2)" />
</xsl:template>
<xsl:template name="calcdays">
<xsl:param name="date" />
<xsl:value-of select="(((substring($date,1,4) - 1970) * 365)+floor((substring($date,1,4) - 1970) div 4)+substring('000,031,059,090,120,151,181,212,243,273,304,334,365',substring($date,6,2)*4-3,3)+(substring($date,9,2)-1)+(1-floor(((substring($date,1,4) mod 4) + 2) div 3))*floor((substring($date,6,2)+17) div 20))" />
</xsl:template>
They're a bit of a mouthful, but they'll calculate the number of seconds/days since midnight 1st January 1970 as an integer, which you can then do straight arithmetic on. They rely on the date format being yyyy-mm-dd hh:mm:ss, but manipulation of the parameters of the substring calls should allow you to process dates in any format you need.