rcv-site/src/InstantRunoff.hs
Jack Wines cee339a8ed
beam database (unfinished), overloadedRecordDot, fourmolu, relude
also one fix where the "skip voting and see results" link is broken.
2025-08-25 07:54:36 -07:00

72 lines
2.5 KiB
Haskell

module InstantRunoff where
import qualified Data.List as L
import qualified Data.List.NonEmpty as LN
import qualified Data.Map.Strict as M
import qualified Data.Set as S
import qualified Data.Maybe as M
import qualified Data.Foldable as F
solve :: forall a. Ord a => [a] -> LN.NonEmpty (LN.NonEmpty a) -> [S.Set a]
solve candidates votes = reverse . (consIfAny notVotedForCandidates) . rank $ votes
where
consIfAny :: S.Set a -> [S.Set a] -> [S.Set a]
consIfAny [] xs = xs
consIfAny x xs = x : xs
-- candidates not on any submitted ballot
notVotedForCandidates :: S.Set a
notVotedForCandidates =
S.difference (S.fromList candidates)
-- all candidates on any submited ballot
. S.unions . LN.map (S.fromList . LN.toList) $ votes
rank :: forall a. Ord a => LN.NonEmpty (LN.NonEmpty a) -> [S.Set a]
rank votes =
maybe [losers] ((losers :) . rank)
. filterVotes (`S.notMember` losers) $ votes
where
-- losers in terms of the first preference on ballot
firstChoiceLosers :: S.Set a
firstChoiceLosers = M.keysSet . M.filter ((==) . L.minimum . M.elems $ voteCounts) $ voteCounts
-- in the event of multiple first choice losers,
-- count how many ballots prefer one choice over another
losers :: S.Set a
losers = S.fromList
. maybe [] (smallestSubsetBy preferred)
. LN.nonEmpty . S.toList
$ firstChoiceLosers
preferred :: a -> a -> Ordering
preferred x0 x1 = compare (numPrefers x0) (numPrefers x1)
where
numPrefers x = length . LN.filter (== Just x) $ preferences
-- which candidate each voter prefers
preferences :: LN.NonEmpty (Maybe a)
preferences = LN.map (F.find (`S.member` [x0, x1])) $ votes
voteCounts :: M.Map a Word
voteCounts =
M.unionsWith (+)
. (M.fromSet (const 0) candidates :)
. map (`M.singleton` 1)
. LN.toList .
LN.map LN.head $ votes
-- set of all candidates on anybody's ballot
candidates :: S.Set a
candidates = S.unions . LN.map (S.fromList . LN.toList) $ votes
filterVotes :: (a -> Bool) -> LN.NonEmpty (LN.NonEmpty a) -> Maybe (LN.NonEmpty (LN.NonEmpty a))
filterVotes f = InstantRunoff.catMaybes . LN.map (LN.nonEmpty . LN.filter f)
catMaybes :: LN.NonEmpty (Maybe a) -> Maybe (LN.NonEmpty a)
catMaybes = LN.nonEmpty . M.catMaybes . LN.toList
-- smallest by the ordering function, not by size
smallestSubsetBy :: forall t. (t -> t -> Ordering) -> LN.NonEmpty t -> [t]
smallestSubsetBy f xs = LN.filter ((== EQ) . f (F.minimumBy f xs)) . LN.sortBy f $ xs