Skip to content

Commit

Permalink
Merge pull request #27 from cde-ev/io-interface
Browse files Browse the repository at this point in the history
Io interface
  • Loading branch information
larsesser authored Jan 13, 2022
2 parents b39384e + ff2b3f4 commit b73be77
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 27 deletions.
30 changes: 5 additions & 25 deletions schulze_condorcet/schulze_condorcet.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
Collection, Container, List, Mapping, Tuple, Sequence
)

from schulze_condorcet.util import as_vote_string, as_vote_tuples
from schulze_condorcet.strength import winning_votes
from schulze_condorcet.types import (
Candidate, DetailedResultLevel, LinkStrength, PairwisePreference, SchulzeResult,
VoteList, StrengthCallback, VoteString
StrengthCallback, VoteString
)


Expand Down Expand Up @@ -43,27 +44,6 @@ def _schulze_winners(d: Mapping[Tuple[Candidate, Candidate], int],
return winners


def _split_vote(vote: VoteString) -> VoteList:
"""Split a vote string into its candidates."""
return (
tuple(
tuple(
Candidate(candidate) for candidate in level.split('=')
) for level in vote.split('>')
)
)


def _split_votes(votes: Collection[VoteString]) -> List[VoteList]:
"""Split a list of vote strings into their candidates."""
return [_split_vote(vote) for vote in votes]


def _recombine_vote(vote: VoteList) -> VoteString:
"""Construct a vote string from its candidates, opposite of _split_vote."""
return VoteString(">".join("=".join(level) for level in vote))


def _check_consistency(votes: Collection[VoteString], candidates: Sequence[Candidate]) -> None:
"""Check that the given vote strings are consistent with the provided candidates.
Expand All @@ -73,7 +53,7 @@ def _check_consistency(votes: Collection[VoteString], candidates: Sequence[Candi
if any(">" in candidate or "=" in candidate for candidate in candidates):
raise ValueError(_("A candidate contains a forbidden character."))
candidates_set = set(candidates)
for vote in _split_votes(votes):
for vote in as_vote_tuples(votes):
vote_candidates = [c for c in itertools.chain.from_iterable(vote)]
vote_candidates_set = set(vote_candidates)
if candidates_set != vote_candidates_set:
Expand Down Expand Up @@ -102,7 +82,7 @@ def _pairwise_preference(
) -> PairwisePreference:
"""Calculate the pairwise preference of all candidates from all given votes."""
counts = {(x, y): 0 for x in candidates for y in candidates}
for vote in _split_votes(votes):
for vote in as_vote_tuples(votes):
for x in candidates:
for y in candidates:
if _subindex(vote, x) < _subindex(vote, y):
Expand Down Expand Up @@ -183,7 +163,7 @@ def schulze_evaluate(
_, result = _schulze_evaluate_routine(votes, candidates, strength)

# Construct a vote string reflecting the overall preference
return _recombine_vote(result)
return as_vote_string(result)


def schulze_evaluate_detailed(
Expand Down
6 changes: 4 additions & 2 deletions schulze_condorcet/types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import (
Dict, List, NewType, Protocol, Tuple, TypedDict, Sequence
Dict, List, NewType, Protocol, Tuple, TypedDict
)


Expand All @@ -11,7 +11,9 @@
Candidate = NewType('Candidate', str)
# A single vote, split into separate levels accordingly to (descending) preference.
# All candidates at the same level (in the same inner tuple) have equal preference.
VoteList = Sequence[Sequence[Candidate]]
VoteTuple = Tuple[Tuple[Candidate, ...], ...]
# We accept VoteLists instead of VoteTuples for convenience.
VoteList = List[List[Candidate]]
# How many voters prefer the first candidate over the second candidate.
PairwisePreference = Dict[Tuple[Candidate, Candidate], int]
# The link strength between two candidates.
Expand Down
61 changes: 61 additions & 0 deletions schulze_condorcet/util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""Offer convenient functions to convert votes and candidates into proper types.
This especially includes marking strings as Candidate or VoteString, and converting the
string-based representations of votes (like 'a>b=c=d>e') into the level-based
representations of votes (like [[a], [b, c, d], [e]]) and vice versa.
"""

from typing import Collection, List, Sequence, Union

from schulze_condorcet.types import Candidate, VoteList, VoteString, VoteTuple


def as_candidate(value: str) -> Candidate:
"""Mark a string as candidate."""
return Candidate(value)


def as_candidates(values: Sequence[str]) -> List[Candidate]:
"""Mark a row of strings as candidates.
We respect the order of the candidates, as this is also respected during evaluation
of the votes using the schulze method.
"""
return [as_candidate(value) for value in values]


def as_vote_string(
value: Union[VoteList, VoteTuple]
) -> VoteString:
"""Convert a level-based representation of the vote into its string representation."""
if isinstance(value, (list, tuple)):
return VoteString(">".join("=".join(level) for level in value))
else:
raise NotImplementedError(value)


def as_vote_strings(
values: Union[Collection[VoteList], Collection[VoteTuple]]
) -> List[VoteString]:
"""Convert each value into a string representation of the vote."""
return [as_vote_string(value) for value in values]


def as_vote_tuple(
value: Union[str, VoteString]
) -> VoteTuple:
"""Convert a string representation of a vote into its level-based representation."""
return (
tuple(
tuple(
Candidate(candidate) for candidate in level.split('=')
) for level in value.split('>')
)
)


def as_vote_tuples(
values: Union[Collection[str], Collection[VoteString]]
) -> List[VoteTuple]:
"""Convert string representations of votes into their level-based representations."""
return [as_vote_tuple(value) for value in values]
30 changes: 30 additions & 0 deletions tests/test_schulze.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import unittest

from schulze_condorcet import schulze_evaluate, schulze_evaluate_detailed
import schulze_condorcet.util as util
from schulze_condorcet.strength import margin, winning_votes
from schulze_condorcet.types import Candidate, DetailedResultLevel as DRL, VoteString

Expand Down Expand Up @@ -517,6 +518,35 @@ def test_result_order(self) -> None:
condensed = schulze_evaluate(reference_votes, candidates)
self.assertEqual("1=0>2", condensed)

def test_util(self) -> None:
candidates = ["1", "2", "3"]
# This does only static type conversion
self.assertEqual(util.as_candidates(candidates), candidates)

# build the same votes, represented as string, list and tuple
vote_str_1 = "1"
vote_str_2 = "1=2>3"
vote_list_1 = [[Candidate("1")]]
vote_list_2 = [[Candidate("1"), Candidate("2")], [Candidate("3")]]
vote_tuple_1 = ((Candidate("1"),),)
vote_tuple_2 = ((Candidate("1"), Candidate("2")), (Candidate("3"),))
vote_str_list = [vote_str_1, vote_str_2]
vote_list_list = [vote_list_1, vote_list_2]
vote_tuple_list = [vote_tuple_1, vote_tuple_2]

self.assertEqual(vote_str_list, util.as_vote_strings(vote_list_list))
self.assertEqual(vote_str_list, util.as_vote_strings(vote_tuple_list))

# mixing different representations of votes is confusing and therefore not
# recommended and forbidden by static type checking.
# However, we ensure the outcome is right nonetheless.
self.assertEqual(
2*vote_str_list,
util.as_vote_strings([*vote_list_list, *vote_tuple_list]) # type:ignore
)

self.assertEqual(vote_tuple_list, util.as_vote_tuples(vote_str_list))


if __name__ == '__main__':
unittest.main()

0 comments on commit b73be77

Please sign in to comment.