InstantRunoff cleanup
This commit is contained in:
parent
ffb200c7e9
commit
8732fbc43d
1 changed files with 35 additions and 22 deletions
|
|
@ -4,61 +4,74 @@ import qualified Data.List.NonEmpty as LN
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
import qualified Data.Maybe as M
|
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 :: 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
|
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
|
-- candidates not on any submitted ballot
|
||||||
notVotedForCandidates :: S.Set a
|
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 :: 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
|
where
|
||||||
|
|
||||||
|
-- losers in terms of the first preference on ballot
|
||||||
firstChoiceLosers :: S.Set a
|
firstChoiceLosers :: S.Set a
|
||||||
firstChoiceLosers = M.keysSet . M.filter ((==) . L.minimum . M.elems $ voteCounts) $ voteCounts
|
firstChoiceLosers = M.keysSet . M.filter ((==) . L.minimum . M.elems $ voteCounts) $ voteCounts
|
||||||
|
|
||||||
-- in the event of multiple first choice losers,
|
-- in the event of multiple first choice losers,
|
||||||
-- count how many ballots prefer one choice over another
|
-- count how many ballots prefer one choice over another
|
||||||
losers :: S.Set a
|
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 :: a -> a -> Ordering
|
||||||
preferred x0 x1 = compare (numMatch x0) (numMatch x1)
|
preferred x0 x1 = compare (numPrefers x0) (numPrefers x1)
|
||||||
where
|
where
|
||||||
numMatch x = L.length . L.filter (== Just x) $ preferences
|
numPrefers x = length . LN.filter (== Just x) $ preferences
|
||||||
|
|
||||||
preferences :: [Maybe a]
|
-- which candidate each voter prefers
|
||||||
preferences = map (ballotPreference . LN.toList) . LN.toList $ votes
|
preferences :: LN.NonEmpty (Maybe a)
|
||||||
|
preferences = LN.map (F.find (`S.member` [x0, x1])) $ votes
|
||||||
ballotPreference :: [a] -> Maybe a
|
|
||||||
ballotPreference = L.find (`S.member` [x0, x1])
|
|
||||||
|
|
||||||
voteCounts :: M.Map a Word
|
voteCounts :: M.Map a Word
|
||||||
voteCounts =
|
voteCounts =
|
||||||
M.unionsWith (+)
|
M.unionsWith (+)
|
||||||
-- If there's a candidate with no first-place votes we need to include that
|
. (candidatesStartingPoint :)
|
||||||
. (candidatesMinimumVoteCounts :)
|
|
||||||
. map (`M.singleton` 1)
|
. map (`M.singleton` 1)
|
||||||
. LN.toList .
|
. LN.toList .
|
||||||
LN.map LN.head $ votes
|
LN.map LN.head $ votes
|
||||||
|
|
||||||
candidatesMinimumVoteCounts :: M.Map a Word
|
-- every candidate starts with zero votes
|
||||||
candidatesMinimumVoteCounts = (M.fromSet (const 0) (allVotedFor 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 :: (a -> Bool) -> LN.NonEmpty (LN.NonEmpty a) -> Maybe (LN.NonEmpty (LN.NonEmpty a))
|
||||||
filterVotes f = catMaybes . LN.map (LN.nonEmpty . LN.filter f)
|
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 (Maybe a) -> Maybe (LN.NonEmpty a)
|
||||||
catMaybes = LN.nonEmpty . M.catMaybes . LN.toList
|
catMaybes = LN.nonEmpty . M.catMaybes . LN.toList
|
||||||
|
|
||||||
smallestSubset :: (t -> t -> Ordering) -> [t] -> [t]
|
-- smallest by the ordering function, not by size
|
||||||
smallestSubset f = last . takeWhile (\xs -> (f (head xs) (last xs)) == O.EQ) . tail . L.inits . L.sortBy f
|
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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue