Source code for trivoting.election.trichotomous_ballot
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Iterable, Collection
from trivoting.election.alternative import Alternative
[docs]
class AbstractTrichotomousBallot(ABC):
"""
Abstract base class for a trichotomous ballot.
A trichotomous ballot partitions alternatives into approved and disapproved sets,
with the implicit third category being neutral or unknown.
Subclasses must implement the `approved` and `disapproved` properties.
"""
@property
@abstractmethod
def approved(self) -> Collection[Alternative]:
pass
@property
@abstractmethod
def disapproved(self) -> Collection[Alternative]:
pass
[docs]
class TrichotomousBallot(AbstractTrichotomousBallot):
"""
Represents a mutable trichotomous ballot, where alternatives are categorized into approved, disapproved, or
implicitly neutral.
Parameters
----------
approved : Iterable[Alternative], optional
Approved alternatives.
disapproved : Iterable[Alternative], optional
Disapproved alternatives.
Attributes
----------
approved : set[Alternative]
The alternatives the voter approves of.
disapproved : set[Alternative]
The alternatives the voter disapproves of.
"""
def __init__(self, *, approved: Iterable[Alternative] = None, disapproved: Iterable[Alternative] = None):
if approved is None:
self._approved = set()
else:
self._approved = set(approved)
if disapproved is None:
self._disapproved = set()
else:
self._disapproved = set(disapproved)
AbstractTrichotomousBallot.__init__(self)
@property
def approved(self) -> set[Alternative]:
"""Set of approved alternatives."""
return self._approved
@approved.setter
def approved(self, value: Iterable[Alternative]):
self._approved = set(value)
@property
def disapproved(self) -> set[Alternative]:
"""Set of disapproved alternatives."""
return self._disapproved
@disapproved.setter
def disapproved(self, value: Iterable[Alternative]):
self._disapproved = set(value)
[docs]
def add_approved(self, alt: Alternative) -> None:
"""
Add an alternative to the approved set.
Parameters
----------
alt : Alternative
The alternative to approve.
"""
self.approved.add(alt)
[docs]
def add_disapproved(self, alt: Alternative) -> None:
"""
Add an alternative to the disapproved set.
Parameters
----------
alt : Alternative
The alternative to disapprove.
"""
self.disapproved.add(alt)
[docs]
def freeze(self) -> FrozenTrichotomousBallot:
"""
Return an immutable version of this ballot.
Returns
-------
FrozenTrichotomousBallot
A frozen (immutable) copy of this ballot.
"""
return FrozenTrichotomousBallot(
approved=self.approved,
disapproved=self.disapproved,
)
def __contains__(self, item):
"""
Check if an alternative is in either the approved or disapproved sets.
Parameters
----------
item : Alternative
Returns
-------
bool
"""
return item in self.approved or item in self.disapproved
def __len__(self):
"""
Return the total number of alternatives in the ballot (both approved and disapproved).
Returns
-------
int
"""
return len(self.approved) + len(self.disapproved)
def __str__(self):
return f"{{{self.approved}}} // {{{self.disapproved}}}"
def __repr__(self):
return self.__str__()
def __eq__(self, other):
if isinstance(other, TrichotomousBallot):
return self.approved == other.approved and self.disapproved == other.disapproved
return NotImplemented
def __lt__(self, other):
if isinstance(other, TrichotomousBallot):
return self.approved < other.approved
return NotImplemented
[docs]
class FrozenTrichotomousBallot(AbstractTrichotomousBallot):
"""
Represents an immutable trichotomous ballot using tuples for storage.
This version is suitable for hashing and storing in sets or as keys in dictionaries.
Parameters
----------
approved : Iterable[Alternative], optional
Approved alternatives.
disapproved : Iterable[Alternative], optional
Disapproved alternatives.
Attributes
----------
approved : tuple[Alternative, ...]
The alternatives the voter approves of.
disapproved : tuple[Alternative, ...]
The alternatives the voter disapproves of.
"""
def __init__(self, *, approved: Iterable[Alternative] = None, disapproved: Iterable[Alternative] = None):
if approved is None:
self._approved = tuple()
else:
self._approved = tuple(approved)
if disapproved is None:
self._disapproved = tuple()
else:
self._disapproved = tuple(disapproved)
AbstractTrichotomousBallot.__init__(self)
@property
def approved(self) -> tuple[Alternative, ...]:
"""Tuple of approved alternatives."""
return self._approved
@property
def disapproved(self) -> tuple[Alternative, ...]:
"""Tuple of disapproved alternatives."""
return self._disapproved
def __contains__(self, item):
"""
Check if an alternative is in either the approved or disapproved sets.
Parameters
----------
item : Alternative
Returns
-------
bool
"""
return item in self.approved or item in self.disapproved
def __len__(self):
"""
Return the total number of alternatives in the ballot (both approved and disapproved).
Returns
-------
int
"""
return len(self.approved) + len(self.disapproved)
def __str__(self):
return f"{{{self.approved}}} // {{{self.disapproved}}}"
def __repr__(self):
return self.__str__()
def __eq__(self, other):
if isinstance(other, FrozenTrichotomousBallot):
return self.approved == other.approved and self.disapproved == other.disapproved
return NotImplemented
def __lt__(self, other):
if isinstance(other, FrozenTrichotomousBallot):
return (self.approved, self.disapproved) < (other.approved, other.disapproved)
return NotImplemented
def __hash__(self):
return hash((self.approved, self.disapproved))