How can I sort a group of students into random groups of (approx.) equal sizes?

Any Ideas for this?
Let us say the group is defined by the indexes of a dictionarytiddler ( $:/temp/students )
and the number of groups is {$:/temp/groupnumber}

I would use
https://mklauber.github.io/tw5-plugins/#Shuffle%20Operator
but I don’t know a smart flexible way to do this which allow to change the number of groups.

This isn’t a purely filter-based solution, but it is the simplest that comes to mind:

\function group.size() [[$:/temp/students]indexes[]count[]divide{$:/temp/groupnumber}round[]]

<style>
ul.groupings li:nth-of-type(<<group.size>>n) { border-bottom: 1px solid black; }
</style>

<ul class="groupings">
<$list filter="[[$:/temp/students]indexes[]shuffle[]]">
<li>{{!!title}}</li>
</$list>
</ul>

My quick testing suggests that the shuffle operator reorders the list with each wiki interaction, so you’ll probably want to print or otherwise save your results if you’ll need to be able to refer back to them later!

1 Like

This works for any number of students. Whatever students remain after integer division puts an equal number of students per group, each of the remaining individuals will be added to a group such that all groups have:

  • integer (number of students / number of groups)
  • OR integer (number of students / number of groups) + 1

Import the JSON below into the My Plugin Library — mklauber's Plugins for TiddlyWiki

Remove and add students to the Students macro to see what happens when the number of students does not divide perfectly for the number of Groupings.

Students in Groups.tid (1.7 KB)

The code, followed by screenshot of sample render:

\define Students() A B C D E F G H I J K L M N O P Q R S T U V W X Y
\define Groupings() 5

<$let studentCount={{{ [enlist<Students>count[]]  }}}
         studentsPerGroup={{{ [<studentCount>divide<Groupings>floor[]] }}}
         remaining={{{ [<studentCount>remainder<Groupings>] }}}
         studentsPerGroupPlus1={{{ [<studentCount>divide<Groupings>floor[]add[1]] }}}>

We have <<studentCount>> students that need to be divided into <<Groupings>> groups.

So <<studentsPerGroup>> students per group.  Each remaining student, if any, will be randomly placed in one of the  <<Groupings>> groups, bumping the number of students in that group to <<studentsPerGroupPlus1>>



<$vars shuffledStudents={{{ [enlist<Students>] +[shuffle[]] +[join[:::]] }}}>

<<shuffledStudents>>

<$list variable="thisGroup" filter="[range[1],<Groupings>]" counter="ind">
* __''Group <<thisGroup>>''__<br>
<$let bfCount={{{ [<thisGroup>!match[1]subtract[1]multiply<studentsPerGroup>] +[else[0]] }}}
         bfCountX={{{ [<ind>compare:number:lteq<remaining>then<ind>subtract[1]] +[else<remaining>] }}}
         bfAdjCount= {{{ [<bfCount>] [<thisGroup>!match[1]then<bfCountX>] +[sum[]]  }}}
         adjustSPG={{{ [<ind>compare:number:lteq<remaining>then[1]else[0]] }}}
         adjStudentsPerGroup={{{ [<studentsPerGroup>add<adjustSPG>] }}}>
adjustStudentsPerGroup: <<adjustSPG>> bfCount: <<bfCount>> bfCountX: <<bfCountX>> bfAdjCount: <<bfAdjCount>> <br>
<$list variable="thisStudent" filter="[<shuffledStudents>split[:::]butfirst<bfAdjCount>first<adjStudentsPerGroup>]">
** <<thisStudent>><br>
</$list>
</$let>
</$list>

</$vars>

</$let>

2 Likes

Oops, just noticed a wee bug. Back in a bit.

That last post was a TID and not a JSON. The fix is in an export that is a JSON instead:

Students in Groups.json (1.7 KB)

The fixed code, needed to add an “=” in there. See if you can spot Waldo:

\define Students() A B C D E F G H I J
\define Groupings() 5

<$let studentCount={{{ [enlist<Students>count[]]  }}}
         studentsPerGroup={{{ [<studentCount>divide<Groupings>floor[]] }}}
         remaining={{{ [<studentCount>remainder<Groupings>] }}}
         studentsPerGroupPlus1={{{ [<studentCount>divide<Groupings>floor[]add[1]] }}}>

We have <<studentCount>> students that need to be divided into <<Groupings>> groups.

So <<studentsPerGroup>> students per group.  Each remaining student, if any, will be randomly placed in one of the  <<Groupings>> groups, bumping the number of students in that group to <<studentsPerGroupPlus1>>



<$vars shuffledStudents={{{ [enlist<Students>] +[shuffle[]] +[join[:::]] }}}>

<<shuffledStudents>>

<$list variable="thisGroup" filter="[range[1],<Groupings>]" counter="ind">
* __''Group <<thisGroup>>''__<br>
<$let bfCount={{{ [<thisGroup>!match[1]subtract[1]multiply<studentsPerGroup>] +[else[0]] }}}
         bfCountX={{{ [<ind>compare:number:lteq<remaining>then<ind>subtract[1]] +[else<remaining>] }}}
         bfAdjCount= {{{ [<bfCount>] =[<thisGroup>!match[1]then<bfCountX>] +[sum[]]  }}}
         adjustSPG={{{ [<ind>compare:number:lteq<remaining>then[1]else[0]] }}}
         adjStudentsPerGroup={{{ [<studentsPerGroup>add<adjustSPG>] }}}>
adjustStudentsPerGroup: <<adjustSPG>> bfCount: <<bfCount>> bfCountX: <<bfCountX>> bfAdjCount: <<bfAdjCount>> <br>
<$list variable="thisStudent" filter="[<shuffledStudents>split[:::]butfirst<bfAdjCount>first<adjStudentsPerGroup>]">
** <<thisStudent>><br>
</$list>
</$let>
</$list>

</$vars>

</$let>

Sanity test for 6 “students”:

(I’ve left the display of internal variables there to check on what the code is doing. The bullets, I did not bother trying to make them pretty. If you’d like the “pretty” version: Students in Groups _pretty.json (1.6 KB) )

Pretty version screenshot:

Now, pardon me. I have to go and ask the guy in the mirror: “Who’s your Daddy?”

1 Like

Thank you @Charlie_Veniot , @etardiff for the great solutions. I chose the etardiff’s solution for the simplicity an the greater ease to adapt it.
This is how I modified it:

\function group.size() [list[$:/temp/classroom/groups]count[]divide{$:/temp/classroom/groups!!count}round[]]
\define shuffler()
<$list filter="[{$:/temp/classroom/groups!!group}indexes[]addprefix[[[]addsuffix{!!suff}shuffle[]]">
{{!!title}}
</$list>
\end

Group:<$select tiddler="$:/temp/classroom/groups" field="group" default="">
<option value="">choose a group</option>
<$list filter='[type[application/x-tiddler-dictionary]tag[group]]'>
<option value=<<currentTiddler>>><$view field='title'/><$view field='description'/></option>
</$list>
</$select>Number of groups:<$select tiddler="$:/temp/classroom/groups" field="count">> default='2'>
<$list filter='[range[1],[15]]'>
<option value=<<currentTiddler>>><$view field='title'/></option>
</$list>
</$select>
 <$wikify name="shuffle" text=<<shuffler>> ><$button class="tc-btn-invisible">
<$action-setfield $tiddler="$:/temp/classroom/groups" list=<<shuffle>>/>
Shuffle
</$button>
</$wikify>

<style>
ul.groupings li:nth-of-type(<<group.size>>n) { border-bottom: 1px solid black; }
</style>

<ul class="groupings">
<$list filter="[list[$:/temp/classroom/groups]]">
<li>{{!!title}}</li>
</$list>
</ul>


@etardiff one issue:

  • changing the groupnumber here does not update the css…and thus does not change the groupnumber.
    further ideas:
  • can this be formatted in a way that the groups are shown in column?
  • The perfect solution would be if each column had an edit-text on top to give the group a name…
<style>
div.groupings { display: grid; }
</style>

Group:
<$select tiddler="$:/temp/classroom/groups" field="group" default="">
	<option value="">choose a group</option>
	<$list filter='[type[application/x-tiddler-dictionary]tag[group]]'>
		<option value=<<currentTiddler>>>
			<$view field='title'/><$view field='description'/>
		</option>
	</$list>
</$select>
Number of groups:
<$select tiddler="$:/temp/classroom/groups" field="count">> default='2'>
	<$list filter='[range[1],[15]]'>
		<option value=<<currentTiddler>>><$view field='title'/></option>
	</$list>
</$select>
<$button class="tc-btn-invisible tc-tiddlylink">
	<$action-setfield
		$tiddler="$:/temp/classroom/groups"
		list={{{ [{$:/temp/classroom/groups!!group}indexes[]addsuffix{!!suff}shuffle[]] +[format:titlelist[]join[ ]] }}} />
	Shuffle
</$button>

<$let
	groups={{$:/temp/classroom/groups!!count}}
	columns={{{ [range<groups>] :map:flat[[auto]] +[join[ ]] }}}
>
<div class="groupings" style.grid-template-columns=<<columns>>>
<$list filter="[range<groups>addprefix[label]]" variable="label">
<div>
<$edit-text tiddler="$:/temp/classroom/groups" field=<<label>> placeholder=<<label>> />
</div>
</$list>
<$list filter="[list[$:/temp/classroom/groups]]">
<div>{{!!title}}</div>
</$list>
</div>
</$let>

This solution isn’t as “smart” as Charlie’s: it still doesn’t know the position or the actual group assignment of any given student in the list. But using a grid layout rather than an unordered list will give us more control over the column display, and it lets us add an appropriate number of headers to the start.

2 Likes

This is great! I really like the concept that it is still one list because it makes it easier to store the result and swap persons between groups. But for this I will need a drag-swap macro …which should be another thread to be found more easily :slight_smile:
With which class can I style the divs to give them borders and make them smaller? Do you see a smart way to give them different colors for text border and background?

You can use div.groupings div to target each list item inside the grid. Grouping by column is a little trickier, but doable since we can use TW features within the stylesheet. For instance:

<style>
div.groupings { display: grid; }

div.groupings div { padding: 0.1em 0.75em; border-right: 1px solid black }

<$let col={{$:/temp/classroom/groups!!count}}>
<$list filter="0 [range<col>] :filter[divide[2]!search[.]]" variable="offset">
div.groupings div:nth-of-type(<<col>>n-<<offset>>) { background-color: #eee }
</$list>
</$let>
</style>

What we’re doing here is using the list to programmatically generate class definitions before applying them. You could also use :filter[divide[2]search[.]] to select the inverse set of columns, or :filter[divide[3]!search[.]] to select every third column, etc.

If you end up moving your CSS into a separate CSS tiddler, do make sure that you’re not using the text/css type for it! Static stylesheets don’t get passed through the widget parser before they’re applied, so they can’t take advantage of dynamic features like this.

2 Likes

After testing for a while, I switched to a modified version of @Charlie_Veniot 's solution - because I needed more stable way to store the groups in fields.
And because I found out that I often have to modify the groups manually afterward, I wanted the groups to be managable by the list-draggable-macro… but somehow modifying by drag and drop does not work yet - does anyone see why?

\define Students() A B C D E F G H I J K L M N O P Q R S T U V W X Y
\define Groupings() 7

<$let studentCount={{{ [enlist<Students>count[]]  }}}
         studentsPerGroup={{{ [<studentCount>divide<Groupings>floor[]] }}}
         remaining={{{ [<studentCount>remainder<Groupings>] }}}
         studentsPerGroupPlus1={{{ [<studentCount>divide<Groupings>floor[]add[1]] }}}>

We have <<studentCount>> students that need to be divided into <<Groupings>> groups.

So <<studentsPerGroup>> students per group.  Each remaining student, if any, will be randomly placed in one of the  <<Groupings>> groups, bumping the number of students in that group to <<studentsPerGroupPlus1>>[$ _plugins_JJ_classroom_widgets_groups.json|attachment](upload://gg1YvFeDMmplcFgMGeyLeiAWnib.json) (3.5 KB)


<$vars shuffledStudents={{{ [enlist<Students>] +[shuffle[]] +[join[:::]] }}}>

<<shuffledStudents>>

<$button class="tc-btn-big-green">
Build New Groups
<$action-deletefield 1 2 3 4 5 6 7 8 9 10 11 12/>
<$list variable="thisGroup" filter="[range[1],<Groupings>]" counter="ind">
<$let bfCount={{{ [<thisGroup>!match[1]subtract[1]multiply<studentsPerGroup>] +[else[0]] }}}
         bfCountX={{{ [<ind>compare:number:lteq<remaining>then<ind>subtract[1]] +[else<remaining>] }}}
         bfAdjCount= {{{ [<bfCount>] [<thisGroup>!match[1]then<bfCountX>] +[sum[]]  }}}
         adjustSPG={{{ [<ind>compare:number:lteq<remaining>then[1]else[0]] }}}
         adjStudentsPerGroup={{{ [<studentsPerGroup>add<adjustSPG>] }}}>
<$list variable="thisStudent" filter="[<shuffledStudents>split[:::]butfirst<bfAdjCount>first<adjStudentsPerGroup>]">
<$action-listops $field=<<thisGroup>> $subfilter="[<thisStudent>]"/>
</$list>
</$let>
</$list>
</$button>

</$vars>

</$let>

<$list filter="[range[1],<Groupings>]" variable="fieldnum">
<div style="width:8em; display:inline-block;border: 1px solid black;vertical-align:top">

!!Group <<fieldnum>>
<$macrocall $name="list-links-draggable" field=<<fieldnum>>/>
</div>
</$list>

This works … the list draggable macro seems not to be able to add and remove…
Thanks again @Charlie_Veniot and @etardiff for the help

\define dragdroplist()
<$droppable actions=<<groupalizer>> >
<div>
Liste
<$list filter="[list[!!$(fieldnum)$]]">

<$link> &#8226; {{!!title}}</$link>
</$list>
</div>
</$droppable>
\end
\define remover() <$action-listops $field="$(delfield)$" $subfilter="-[<actionTiddler>]"/>
\define groupalizer()
<$list filter="[range[1],<Groupings>]" variable="delfield">
<<remover>>
</$list>
<$action-listops $field="$(fieldnum)$" $subfilter="+[insertbefore:currentTiddler<actionTiddler>]"/>
\end
\define Students() A B C D E F G H I J K L M N O P Q R S T U V W X Y
\define Groupings() 7

<$let studentCount={{{ [enlist<Students>count[]]  }}}
         studentsPerGroup={{{ [<studentCount>divide<Groupings>floor[]] }}}
         remaining={{{ [<studentCount>remainder<Groupings>] }}}
         studentsPerGroupPlus1={{{ [<studentCount>divide<Groupings>floor[]add[1]] }}}>

We have <<studentCount>> students that need to be divided into <<Groupings>> groups.

So <<studentsPerGroup>> students per group.  Each remaining student, if any, will be randomly placed in one of the  <<Groupings>> groups, bumping the number of students in that group to <<studentsPerGroupPlus1>>


<$vars shuffledStudents={{{ [enlist<Students>] +[shuffle[]] +[join[:::]] }}}>

<<shuffledStudents>>

<$button class="tc-btn-big-green">
New Random Groups
<$action-deletefield 1 2 3 4 5 6 7 8 9 10 11 12/>
<$list variable="thisGroup" filter="[range[1],<Groupings>]" counter="ind">
<$let bfCount={{{ [<thisGroup>!match[1]subtract[1]multiply<studentsPerGroup>] +[else[0]] }}}
         bfCountX={{{ [<ind>compare:number:lteq<remaining>then<ind>subtract[1]] +[else<remaining>] }}}
         bfAdjCount= {{{ [<bfCount>] [<thisGroup>!match[1]then<bfCountX>] +[sum[]]  }}}
         adjustSPG={{{ [<ind>compare:number:lteq<remaining>then[1]else[0]] }}}
         adjStudentsPerGroup={{{ [<studentsPerGroup>add<adjustSPG>] }}}>
<$list variable="thisStudent" filter="[<shuffledStudents>split[:::]butfirst<bfAdjCount>first<adjStudentsPerGroup>]">
<$action-listops $field=<<thisGroup>> $subfilter="[<thisStudent>]"/>
</$list>
</$let>
</$list>
</$button>

</$vars>

</$let>

<$list filter="[range[1],<Groupings>]" variable="fieldnum">
<div style="width:8em; display:inline-block;border: 1px solid black;vertical-align:top">
<span style="font-size:1.5em">Group <<fieldnum>></span>
<<dragdroplist>>
</div>
</$list>

I guess I broke the filter in the previous version, here is the one working correctly - including drag n drop…
$ _plugins_JJ_classroom_widgets_groups.json (3.5 KB)

I have now finished the grouptool based on @Charlie_Veniot 's proposition which I post as a permaview below, because it relies on language-dictionary-tiddler you would have to adapt to have your language.
New features:

  • it now allows reordering groups and by drag and drop and for the big classroom-touchscreen also by dropdown.
  • and it allows absent students not assigned to any group.

Grouptools

2 Likes