1

I am writing a program, that will divide my students into groups. Lets say the groups are A, B, C and D. In each group there can be only so many students. Those numbers I have in a global variable:

<xsl:variable name="volnemiesta" select="16, 15, 30, 0"/>.

The students can pick their group as preferences. So one can pick A,B,C,D, another B,C,D,A. Each student has a score. The students are divided according the score, those with higher score get to pick first, those with lower score later. Each student get into the group which is sooner in his preferences and has space.

I need to remember how many student are already in each group so the program can assign students only to not full groups. The main part of my program is as follows:

<xsl:variable name="volnemiesta" select="16, 15, 30, 0"/>

<xsl:template match="/triedy">
   <xsl:variable name="zameraniavsetky" select="string-join((trieda/student/@vyber)[. != ''], ',')"/>
   <xsl:variable name="zamerania">
      <xsl:perform-sort select="distinct-values(tokenize($zameraniavsetky, ','))">
         <xsl:sort select="."/>
      </xsl:perform-sort>
   </xsl:variable>
   <xsl:result-document href="./zoznam.txt" method="text">
      <xsl:for-each select="trieda/student">
         <xsl:sort select="@priemer = ''"/>
         <xsl:sort select="@priemer" data-type="number"/>
         <xsl:sort select="@priezvisko"/>
         <xsl:sort select="@meno"/>
         <xsl:variable name="zameranie" select="my:prirad(tokenize(@vyber, ','), $volnemiesta)"/>
         <xsl:variable name="volnemiesta" select="my:zmen($zameranie, $volnemiesta)"/>
         <xsl:apply-templates select=".">
            <xsl:with-param name="zameranie" select="$zameranie"/>
         </xsl:apply-templates>
      </xsl:for-each>
   </xsl:result-document>
</xsl:template>

The line <xsl:variable name="zameranie" select="my:prirad(tokenize(@vyber, ','), $volnemiesta)"/> calculates the group assigned to the student based on his preferences and number of free places in the group. And this line: <xsl:variable name="volnemiesta" select="my:zmen($zameranie, $volnemiesta)"/> should compute the changed free space in the groups. And here is the problem. I know I cannot change the value of a variable. How can I do that?

Update 1

So here is my full stylesheet. I think I do something wrong in the my:zmen function, because after the computation the error is Error at char 34 in expression in xsl:accumulator-rule/@select on line 8 column 116 of zamerania.xsl: XTTE0570 Cannot convert string "15 15 30 0" to an integer.

<?xml version="1.1" encoding="UTF-8"?>
<xsl:stylesheet version="3.0" xml:lang="sk"
   xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
   xmlns:xs="http://www.w3.org/2001/XMLSchema"
   xmlns:my="http://www.spsjm.sk">

<xsl:accumulator name="volnemiesta" as="xs:integer+" initial-value="16, 15, 30, 0">
  <xsl:accumulator-rule match="trieda/student" select="my:zmen(my:prirad(tokenize(@vyber, ','), $value), $value)"/>
</xsl:accumulator>

<xsl:mode use-accumulators="volnemiesta"/>

<xsl:template match="text()"/>

<xsl:template match="/triedy">
   <xsl:variable name="zameraniavsetky" select="string-join((trieda/student/@vyber)[. != ''], ',')"/>
   <xsl:variable name="zamerania">
      <xsl:perform-sort select="distinct-values(tokenize($zameraniavsetky, ','))">
         <xsl:sort select="."/>
      </xsl:perform-sort>
   </xsl:variable>
   <xsl:result-document href="./zoznam.txt" method="text">
      <xsl:apply-templates select="trieda/student">
         <xsl:sort select="@priemer = ''"/>
         <xsl:sort select="@priemer" data-type="number"/>
         <xsl:sort select="@priezvisko"/>
         <xsl:sort select="@meno"/>
      </xsl:apply-templates>
   </xsl:result-document>
</xsl:template>

<xsl:template match="student">
   <xsl:variable name="zameranie" select="my:prirad(tokenize(@vyber, ','), accumulator-before('volnemiesta'))"/>
   <xsl:value-of select="@meno"/>
   <xsl:text>&#x09;&#x09;</xsl:text>
   <xsl:value-of select="@priezvisko"/>
   <xsl:text>&#x09;&#x09;</xsl:text>
   <xsl:value-of select="../@id"/>
   <xsl:text>&#x09;&#x09;</xsl:text>
   <xsl:value-of select="@priemer"/>
   <xsl:text>&#x09;&#x09;</xsl:text>
   <xsl:value-of select="@vyber"/>
   <xsl:text>&#x09;&#x09;=>&#x09;&#x09;</xsl:text>
   <xsl:value-of select="$zameranie"/>
   <xsl:text>&#x0A;</xsl:text>
</xsl:template>

<xsl:function name="my:prirad">
   <xsl:param name="vybrane"/>
   <xsl:param name="volnemiesta"/>
   <xsl:variable name="zameranie">
      <xsl:choose>
         <xsl:when test="$vybrane[1] = 'EEN' and $volnemiesta[1] &gt; 0">
            <xsl:text>EEN</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[1] = 'OBZ' and $volnemiesta[2] &gt; 0">
            <xsl:text>OBZ</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[1] = 'PIT' and $volnemiesta[3] &gt; 0">
            <xsl:text>PIT</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[1] = 'VYE' and $volnemiesta[4] &gt; 0">
            <xsl:text>VYE</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[2] = 'EEN' and $volnemiesta[1] &gt; 0">
            <xsl:text>EEN</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[2] = 'OBZ' and $volnemiesta[2] &gt; 0">
            <xsl:text>OBZ</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[2] = 'PIT' and $volnemiesta[3] &gt; 0">
            <xsl:text>PIT</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[2] = 'VYE' and $volnemiesta[4] &gt; 0">
            <xsl:text>VYE</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[3] = 'EEN' and $volnemiesta[1] &gt; 0">
            <xsl:text>EEN</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[3] = 'OBZ' and $volnemiesta[2] &gt; 0">
            <xsl:text>OBZ</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[3] = 'PIT' and $volnemiesta[3] &gt; 0">
            <xsl:text>PIT</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[3] = 'VYE' and $volnemiesta[4] &gt; 0">
            <xsl:text>VYE</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[4] = 'EEN' and $volnemiesta[1] &gt; 0">
            <xsl:text>EEN</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[4] = 'OBZ' and $volnemiesta[2] &gt; 0">
            <xsl:text>OBZ</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[4] = 'PIT' and $volnemiesta[3] &gt; 0">
            <xsl:text>PIT</xsl:text>
         </xsl:when>
         <xsl:when test="$vybrane[4] = 'VYE' and $volnemiesta[4] &gt; 0">
            <xsl:text>VYE</xsl:text>
         </xsl:when>
      </xsl:choose>
   </xsl:variable>
   <xsl:sequence select="$zameranie"/>
</xsl:function>

<xsl:function name="my:zmen">
   <xsl:param name="zameranie"/>
   <xsl:param name="volnemiesta"/>
   <xsl:variable name="upravenemiesta" as="xs:integer+">
      <xsl:choose>
         <xsl:when test="$zameranie='EEN'">
            <xsl:value-of select="$volnemiesta[1]-1, $volnemiesta[2], $volnemiesta[3], $volnemiesta[4]"/>
         </xsl:when>
         <xsl:when test="$zameranie='OBZ'">
            <xsl:value-of select="$volnemiesta[1], $volnemiesta[2]-1, $volnemiesta[3], $volnemiesta[4]"/>
         </xsl:when>
         <xsl:when test="$zameranie='PIT'">
            <xsl:value-of select="$volnemiesta[1], $volnemiesta[2], $volnemiesta[3]-1, $volnemiesta[4]"/>
         </xsl:when>
         <xsl:when test="$zameranie='VYE'">
            <xsl:value-of select="$volnemiesta[1], $volnemiesta[2], $volnemiesta[3], $volnemiesta[4]-1"/>
         </xsl:when>
      </xsl:choose>
   </xsl:variable>
   <xsl:sequence select="$upravenemiesta"/>
</xsl:function>
</xsl:stylesheet>

Update 2

Sample XML file ziaci.xml.

<triedy xml:lang="sk">
   <trieda id="III.C">
      <student meno="Adam" priezvisko="Prvý" priemer="1.0" vyber="EEN,PIT,OBZ"/>
      <student meno="Bohumil" priezvisko="Druhý" priemer="3.2" vyber="PIT,OBZ,EEN"/>
      <student meno="Cyril" priezvisko="Tretí" priemer="1.6" vyber="EEN,OBZ,PIT"/>
      <student meno="Daniel" priezvisko="Štvrtý" priemer="1.6" vyber="EEN,PIT,OBZ"/>
      <student meno="Emil" priezvisko="Piaty" priemer="2.0" vyber="OBZ,EEN,PIT"/>
   </trieda>
   <trieda id="III.D">
      <student meno="Filip" priezvisko="Šiesty" priemer="2.8" vyber="OBZ,EEN,PIT"/>
      <student meno="Gustav" priezvisko="Siedmy" priemer="1.4" vyber="EEN,PIT,OBZ"/>
      <student meno="Hugo" priezvisko="Osmy" priemer="1.6" vyber="EEN,PIT,OBZ"/>
      <student meno="Ivan" priezvisko="Deviaty" priemer="2.6" vyber="PIT,EEN,OBZ"/>
      <student meno="Jano" priezvisko="Desiaty" priemer="2.0" vyber="EEN,PIT,OBZ"/>
   </trieda>
</triedy>
5
  • Apparently xslt is functional like haskell, so this could be done recursively (include the template with (volnemiesta, tail students) as inputs, exit when students is empty, passing updated volnemiesta for every nested level). Commented Sep 1 at 20:47
  • 1
    These days (actually since 2017) the latest version of XSLT is 3.0 which has xsl:iterate to sequentially process some items and pass variable/parameter values on to the next processing. As you use stuff like perform-sort and xsl:result-document, you probably use an XSLT 3 processor like Saxon 12 (Saxon has been an XSLT 3 processor since 9.8), thus, if you want to keep a state and think in terms of loops and variables xsl:iterate is an option. There also accumulators, fn:fold-left or recursion in general, be it with functions or apply-templates (where you can also pass parameters) Commented Sep 1 at 20:48
  • This is an interesting problem, similar to the Stable Marriage Problem. You make reference to my:prirad() and my:zmen which are your functions for calculating group assignment to a student and also space left in a group. That's taking an iterative and procedural approach, wherein the notion of updating variables as the algorithm proceeds down the list of students. XSLT is not iterative and <xsl:for-each> is not a loop but a mapping. To approach requires thinking of how to fill up each group according to preferences. I'll see if I can't propose a solution. Commented Sep 1 at 20:49
  • 2
    @al.truisme, I agree, it's a kind of Stable Marriage Problem and there is an XSLT 2.0 implementation of Gale-Shapley algorithm (rosettacode.org/wiki/Stable_marriage_problem#XSLT_2.0) Commented Sep 2 at 5:48
  • 1
    If you want your variable to be a sequence of integers, i.e. <xsl:variable name="upravenemiesta" as="xs:integer+">, anywhere where you want to return that sequence, don't use xsl:value-of (as that creates a text node), use xsl:sequence e.g. <xsl:sequence select="$volnemiesta[1]-1, $volnemiesta[2], $volnemiesta[3], $volnemiesta[4]"/> Commented Sep 2 at 13:04

2 Answers 2

2

I would (assuming XSLT 3) suggest to store the volnemiesta in an accumulator e.g.

<xsl:accumulator name="volnemiesta" as="xs:integer+" initial-value="16, 15, 30, 0">
  <xsl:accumulator-rule match="trieda/student" select="my:zmen(my:prirad(tokenize(@vyber, ','), $value), $value)"/>
</xsl:accumulator>

and then replace the xsl:for-each with xsl:apply-templates

      <xsl:apply-templates select="trieda/student">
         <xsl:sort select="@priemer = ''"/>
         <xsl:sort select="@priemer" data-type="number"/>
         <xsl:sort select="@priezvisko"/>
         <xsl:sort select="@meno"/>
      </xsl:apply-templates>

...

<xsl:template match="trieda/student">
  <xsl:variable name="zameranie" select="my:prirad(tokenize(@vyber, ','), accumulator-before('volnemiesta'))"/>
  ...

</xsl:template>

And additionally, as your comments indicate, as the normal accumulator computation happens on processing nodes in tree order, you would need to first sort your elements into a temporary tree:

<xsl:template match="/triedy">
   <xsl:variable name="zameraniavsetky" select="string-join((trieda/student/@vyber)[. != ''], ',')"/>
   <xsl:variable name="zamerania">
      <xsl:perform-sort select="distinct-values(tokenize($zameraniavsetky, ','))">
         <xsl:sort select="."/>
      </xsl:perform-sort>
   </xsl:variable>
   <xsl:variable name="sorted-students">
     <xsl:perform-sort select="trieda/student">
       <xsl:sort select="@priemer = ''"/>
         <xsl:sort select="@priemer" data-type="number"/>
         <xsl:sort select="@priezvisko"/>
         <xsl:sort select="@meno"/>
     </xsl:perform-sort>
   </xsl:variable>
   <xsl:result-document href="./zoznam.txt" method="text">
      <xsl:apply-templates select="$sorted-students/student"/>
   </xsl:result-document>
</xsl:template>

The xsl:accumulator-rule match="trieda/student" needs to be changed to xsl:accumulator-rule match="student" for that to work out with the temporary tree.

Sign up to request clarification or add additional context in comments.

14 Comments

I tried the accumulator path, but now in the first round I get an arror telling me it cannot convert from string to xs:integer. I will update my question with the full stylesheet.
Can you add a small XML input sample as well? It is easier to debug code that we can run/execute.
For code using value-of, like <xsl:when test="$zameranie='EEN'"><xsl:value-of select="$volnemiesta[1]-1, $volnemiesta[2], $volnemiesta[3], $volnemiesta[4]"/>, try using sequence e.g. <xsl:when test="$zameranie='EEN'"><xsl:sequence select="$volnemiesta[1]-1, $volnemiesta[2], $volnemiesta[3], $volnemiesta[4]"/>
Thanks, I changed it. Do I still need the variable upravenemiesta, when i create directly a sequence then?
@trinarSK, you can of course use your xsl:choose/xsl:when directly as the function body, without using a variable.
Ok. It works almost exactly as supposed, but the accumulator is behaving strangely. It doesn't count from high to low but the numbers jump. Like it should start at (16 15 30 0) and end at (0 0 0 0) with the numbers decreasing. But the first numbers are (3 7 19 0), (11 13 27 0), (9 12 25 0), (8 12 25 0), (0 5 5 0)... Is the accumulator maybe called more times somewhere? I think the accumulator takes the student not sorted but as they are in the file, because the first person in the file has numbers (15 15 30 0) even he is only 32nd in line.
@trinarSK, yes, you are right, accumulators are processed/computed based on the xsl:accumulator-rule match="trieda/student" and that is document order traversal and not a sorted processing with xsl:apply-templates. I will need to think about the problem again, you might want to try the xsl:iterate approach I commented on initially and that the other answer has spelled out. If I find a way around the mismatch of this answer's suggestions (i.e. accumulator with sorted items) I will edit or comment.
I have edited the answer with a suggestion to first sort and store the students in a temporary tree (variable), let me know whether then the accumulator values are as you need them.
This is working perfectly! Only in the accumulator-rule match there should be only student and not trieda/student now. Thanks very much.
One question thou. With the sorting beforehand I lost the ability to get parents attributes. How can i get the attribute id in parent trieda?
You can't use xsl:perform-sort then, but certainly use <xsl:variable name="sorted-students"><xsl:apply-templates select="trieda" mode="copy"/></xsl:variable><xsl:mode name="copy" on-no-match="shallow-copy"/><xsl:template mode="copy" match="trieda"><xsl:copy><xsl:apply-templates select="@*"/><xsl:apply-templates select="student" mode="#current"><xsl:sort select="@priemer = ''"/> <xsl:sort select="@priemer" data-type="number"/> <xsl:sort select="@priezvisko"/> <xsl:sort select="@meno"/></xsl:apply-templates></xsl:copy></xsl:template> to have both the student and their parent are in
I tought about appending the trieda id to the student node when creating the temporary tree. And from there to read it in student. But it is even posible to append an attribute to a node inside sorting?
You can create any result tree you want while sorting (with xsl:sort inside of xsl:apply-templates or xsl:for-each). So that is why I suggested to no longer use xsl:perform-sort but switch to xsl:apply-templates and then make sure you keep the parent with the attributes; but you could of course write a template for student in the mode (I have named it copy but choose any mode name you like) and then have that templates copy over the parent's attribute(s) you need directly to the student element.
so in the end I did this
0

The accumulator approach is more elegant but a bit too heady for me so late at night.

I put together a list of 200 names (unique) and different order permutations:

<preferences>
    <preference name="James" order="A B D C"/>
    <preference name="Mary" order="A C B D"/>
    <preference name="Michael" order="A C D B"/>
    <preference name="Patricia" order="A D B C"/>
    <preference name="John" order="A D C B"/>
    <preference name="Jennifer" order="B A C D"/>
    <preference name="Robert" order="B A D C"/>
    <preference name="Linda" order="B C A D"/>
    <preference name="David" order="B C D A"/>
...
</preferences>

The output is 5 groups, because when all of the groups are full, you put the student in the NONE group.

The output looks like:

<groups>
  <group name="A">
     <member>James</member>
     <member>Mary</member>
     <member>Michael</member>
     ...
     <member>Mark</member>
     <member>Carol</member>
     <member>Kevin</member>
     <member>Deborah</member>
  </group>
  <group name="B">
     <member>Jennifer</member>
     <member>Robert</member>
     <member>Linda</member>
     <member>David</member>
     ...
  </group>
  <group name="D"/>
  <group name="NONE">
     <member>Kathleen</member>
     <member>Nicholas</member>
     <member>Angela</member>
     <member>Gary</member>
     <member>Dorothy</member>
     <member>Eric</member>
     <member>Shirley</member>
     <member>Jonathan</member>
     <member>Emma</member>
     ...
  </group>
</groups>

Here's my approach with <xsl:iterate>.

<xsl:stylesheet
        xmlns:xsl ="http://www.w3.org/1999/XSL/Transform"
        xmlns:xs  = "http://www.w3.org/2001/XMLSchema"
        xmlns:map = "http://www.w3.org/2005/xpath-functions/map"
        xmlns:l   = "local:functions"
        
        exclude-result-prefixes="#all"
        expand-text="yes"
        version="3.0">

    <xsl:mode on-no-match="shallow-copy"/>

    <xsl:output indent="yes" />

    <xsl:variable name="group-size" as="map(xs:string, xs:integer)" >
        <xsl:map>
            <xsl:map-entry key="'A'" select="xs:integer(16)" />
            <xsl:map-entry key="'B'" select="xs:integer(15)" />
            <xsl:map-entry key="'C'" select="xs:integer(30)" />
            <xsl:map-entry key="'D'" select="xs:integer(0)" />
        </xsl:map>
    </xsl:variable>

    <xsl:param name="start-groups" as="map(xs:string, xs:string*)" >
        <xsl:map>
            <xsl:map-entry key="'A'"    select="()" />
            <xsl:map-entry key="'B'"    select="()" />
            <xsl:map-entry key="'C'"    select="()" />
            <xsl:map-entry key="'D'"    select="()" />
            <xsl:map-entry key="'NONE'" select="()" />
        </xsl:map>
    </xsl:param>

    <xsl:template match="/" >
        <xsl:iterate select="/preferences/preference" >

            <xsl:param name="assigned-groups" as="map(xs:string, xs:string*)" select="$start-groups" />

            <xsl:on-completion>
                <groups>
                    <xsl:for-each select="map:keys($assigned-groups)" >
                        <xsl:sort />
                        <group name="{current()}" >
                            <xsl:for-each select="$assigned-groups(current())" >
                                <member>{current()}</member>
                            </xsl:for-each>
                        </group>
                    </xsl:for-each>
                </groups>
            </xsl:on-completion>

            <xsl:variable name="best" as="xs:string" select="l:findBest(tokenize(@order), $assigned-groups)" />

            <xsl:next-iteration>
                <xsl:with-param name="assigned-groups"
                                select="map:put($assigned-groups, $best, ($assigned-groups($best), string(@name)) )" />
            </xsl:next-iteration>

        </xsl:iterate>
    </xsl:template>

    <xsl:function name="l:findBest" as="xs:string" >
        <xsl:param name="order"           as="xs:string*"                 />
        <xsl:param name="assigned-groups" as="map(xs:string, xs:string*)" />

        <xsl:sequence select="if (empty($order))
                              then 'NONE'
                              else let $head := head($order)
                                   return if ( count($assigned-groups($head)) lt $group-size($head) )
                                          then $head
                                          else l:findBest(tail($order), $assigned-groups)" />
    </xsl:function>

</xsl:stylesheet>

3 Comments

Also, just re-reading, I see that there is a bit about choosing according to "score" which student gets first pick. Just need to add a score attribute in the preferences.xml and then do a xsl:perform-sort on the preferences data, using the "score" as the sorting key.
Thanks for the suggestion, but it seems too complicated for me to understand. I will try the accumulator way.
It will be good to see what you end up finding the best solution for you.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.