@@ -2,6 +2,7 @@ package database
22
33import (
44 "context"
5+ "sort"
56 "time"
67
78 "github.com/sirupsen/logrus"
@@ -181,18 +182,35 @@ func GetClosedVotedPolls(ctx context.Context, userId string) ([]*Poll, error) {
181182 return polls , nil
182183}
183184
184- func calculateRankedResult (votesRaw []RankedVote ) ([]map [string ]int , error ) {
185- // We want to store those that were eliminated
185+ // calculateRankedResult determines a result for a ranked choice vote
186+ // votesRaw is the RankedVote entries that are returned directly from the database
187+ // The algorithm defined in the Constitution as of 26 Nov 2025 is as follows:
188+ //
189+ // > The winning option is selected outright if it gains more than half the votes
190+ // > cast as a first preference. If not, the option with the fewest number of first
191+ // > preference votes is eliminated and their votes move to the second preference
192+ // > marked on the ballots. This process continues until one option has half of the
193+ // > votes cast and is elected.
194+ //
195+ // The return value consists of a list of voting rounds. Each round contains a
196+ // mapping of the vote options to their vote share for that round. If the vote
197+ // is not decided in a given round, there will be a subsequent round with the
198+ // option that had the fewest votes eliminated, and its votes redistributed.
199+ //
200+ // The last entry in this list is the final round, and the option with the most
201+ // votes in this round is the winner. If all options have the same, then it is
202+ // unfortunately a tie, and the vote is not resolvable, as there is no lowest
203+ // option to eliminate.
204+ func calculateRankedResult (ctx context.Context , votesRaw []RankedVote ) ([]map [string ]int , error ) {
205+ // We want to store those that were eliminated so we don't accidentally reinclude them
186206 eliminated := make ([]string , 0 )
187207 votes := make ([][]string , 0 )
188208 finalResult := make ([]map [string ]int , 0 )
189209
190210 //change ranked votes from a map (which is unordered) to a slice of votes (which is ordered)
191211 //order is from first preference to last preference
192212 for _ , vote := range votesRaw {
193- temp , cf := context .WithTimeout (context .Background (), 1 * time .Second )
194- optionList := orderOptions (vote .Options , temp )
195- cf ()
213+ optionList := orderOptions (ctx , vote .Options )
196214 votes = append (votes , optionList )
197215 }
198216
@@ -321,7 +339,7 @@ func (poll *Poll) GetResult(ctx context.Context) ([]map[string]int, error) {
321339 }
322340 var votesRaw []RankedVote
323341 cursor .All (ctx , & votesRaw )
324- return calculateRankedResult (votesRaw )
342+ return calculateRankedResult (ctx , votesRaw )
325343 }
326344 return nil , nil
327345}
@@ -335,21 +353,35 @@ func containsValue(slice []string, value string) bool {
335353 return false
336354}
337355
338- func orderOptions (options map [string ]int , ctx context.Context ) []string {
339- result := make ([]string , 0 , len (options ))
340- order := 1
341- for order <= len (options ) {
342- for option , preference := range options {
343- select {
344- case <- ctx .Done ():
345- return make ([]string , 0 )
346- default :
347- if preference == order {
348- result = append (result , option )
349- order += 1
350- }
351- }
352- }
356+ // orderOptions takes a RankedVote's options, and returns an ordered list of
357+ // their choices
358+ //
359+ // it's invalid for a vote to list the same number multiple times, the output
360+ // will vary based on the map ordering of the options, and so is not guaranteed
361+ // to be deterministic
362+ //
363+ // ctx is no longer used, as this function is not expected to hang, but remains
364+ // an argument per golang standards
365+ //
366+ // the return values is the option keys, ordered from lowest to highest
367+ func orderOptions (ctx context.Context , options map [string ]int ) []string {
368+ // Figure out all the ranks they've listed
369+ var ranks []int = make ([]int , len (options ))
370+ reverse_map := make (map [int ]string )
371+ i := 0
372+ for option , rank := range options {
373+ ranks [i ] = rank
374+ reverse_map [rank ] = option
375+ i += 1
353376 }
354- return result
377+
378+ sort .Ints (ranks )
379+
380+ // normalise the ranks for counts that don't start at 1
381+ var choices []string = make ([]string , len (ranks ))
382+ for idx , rank := range ranks {
383+ choices [idx ] = reverse_map [rank ]
384+ }
385+
386+ return choices
355387}
0 commit comments