Source code for pabutools.election.ballot.ordinalballot

"""
Ordinal ballots, i.e., ballots in which the voters order the projects given their preferences.
"""

from __future__ import annotations

from abc import ABC, abstractmethod
from collections.abc import Collection, Iterable

from pabutools.election.ballot.ballot import FrozenBallot, Ballot, AbstractBallot
from pabutools.election.instance import Project


[docs] class AbstractOrdinalBallot(AbstractBallot, ABC, Collection[Project]): """ Abstract class for ordinal ballots. Essentially used for typing purposes. """
[docs] @abstractmethod def position(self, project: Project) -> int: """ Returns the position of a project in the ordinal ballot. Parameters ---------- project : :py:class:`~pabutools.election.instance.Project` The project. Returns ------- int The position of the project. """
[docs] class FrozenOrdinalBallot(tuple[Project], FrozenBallot, AbstractOrdinalBallot): """ Frozen ordinal ballot, that is, a ballot in which the voter has ordered some projects according to their preferences. It derives from the Python class `tuple` and can be used as one. Parameters ---------- init: Iterable[:py:class:`~pabutools.election.instance.Project`], optional Iterable of :py:class:`~pabutools.election.instance.Project` used to initialise the tuple. In case an :py:class:`~pabutools.election.ballot.ballot.AbstractBallot` object is passed, the additional attributes are also copied (except if the corresponding keyword arguments have been given). Defaults to `()`. name : str, optional The identifier of the ballot. Defaults to `""`. meta : dict, optional Additional information concerning the ballot, stored in a dictionary. Keys and values are typically strings. Could for instance store the gender of the voter, their location etc. Defaults to `dict()`. Attributes ---------- name : str The identifier of the ballot. meta : dict Additional information concerning the ballot, stored in a dictionary. Keys and values are typically strings. Could for instance store the gender of the voter, their location etc. """
[docs] def position(self, project: Project) -> int: return self.index(project)
def __init__( self, init: Iterable[Project] = (), name: str | None = None, meta: dict | None = None, ) -> None: tuple.__init__(self) if name is None: if isinstance(init, AbstractBallot): name = init.name else: name = "" if meta is None: if isinstance(init, AbstractBallot): meta = init.meta else: meta = dict() FrozenBallot.__init__(self, name=name, meta=meta) AbstractOrdinalBallot.__init__(self, name=name, meta=meta) def __new__( cls, iterable: Collection[Project] = (), name: str = "", meta: dict | None = None, ): if len(set(iterable)) != len(iterable): raise ValueError( "Some projects are repeated in {}, this is not a valid ordinal ballot.".format( iterable ) ) return tuple.__new__(cls, tuple(iterable)) def __hash__(self): return tuple.__hash__(self)
[docs] class OrdinalBallot(dict, Ballot, AbstractOrdinalBallot): """ Ordinal ballot, that is, a ballot in which the voter has ordered some projects according to their preferences. It behaves as an ordered set (implemented using Python `dict` for technical reasons). The convention is that the elements are presented from the most preferred one to the least preferred one. Parameters ---------- init: Iterable[:py:class:`~pabutools.election.instance.Project`], optional Iterable of :py:class:`~pabutools.election.instance.Project` used to initialise the ballot. In case an :py:class:`~pabutools.election.ballot.ballot.AbstractBallot` object is passed, the additional attributes are also copied (except if the corresponding keyword arguments have been given). Defaults to `()`. name : str, optional The identifier of the ballot. Defaults to `""`. meta : dict, optional Additional information concerning the ballot, stored in a dictionary. Keys and values are typically strings. Could for instance store the gender of the voter, their location etc. Defaults to `dict()`. Attributes ---------- name : str The identifier of the ballot. meta : dict Additional information concerning the ballot, stored in a dictionary. Keys and values are typically strings. Could for instance store the gender of the voter, their location etc. """
[docs] def position(self, project: Project) -> int: return self.index(project)
def __init__( self, init: Iterable[Project] = (), name: str | None = None, meta: dict | None = None, ) -> None: if name is None: if isinstance(init, AbstractBallot): name = init.name else: name = "" if meta is None: if isinstance(init, AbstractBallot): meta = init.meta else: meta = dict() dict.__init__(self, {e: None for e in init}) Ballot.__init__(self, name=name, meta=meta) AbstractOrdinalBallot.__init__(self, name=name, meta=meta)
[docs] def append(self, project: Project) -> None: """ Appends a project to the order. If the project is already present, its position does not change. Parameters ---------- project : :py:class:`~pabutools.election.instance.Project` The project to append. """ self[project] = None
def __add__(self, other): if not isinstance(other, OrdinalBallot): raise TypeError("Only ordinal ballots can be added to ordinal ballots") result = OrdinalBallot(self, name=self.name, meta=self.meta) for e in other: result[e] = None return result def __reversed__(self): rev_generator = dict.__reversed__(self) res = OrdinalBallot(name=self.name, meta=self.meta) for key in rev_generator: res.append(key) return res
[docs] def index(self, project: Project) -> int: """ Returns the index of the project given as argument by looping through all the projects until finding it. If the required project is not found, a `ValueError` is raised. Parameters ---------- project : :py:class:`~pabutools.election.instance.Project` The project to append. Returns ------- int The index of the project in the order. """ i = 0 for e in self: if e == project: return i i += 1 raise ValueError("{} is not in the ballot".format(project))
[docs] def at_index(self, index: int) -> Project: """ Returns the project at index `index`, or None . A `ValueError` is raised if the index is invalid. Parameters ---------- index : int The index. Returns ------- :py:class:`~pabutools.election.instance.Project` The project at position `index`. """ i = 0 for e in self: if i == index: return e i += 1 raise ValueError( "Index {} invalid for ordinal ballot of length {}.".format( index, len(self) ) )
[docs] def frozen(self) -> FrozenOrdinalBallot: """ Returns the frozen ordinal ballot (that is hashable) corresponding to the ballot. Returns ------- FrozenOrdinalBallot The frozen ordinal ballot. """ return FrozenOrdinalBallot(self)
def __eq__(self, other) -> bool: if not isinstance(other, OrdinalBallot): return False if len(self) != len(other): return False for e1, e2 in zip(self, other): if e1 != e2: return False return True def __ne__(self, other) -> bool: return not self.__eq__(other) def __lt__(self, other) -> bool: if not isinstance(other, OrdinalBallot): raise TypeError("'<' can only be used between two ordinal ballots") for e1, e2 in zip(self, other): if e1 < e2: return True elif e2 < e1: return False return len(self) < len(other) def __le__(self, other) -> bool: if not isinstance(other, OrdinalBallot): raise TypeError("'<' can only be used between two ordinal ballots") for e1, e2 in zip(self, other): if e1 < e2: return True elif e2 < e1: return False return len(self) <= len(other) def __repr__(self) -> str: return list(self.keys()).__repr__() def __str__(self) -> str: return list(self.keys()).__str__() # This allows dict method returning copies of a dict to work @classmethod def _wrap_methods(cls, names): def wrap_method_closure(name): def inner(self, *args): result = getattr(super(cls, self), name)(*args) if isinstance(result, dict) and not isinstance(result, cls): result = cls(result, name=self.name, meta=self.meta) return result inner.fn_name = name setattr(cls, name, inner) for n in names: wrap_method_closure(n)
OrdinalBallot._wrap_methods(["copy", "__ior__", "__or__", "__ror__"])