254 lines
7.3 KiB
Python
254 lines
7.3 KiB
Python
# pyRCV2: Preferential vote counting
|
|
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
__pragma__ = lambda x: None
|
|
is_py = False
|
|
__pragma__('skip')
|
|
is_py = True
|
|
import functools
|
|
import math
|
|
__pragma__('noskip')
|
|
|
|
def compatible_types(f):
|
|
if is_py:
|
|
__pragma__('skip')
|
|
import os
|
|
if 'PYTEST_CURRENT_TEST' in os.environ:
|
|
@functools.wraps(f)
|
|
def wrapper(self, other):
|
|
if not isinstance(other, self.__class__):
|
|
raise ValueError('Attempt to operate on incompatible types')
|
|
return f(self, other)
|
|
return wrapper
|
|
else:
|
|
return f
|
|
__pragma__('noskip')
|
|
else: # pragma: no cover
|
|
# FIXME: Do we need to perform type checking in JS?
|
|
return f
|
|
|
|
class BaseNum:
|
|
__slots__ = ['impl'] # Optimisation to reduce overhead of initialising new object
|
|
|
|
# These enum values may be overridden in subclasses depending on underlying library
|
|
ROUND_DOWN = 0
|
|
ROUND_HALF_UP = 1
|
|
ROUND_HALF_EVEN = 2
|
|
ROUND_UP = 3
|
|
|
|
def __init__(self, value):
|
|
if isinstance(value, self.__class__):
|
|
self.impl = value.impl
|
|
else:
|
|
self.impl = self._to_impl(value)
|
|
|
|
@classmethod
|
|
def _to_impl(cls, value):
|
|
"""
|
|
Internal use: Convert the given value to an impl
|
|
Subclasses must override this method
|
|
"""
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
|
|
@classmethod
|
|
def _from_impl(cls, impl):
|
|
"""Internal use: Return an instance directly from the given impl without performing checks"""
|
|
if is_py:
|
|
obj = cls.__new__(cls)
|
|
else:
|
|
# Transcrypt's __new__ (incorrectly) calls the constructor
|
|
obj = __pragma__('js', '{}', 'Object.create (cls, {__class__: {value: cls, enumerable: true}})')
|
|
obj.impl = impl
|
|
return obj
|
|
|
|
def pp(self, dp):
|
|
"""
|
|
Pretty print to specified number of decimal places
|
|
Subclasses must override this method
|
|
"""
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
|
|
# Implementation of arithmetic on impls
|
|
# Subclasses must override these functions:
|
|
|
|
@classmethod
|
|
def _add_impl(cls, i1, i2):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
@classmethod
|
|
def _sub_impl(cls, i1, i2):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
@classmethod
|
|
def _mul_impl(cls, i1, i2):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
@classmethod
|
|
def _truediv_impl(cls, i1, i2):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
|
|
@compatible_types
|
|
def __eq__(self, other):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
@compatible_types
|
|
def __ne__(self, other):
|
|
return not (self.__eq__(other))
|
|
@compatible_types
|
|
def __gt__(self, other):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
@compatible_types
|
|
def __ge__(self, other):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
@compatible_types
|
|
def __lt__(self, other):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
@compatible_types
|
|
def __le__(self, other):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
|
|
def __pow__(self, power):
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
|
|
def round(self, dps, mode):
|
|
"""
|
|
Round to the specified number of decimal places, using the ROUND_* mode specified
|
|
Subclasses must override this method
|
|
"""
|
|
raise NotImplementedError('Method not implemented') # pragma: no cover
|
|
|
|
# Implement various data model functions based on _*_impl
|
|
|
|
@compatible_types
|
|
def __add__(self, other):
|
|
return self._from_impl(self._add_impl(self.impl, other.impl))
|
|
@compatible_types
|
|
def __sub__(self, other):
|
|
return self._from_impl(self._sub_impl(self.impl, other.impl))
|
|
@compatible_types
|
|
def __mul__(self, other):
|
|
return self._from_impl(self._mul_impl(self.impl, other.impl))
|
|
@compatible_types
|
|
def __truediv__(self, other):
|
|
return self._from_impl(self._truediv_impl(self.impl, other.impl))
|
|
|
|
@compatible_types
|
|
def __iadd__(self, other):
|
|
self.impl = self._add_impl(self.impl, other.impl)
|
|
return self
|
|
@compatible_types
|
|
def __isub__(self, other):
|
|
self.impl = self._sub_impl(self.impl, other.impl)
|
|
return self
|
|
@compatible_types
|
|
def __imul__(self, other):
|
|
self.impl = self._mul_impl(self.impl, other.impl)
|
|
return self
|
|
@compatible_types
|
|
def __itruediv__(self, other):
|
|
self.impl = self._truediv_impl(self.impl, other.impl)
|
|
return self
|
|
|
|
def __idiv__(self, other):
|
|
# For Transcrypt
|
|
return self.__itruediv__(other)
|
|
|
|
def __floor__(self):
|
|
return self.round(0, self.ROUND_DOWN)
|
|
|
|
class BasePyNum(BaseNum):
|
|
"""Helper class for Num wrappers of Python objects that already implement overloading"""
|
|
|
|
_py_class = None # Subclasses must specify
|
|
|
|
@classmethod
|
|
def _to_impl(cls, value):
|
|
"""Implements BaseNum._to_impl"""
|
|
return cls._py_class(value)
|
|
|
|
def pp(self, dp):
|
|
"""Implements BaseNum.pp"""
|
|
return format(self.impl, '.{}f'.format(dp))
|
|
|
|
@classmethod
|
|
def _add_impl(cls, i1, i2):
|
|
"""Implements BaseNum._add_impl"""
|
|
return i1 + i2
|
|
@classmethod
|
|
def _sub_impl(cls, i1, i2):
|
|
"""Implements BaseNum._sub_impl"""
|
|
return i1 - i2
|
|
@classmethod
|
|
def _mul_impl(cls, i1, i2):
|
|
"""Implements BaseNum._mul_impl"""
|
|
return i1 * i2
|
|
@classmethod
|
|
def _truediv_impl(cls, i1, i2):
|
|
"""Implements BaseNum._truediv_impl"""
|
|
return i1 / i2
|
|
|
|
@compatible_types
|
|
def __eq__(self, other):
|
|
"""Implements BaseNum.__eq__"""
|
|
return self.impl == other.impl
|
|
@compatible_types
|
|
def __ne__(self, other):
|
|
"""Overrides BaseNum.__ne__"""
|
|
return self.impl != other.impl
|
|
@compatible_types
|
|
def __gt__(self, other):
|
|
"""Implements BaseNum.__gt__"""
|
|
return self.impl > other.impl
|
|
@compatible_types
|
|
def __ge__(self, other):
|
|
"""Implements BaseNum.__ge__"""
|
|
return self.impl >= other.impl
|
|
@compatible_types
|
|
def __lt__(self, other):
|
|
"""Implements BaseNum.__lt__"""
|
|
return self.impl < other.impl
|
|
@compatible_types
|
|
def __le__(self, other):
|
|
"""Implements BaseNum.__le__"""
|
|
return self.impl <= other.impl
|
|
|
|
def __pow__(self, power):
|
|
"""Implements BaseNum.__pow__"""
|
|
return self._from_impl(self.impl ** power)
|
|
|
|
@compatible_types
|
|
def __iadd__(self, other):
|
|
"""Overrides BaseNum.__iadd__"""
|
|
self.impl += other.impl
|
|
return self
|
|
@compatible_types
|
|
def __isub__(self, other):
|
|
"""Overrides BaseNum.__isub__"""
|
|
self.impl -= other.impl
|
|
return self
|
|
@compatible_types
|
|
def __imul__(self, other):
|
|
"""Overrides BaseNum.__imul__"""
|
|
self.impl *= other.impl
|
|
return self
|
|
@compatible_types
|
|
def __itruediv__(self, other):
|
|
"""Overrides BaseNum.__itruediv__"""
|
|
self.impl /= other.impl
|
|
return self
|
|
|
|
def __floor__(self):
|
|
return self._from_impl(math.floor(self.impl))
|
|
|
|
def __repr__(self):
|
|
return '<{} {}>'.format(self.__class__.__name__, str(self.impl))
|