I'm currently working on an xslt using xslt 1.0 to look at a response from a webservice and tell me if there is a difference in value. Issue is the response is dynamic. I am having some issues with using keys/dont know if the way I'm using it is correct
In my example we are checking attendance in high school classes and need to know if there is a difference in the number of students who have attended in the last 5 days. The xml response we receive is below
<response>
<school1>
<class>
<attendance><class>a</class><day>monday</day><count>20</count></attendance>
<attendance><class>a</class><day>tuesday</day><count>20</count></attendance>
<attendance><class>a</class><day>wednesday</day><count>20</count></attendance>
<attendance><class>a</class><day>thursday</day><count>20</count></attendance>
<attendance><class>a</class><day>friday</day><count>20</count></attendance>
<attendance><class>b</class><day>monday</day><count>20</count></attendance>
<attendance><class>b</class><day>tuesday</day><count>20</count></attendance>
<attendance><class>b</class><day>wednesday</day><count>12</count></attendance>
<attendance><class>b</class><day>thursday</day><count>20</count></attendance>
<attendance><class>b</class><day>friday</day><count>20</count></attendance>
<attendance><class>c</class><day>monday</day><count>22</count></attendance>
<attendance><class>c</class><day>tuesday</day><count>23</count></attendance>
<attendance><class>c</class><day>wednesday</day><count>12</count></attendance>
<attendance><class>c</class><day>thursday</day><count>20</count></attendance>
<attendance><class>c</class><day>friday</day><count>22</count></attendance>
</class>
</school1>
</response>
My xslt should step through each class and compare the count, if there is a difference in numbers it should write out a message to alert the person. The xslt I have is throwing compile errors so i'm not sure I am using keys(a new concept to me) correctly. The xslt I have for this is:
<xsl:stylesheet version="1.0">
<xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
<xsl:key name="test" match="attendance[class='a']" use="count"/>
<xsl:key name="test2" match="attendance[class='a'][last()]" use="count"/>
<xsl:key name="test3" match="attendance[class='b']" use="count"/>
<xsl:key name="test4" match="attendance[class='b'][last()]" use="count"/>
<xsl:key name="test5" match="attendance[class='c']" use="count"/>
<xsl:key name="test6" match="attendance[class='c'][last()]" use="count"/>
<xsl:template match="/">
<xsl:for-each select="/response/school1/class/attendance">
<xsl:choose>
<xsl:when test="$test!=$test2">
<xsl:value-of select="Check class roll" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="Perfect Attendance" />
</xsl:otherwise>
<xsl:when test ="$test3!=$test4">
<xsl:value-of select="Check class roll" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="Perfect Attendance" />
</xsl:otherwise>
<xsl:when test ="$test5!=$test6">
<xsl:value-of select="Check class roll" />
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="Perfect Attendance" />
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
</xsl:template>
</xsl:stylesheet>
I expect an output of
Perfect Attendance Check class roll Check class roll
Any help would be greatly appreciated.
CodePudding user response:
If you're willing to hardcode the class names into your stylesheet, then you could get the expected result quite simply using:
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:key name="attend-by-class" match="attendance" use="class" />
<xsl:template match="/">
<xsl:variable name="a" select="key('attend-by-class', 'a')" />
<xsl:variable name="b" select="key('attend-by-class', 'b')" />
<xsl:variable name="c" select="key('attend-by-class', 'c')" />
<!-- output -->
<xsl:choose>
<xsl:when test="$a/count != $a/count">Check class roll</xsl:when>
<xsl:otherwise>Perfect Attendance</xsl:otherwise>
</xsl:choose>
<xsl:text> </xsl:text>
<xsl:choose>
<xsl:when test="$b/count != $b/count">Check class roll</xsl:when>
<xsl:otherwise>Perfect Attendance</xsl:otherwise>
</xsl:choose>
<xsl:text> </xsl:text>
<xsl:choose>
<xsl:when test="$c/count != $c/count">Check class roll</xsl:when>
<xsl:otherwise>Perfect Attendance</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>
A smarter approach would eliminate both hard-coding and code repetition by utilizing the same key to perform Muenchian grouping by class, before doing the comparison within each group.
Note also that a result of "Perfect Attendance"
does not in fact signify perfect attendance. All we are testing here are fluctuations in attendance. If class a
has 21 students, and exactly one student was absent on every one of the 5 days tested, then the result will be reported as "Perfect Attendance"
nevertheless.
CodePudding user response:
This solution may interest you. Good luck!
<xsl:key name="class" match="attendance" use="class"/>
<xsl:template match="class">
<!-- Get the first class of each group. -->
<xsl:apply-templates select="attendance[generate-id(.) = generate-id(key('class',class)[1])]"/>
</xsl:template>
<xsl:template match="attendance">
<xsl:choose>
<xsl:when test="key('class',class)/count != key('class',class)/count">
<xsl:value-of select="'Check class roll'"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="'Perfect Attendance'"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template match="node()|@*">
<xsl:apply-templates select="node()|@*"/>
</xsl:template>
CodePudding user response:
This is actually a classic of the kind of problem which is best solved with grouping rather than using keys. You want to process the attendance
elements grouped by their class
. and for each group, you want to see if there is any discrepancy between the count
elements in the group.
EDIT I've made some changes to my original answer since obviously for-each-group
is not avialable in XSLT 1 - sorry!
<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="/">
<xsl:variable name="attendances" select="/response/school1/class/attendance"/>
<attendances>
<xsl:for-each select="$attendances[not(class=preceding-sibling::attendance/class)]">
<xsl:variable name="current-class" select="class"/>
<xsl:variable name="current-class-attendances" select="$attendances[class=$current-class]"/>
<result >
<xsl:choose>
<xsl:when test="$current-class-attendances/count != $current-class-attendances/count">
<xsl:text>Check class roll</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>Perfect Attendance</xsl:text>
</xsl:otherwise>
</xsl:choose>
</result>
</xsl:for-each>
</attendances>
</xsl:template>
</xsl:stylesheet>
I have changed the template which matches /
so that it creates a root element (which I called attendances
). If your stylesheet doesn't produce a single root element then the output will not be well-formed XML. If you actually do want to produce plain text, then you should change your xsl:output/@method
to text
. I also wrapped the text messages with result
and gave them a class
attribute so you could see which result belongs to which class.
The for-each
statement selects every attendance which is the first to have a particular class (i.e. it has no preceding-siblings which have the same class). I have a current-class-attendances
variable which filters the full list of attendances down to just the ones for the current class. The xsl:when
statement checks to see if there are any differences in the count
among those attendance
elements.
The expression $current-class-attendances/count != $current-class-attendances/count
may seem a little cryptic, but it's actually straightforward. In XPath, this is called a "general comparison" https://www.w3.org/TR/xpath20/#id-general-comparisons. If you compare two nodesets using an operator like !=
(or =
, <
, etc) then the expression will evaluate to true if the comparison is true for at least one of the nodes from the left-hand nodeset and one from the right-hand nodeset. So $some-nodes != $some-nodes
means "is there any node in the $some-nodes nodeset which is not equal to another node in the $some-nodes nodeset?"
Result:
<attendance>
<result >Perfect Attendance</result>
<result >Check class roll</result>
<result >Check class roll</result>
</attendance>
Can I suggest though that you really need to do some systematic study of the basics of XSLT, e.g. working your way through a book about it?
EDIT 2
Here's a version of the stylesheet which uses a key:
<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:key name="attendances-by-class" match="/response/school1/class/attendance" use="class"/>
<xsl:template match="/">
<xsl:variable name="first-attendances-of-each-class" select="
/response/school1/class/attendance
[generate-id(.) = generate-id(key('attendances-by-class', class)[1])]
"/>
<attendances>
<xsl:for-each select="$first-attendances-of-each-class">
<xsl:variable name="current-class-attendances" select="key('attendances-by-class', class)"/>
<result >
<xsl:choose>
<xsl:when test="$current-class-attendances/count != $current-class-attendances/count">
<xsl:text>Check class roll</xsl:text>
</xsl:when>
<xsl:otherwise>
<xsl:text>Perfect Attendance</xsl:text>
</xsl:otherwise>
</xsl:choose>
</result>
</xsl:for-each>
</attendances>
</xsl:template>
</xsl:stylesheet>
The attendances-by-class
key is an index of the attendance
elements by the value of their class
child element.
Firstly the key is used to obtain a list of the attendance
elements which are the first to have a particular value in their class
(this is what the /response/school1/class/attendance[generate-id(.) = generate-id(key('attendances-by-class', class)[1])]
expression achieves. The generate-id()
function produces a unique string value for the attendance
node, and that's compared with the corresponding value of the first of the attendance
elements returned by the key. This is a bit more complicated than in my previous example which didn't use keys.
The for-each
loop then iterates over that set of attendance
elements. Inside the for-each
loop the key is used again to retrieve the attendance
records which belong to the same group. Here the key is a slight simplification over my previous example.
You asked for a help using key
and so here's an answer. But I'd argue that the extra overhead of a key is barely worth it for a situation such as this: the number of records is so small (just 15) that the cost of searching use preceding-sibling
is minimal, and actually comparable to the overhead for the XSLT processor of collating an index. It may save a few microseconds, but in the context of a piece of software that makes a web service call, that will be a tiny fraction of the total time.