InstantRunoff cleanup

This commit is contained in:
Jack Wines 2023-07-09 01:48:26 -07:00
parent ffb200c7e9
commit 8732fbc43d

View file

@ -4,61 +4,74 @@ 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.Ord as O
import qualified Data.Foldable as F
solve :: forall a. Ord a => [a] -> LN.NonEmpty (LN.NonEmpty a) -> [S.Set a]
solve candidates votes = (reverse solved) ++ if (S.null notVotedForCandidates) then [] else [notVotedForCandidates]
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) . S.unions $ solved
notVotedForCandidates =
S.difference (S.fromList candidates)
-- all candidates on any submited ballot
. S.unions . LN.map (S.fromList . LN.toList) $ votes
solved = rank votes
rank :: forall a. Ord a => LN.NonEmpty (LN.NonEmpty a) -> [S.Set a]
rank votes = maybe (L.singleton . M.keysSet $ voteCounts) ((losers :) . rank) . filterVotes (`S.notMember` losers) $ votes
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 . smallestSubset preferred . S.toList $ firstChoiceLosers
losers = S.fromList
. maybe [] (smallestSubsetBy preferred)
. LN.nonEmpty . S.toList
$ firstChoiceLosers
preferred :: a -> a -> Ordering
preferred x0 x1 = compare (numMatch x0) (numMatch x1)
preferred x0 x1 = compare (numPrefers x0) (numPrefers x1)
where
numMatch x = L.length . L.filter (== Just x) $ preferences
numPrefers x = length . LN.filter (== Just x) $ preferences
preferences :: [Maybe a]
preferences = map (ballotPreference . LN.toList) . LN.toList $ votes
ballotPreference :: [a] -> Maybe a
ballotPreference = L.find (`S.member` [x0, x1])
-- 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 (+)
-- If there's a candidate with no first-place votes we need to include that
. (candidatesMinimumVoteCounts :)
. (candidatesStartingPoint :)
. map (`M.singleton` 1)
. LN.toList .
LN.map LN.head $ votes
candidatesMinimumVoteCounts :: M.Map a Word
candidatesMinimumVoteCounts = (M.fromSet (const 0) (allVotedFor votes))
-- every candidate starts with zero votes
-- there are occasionally candidates with no first choice votes at all
-- we need to know about them
candidatesStartingPoint :: M.Map a Word
candidatesStartingPoint =
M.fromSet (const 0)
-- set of all candidates on anybody's ballot
. 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 = catMaybes . LN.map (LN.nonEmpty . LN.filter f)
allVotedFor :: Ord a => LN.NonEmpty (LN.NonEmpty a) -> S.Set a
allVotedFor = S.unions . map (S.fromList . LN.toList) . LN.toList
catMaybes :: LN.NonEmpty (Maybe a) -> Maybe (LN.NonEmpty a)
catMaybes = LN.nonEmpty . M.catMaybes . LN.toList
smallestSubset :: (t -> t -> Ordering) -> [t] -> [t]
smallestSubset f = last . takeWhile (\xs -> (f (head xs) (last xs)) == O.EQ) . tail . L.inits . L.sortBy f
-- 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