Skip to content

Commit

Permalink
Merge pull request #28 from cde-ev/publish-validation
Browse files Browse the repository at this point in the history
Publish validation
  • Loading branch information
larsesser authored Oct 5, 2023
2 parents c01b50f + e30272c commit 3cd651b
Show file tree
Hide file tree
Showing 3 changed files with 45 additions and 42 deletions.
47 changes: 15 additions & 32 deletions schulze_condorcet/schulze_condorcet.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import itertools
from gettext import gettext as _
from typing import (
Collection, Container, List, Mapping, Tuple, Sequence
)

from schulze_condorcet.util import as_vote_string, as_vote_tuples
from schulze_condorcet.util import (
as_vote_string, as_vote_tuples, validate_candidates, validate_votes
)
from schulze_condorcet.strength import winning_votes
from schulze_condorcet.types import (
Candidate, DetailedResultLevel, LinkStrength, PairwisePreference, SchulzeResult,
Expand Down Expand Up @@ -44,27 +45,6 @@ def _schulze_winners(d: Mapping[Tuple[Candidate, Candidate], int],
return winners


def _check_consistency(votes: Collection[VoteString], candidates: Sequence[Candidate]) -> None:
"""Check that the given vote strings are consistent with the provided candidates.
This means, each vote string contains exactly the given candidates, separated by
'>' and '=', and each candidate occurs in each vote string exactly once.
"""
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 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:
if candidates_set < vote_candidates_set:
raise ValueError(_("Superfluous candidate in vote string."))
else:
raise ValueError(_("Missing candidate in vote string."))
if not len(vote_candidates) == len(vote_candidates_set):
raise ValueError(_("Every candidate must occur exactly once in each vote."))


def _subindex(alist: Collection[Container[str]], element: str) -> int:
"""The element is in the list at which position in the big list.
Expand Down Expand Up @@ -125,8 +105,8 @@ def _schulze_evaluate_routine(


def schulze_evaluate(
votes: Collection[VoteString],
candidates: Sequence[Candidate],
votes: Collection[str],
candidates: Sequence[str],
strength: StrengthCallback = winning_votes
) -> VoteString:
"""Use the Schulze method to cumulate preference lists (votes) into one list (vote).
Expand Down Expand Up @@ -158,7 +138,8 @@ def schulze_evaluate(
:returns: A vote string, reflecting the overall preference.
"""
# Validate votes and candidate input to be consistent
_check_consistency(votes, candidates)
candidates = validate_candidates(candidates)
votes = validate_votes(votes, candidates)

_, result = _schulze_evaluate_routine(votes, candidates, strength)

Expand All @@ -167,8 +148,8 @@ def schulze_evaluate(


def schulze_evaluate_detailed(
votes: Collection[VoteString],
candidates: Sequence[Candidate],
votes: Collection[str],
candidates: Sequence[str],
strength: StrengthCallback = winning_votes
) -> List[DetailedResultLevel]:
"""Construct a more detailed representation of the result by adding some stats.
Expand All @@ -178,7 +159,8 @@ def schulze_evaluate_detailed(
of preference in the overall result.
"""
# Validate votes and candidate input to be consistent
_check_consistency(votes, candidates)
candidates = validate_candidates(candidates)
votes = validate_votes(votes, candidates)

counts, result = _schulze_evaluate_routine(votes, candidates, strength)

Expand Down Expand Up @@ -206,8 +188,8 @@ def schulze_evaluate_detailed(


def pairwise_preference(
votes: Collection[VoteString],
candidates: Sequence[Candidate],
votes: Collection[str],
candidates: Sequence[str],
) -> PairwisePreference:
"""Calculate the pairwise preference of all candidates from all given votes.
Expand All @@ -216,6 +198,7 @@ def pairwise_preference(
other.
"""
# Validate votes and candidate input to be consistent
_check_consistency(votes, candidates)
candidates = validate_candidates(candidates)
votes = validate_votes(votes, candidates)

return _pairwise_preference(votes, candidates)
38 changes: 29 additions & 9 deletions schulze_condorcet/util.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,47 @@
"""Offer convenient functions to convert votes and candidates into proper types.
"""Offer convenient converting and validation functions for votes and candidates.
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.
"""

import itertools
from gettext import gettext as _
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 validate_candidates(candidates: Sequence[str]) -> Sequence[Candidate]:
"""Validate a sequence of candidates.
We respect the order of the candidates, as this is also respected during evaluation
of the votes using the schulze method."""

def as_candidates(values: Sequence[str]) -> List[Candidate]:
"""Mark a row of strings as candidates.
if any(">" in candidate or "=" in candidate for candidate in candidates):
raise ValueError(_("A candidate contains a forbidden character."))
return [Candidate(candidate) for candidate in candidates]

We respect the order of the candidates, as this is also respected during evaluation
of the votes using the schulze method.

def validate_votes(votes: Collection[str], candidates: Sequence[str]) -> Collection[VoteString]:
"""Check that the given vote strings are consistent with the provided candidates.
This means, each vote string contains exactly the given candidates, separated by
'>' and '=', and each candidate occurs in each vote string exactly once.
"""
return [as_candidate(value) for value in values]
candidates = validate_candidates(candidates)
candidates_set = set(candidates)
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:
if candidates_set < vote_candidates_set:
raise ValueError(_("Superfluous candidate in vote string."))
else:
raise ValueError(_("Missing candidate in vote string."))
if not len(vote_candidates) == len(vote_candidates_set):
raise ValueError(_("Every candidate must occur exactly once in each vote."))
return [VoteString(vote) for vote in votes]


def as_vote_string(
Expand Down
2 changes: 1 addition & 1 deletion tests/test_schulze.py
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@ def test_result_order(self) -> None:
def test_util(self) -> None:
candidates = ["1", "2", "3"]
# This does only static type conversion
self.assertEqual(util.as_candidates(candidates), candidates)
self.assertEqual(util.validate_candidates(candidates), candidates)

# build the same votes, represented as string, list and tuple
vote_str_1 = "1"
Expand Down

0 comments on commit 3cd651b

Please sign in to comment.