Optimisations for number handling
DRY number classes Implement __iadd__, etc. for better performance
This commit is contained in:
parent
673b531475
commit
8d7a1ea1f9
242
pyRCV2/numbers/base.py
Normal file
242
pyRCV2/numbers/base.py
Normal file
@ -0,0 +1,242 @@
|
|||||||
|
# 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:
|
||||||
|
# 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')
|
||||||
|
|
||||||
|
@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')
|
||||||
|
|
||||||
|
# Implementation of arithmetic on impls
|
||||||
|
# Subclasses must override these functions:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _add_impl(cls, i1, i2):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
@classmethod
|
||||||
|
def _sub_impl(cls, i1, i2):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
@classmethod
|
||||||
|
def _mul_impl(cls, i1, i2):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
@classmethod
|
||||||
|
def _truediv_impl(cls, i1, i2):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
|
||||||
|
@compatible_types
|
||||||
|
def __eq__(self, other):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
@compatible_types
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not (self.__eq__(other))
|
||||||
|
@compatible_types
|
||||||
|
def __gt__(self, other):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
@compatible_types
|
||||||
|
def __ge__(self, other):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
@compatible_types
|
||||||
|
def __lt__(self, other):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
@compatible_types
|
||||||
|
def __le__(self, other):
|
||||||
|
raise NotImplementedError('Method not implemented')
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
# 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 __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
|
||||||
|
|
||||||
|
@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))
|
@ -1,5 +1,5 @@
|
|||||||
# pyRCV2: Preferential vote counting
|
# pyRCV2: Preferential vote counting
|
||||||
# Copyright © 2020 Lee Yingtong Li (RunasSudo)
|
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -14,61 +14,65 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from pyRCV2.numbers.base import BaseNum, compatible_types
|
||||||
|
|
||||||
Big.DP = 6
|
Big.DP = 6
|
||||||
|
|
||||||
def set_dps(dps):
|
def set_dps(dps):
|
||||||
Big.DP = dps
|
Big.DP = dps
|
||||||
|
|
||||||
class Fixed:
|
class Fixed(BaseNum):
|
||||||
"""
|
"""
|
||||||
Wrapper for big.js (fixed-point arithmetic)
|
Wrapper for big.js (fixed-point arithmetic)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROUND_DOWN = 0
|
@classmethod
|
||||||
ROUND_HALF_UP = 1
|
def _to_impl(cls, value):
|
||||||
ROUND_HALF_EVEN = 2
|
"""Implements BaseNum._to_impl"""
|
||||||
ROUND_UP = 3
|
return Big(value).round(Big.DP)
|
||||||
|
|
||||||
def __init__(self, val):
|
|
||||||
if isinstance(val, Fixed):
|
|
||||||
self.impl = val.impl
|
|
||||||
else:
|
|
||||||
self.impl = Big(val).round(Big.DP)
|
|
||||||
|
|
||||||
def pp(self, dp):
|
def pp(self, dp):
|
||||||
"""Pretty print to specified number of decimal places"""
|
"""Implements BaseNum.pp"""
|
||||||
return self.impl.toFixed(dp)
|
return self.impl.toFixed(dp)
|
||||||
|
|
||||||
def to_rational(self):
|
@classmethod
|
||||||
"""Convert to an instance of Rational"""
|
def _add_impl(cls, i1, i2):
|
||||||
from pyRCV2.numbers import Rational
|
"""Implements BaseNum._add_impl"""
|
||||||
return Rational(self.impl.toString())
|
return i1.plus(i2)
|
||||||
|
@classmethod
|
||||||
def __add__(self, other):
|
def _sub_impl(cls, i1, i2):
|
||||||
return Fixed(self.impl.plus(other.impl))
|
"""Implements BaseNum._sub_impl"""
|
||||||
def __sub__(self, other):
|
return i1.minus(i2)
|
||||||
return Fixed(self.impl.minus(other.impl))
|
@classmethod
|
||||||
def __mul__(self, other):
|
def _mul_impl(cls, i1, i2):
|
||||||
return Fixed(self.impl.times(other.impl))
|
"""Implements BaseNum._mul_impl"""
|
||||||
def __div__(self, other):
|
return i1.times(i2)
|
||||||
return Fixed(self.impl.div(other.impl))
|
@classmethod
|
||||||
|
def _truediv_impl(cls, i1, i2):
|
||||||
|
"""Implements BaseNum._truediv_impl"""
|
||||||
|
return i1.div(i2)
|
||||||
|
|
||||||
|
@compatible_types
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
"""Implements BaseNum.__eq__"""
|
||||||
return self.impl.eq(other.impl)
|
return self.impl.eq(other.impl)
|
||||||
def __ne__(self, other):
|
@compatible_types
|
||||||
return not self.impl.eq(other.impl)
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
|
"""Implements BaseNum.__gt__"""
|
||||||
return self.impl.gt(other.impl)
|
return self.impl.gt(other.impl)
|
||||||
|
@compatible_types
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
|
"""Implements BaseNum.__ge__"""
|
||||||
return self.impl.gte(other.impl)
|
return self.impl.gte(other.impl)
|
||||||
|
@compatible_types
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
|
"""Implements BaseNum.__lt__"""
|
||||||
return self.impl.lt(other.impl)
|
return self.impl.lt(other.impl)
|
||||||
|
@compatible_types
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
|
"""Implements BaseNum.__le__"""
|
||||||
return self.impl.lte(other.impl)
|
return self.impl.lte(other.impl)
|
||||||
|
|
||||||
def __floor__(self):
|
|
||||||
return self.round(0, Fixed.ROUND_DOWN)
|
|
||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""Round to the specified number of decimal places, using the ROUND_* mode specified"""
|
"""Implements BaseNum.round"""
|
||||||
return Fixed(self.impl.round(dps, mode))
|
return Fixed(self.impl.round(dps, mode))
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# pyRCV2: Preferential vote counting
|
# pyRCV2: Preferential vote counting
|
||||||
# Copyright © 2020 Lee Yingtong Li (RunasSudo)
|
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -14,9 +14,9 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from pyRCV2.numbers.base import BasePyNum, compatible_types
|
||||||
|
|
||||||
import decimal
|
import decimal
|
||||||
import functools
|
|
||||||
import math
|
|
||||||
|
|
||||||
_quantize_exp = 6
|
_quantize_exp = 6
|
||||||
|
|
||||||
@ -24,76 +24,23 @@ def set_dps(dps):
|
|||||||
global _quantize_exp
|
global _quantize_exp
|
||||||
_quantize_exp = decimal.Decimal('10') ** -dps
|
_quantize_exp = decimal.Decimal('10') ** -dps
|
||||||
|
|
||||||
def compatible_types(f):
|
class Fixed(BasePyNum):
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(self, other):
|
|
||||||
if not isinstance(other, Fixed):
|
|
||||||
raise ValueError('Attempt to operate on incompatible types')
|
|
||||||
return f(self, other)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
class Fixed:
|
|
||||||
"""
|
"""
|
||||||
Wrapper for Python Decimal (for fixed-point arithmetic)
|
Wrapper for Python Decimal (for fixed-point arithmetic)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_py_class = decimal.Decimal # For BasePyNum
|
||||||
|
|
||||||
ROUND_DOWN = decimal.ROUND_DOWN
|
ROUND_DOWN = decimal.ROUND_DOWN
|
||||||
ROUND_HALF_UP = decimal.ROUND_HALF_UP
|
ROUND_HALF_UP = decimal.ROUND_HALF_UP
|
||||||
ROUND_HALF_EVEN = decimal.ROUND_HALF_EVEN
|
ROUND_HALF_EVEN = decimal.ROUND_HALF_EVEN
|
||||||
ROUND_UP = decimal.ROUND_UP
|
ROUND_UP = decimal.ROUND_UP
|
||||||
|
|
||||||
def __init__(self, val):
|
@classmethod
|
||||||
if isinstance(val, Fixed):
|
def _to_impl(cls, value):
|
||||||
self.impl = val.impl
|
"""Overrides BasePyNum._to_impl"""
|
||||||
else:
|
return decimal.Decimal(value).quantize(_quantize_exp)
|
||||||
self.impl = decimal.Decimal(val).quantize(_quantize_exp)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Fixed {}>'.format(str(self.impl))
|
|
||||||
def pp(self, dp):
|
|
||||||
"""Pretty print to specified number of decimal places"""
|
|
||||||
return format(self.impl, '.{}f'.format(dp))
|
|
||||||
|
|
||||||
def to_rational(self):
|
|
||||||
"""Convert to an instance of Rational"""
|
|
||||||
from pyRCV2.numbers import Rational
|
|
||||||
return Rational(self.impl)
|
|
||||||
|
|
||||||
@compatible_types
|
|
||||||
def __add__(self, other):
|
|
||||||
return Fixed(self.impl + other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __sub__(self, other):
|
|
||||||
return Fixed(self.impl - other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __mul__(self, other):
|
|
||||||
return Fixed(self.impl * other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __truediv__(self, other):
|
|
||||||
return Fixed(self.impl / other.impl)
|
|
||||||
|
|
||||||
@compatible_types
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.impl == other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __ne__(self, other):
|
|
||||||
return self.impl != other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __gt__(self, other):
|
|
||||||
return self.impl > other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __ge__(self, other):
|
|
||||||
return self.impl >= other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __lt__(self, other):
|
|
||||||
return self.impl < other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __le__(self, other):
|
|
||||||
return self.impl <= other.impl
|
|
||||||
|
|
||||||
def __floor__(self):
|
|
||||||
return Fixed(math.floor(self.impl))
|
|
||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""Round to the specified number of decimal places, using the ROUND_* mode specified"""
|
"""Implements BaseNum.round"""
|
||||||
return Fixed(self.impl.quantize(decimal.Decimal('10') ** -dps, mode))
|
return Fixed(self.impl.quantize(decimal.Decimal('10') ** -dps, mode))
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# pyRCV2: Preferential vote counting
|
# pyRCV2: Preferential vote counting
|
||||||
# Copyright © 2020 Lee Yingtong Li (RunasSudo)
|
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -14,65 +14,77 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
class Native:
|
from pyRCV2.numbers.base import BaseNum, compatible_types
|
||||||
|
|
||||||
|
class Native(BaseNum):
|
||||||
"""
|
"""
|
||||||
Wrapper for JS numbers (naive floating-point arithmetic)
|
Wrapper for JS numbers (naive floating-point arithmetic)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROUND_DOWN = 0
|
@classmethod
|
||||||
ROUND_HALF_UP = 1
|
def _to_impl(cls, value):
|
||||||
ROUND_HALF_EVEN = 2
|
"""Implements BaseNum._to_impl"""
|
||||||
ROUND_UP = 3
|
return parseFloat(value)
|
||||||
|
|
||||||
def __init__(self, val):
|
|
||||||
if isinstance(val, Native):
|
|
||||||
self.impl = val.impl
|
|
||||||
else:
|
|
||||||
self.impl = parseFloat(val)
|
|
||||||
|
|
||||||
def pp(self, dp):
|
def pp(self, dp):
|
||||||
"""Pretty print to specified number of decimal places"""
|
"""Implements BaseNum.pp"""
|
||||||
return self.impl.toFixed(dp)
|
return self.impl.toFixed(dp)
|
||||||
|
|
||||||
def to_rational(self):
|
@classmethod
|
||||||
"""Convert to an instance of Rational"""
|
def _add_impl(cls, i1, i2):
|
||||||
from pyRCV2.numbers import Rational
|
"""Implements BaseNum._add_impl"""
|
||||||
return Rational(self.impl)
|
return i1 + i2
|
||||||
|
@classmethod
|
||||||
def __add__(self, other):
|
def _sub_impl(cls, i1, i2):
|
||||||
return Native(self.impl + other.impl)
|
"""Implements BaseNum._sub_impl"""
|
||||||
def __sub__(self, other):
|
return i1 - i2
|
||||||
return Native(self.impl - other.impl)
|
@classmethod
|
||||||
def __mul__(self, other):
|
def _mul_impl(cls, i1, i2):
|
||||||
return Native(self.impl * other.impl)
|
"""Implements BaseNum._mul_impl"""
|
||||||
def __div__(self, other):
|
return i1 * i2
|
||||||
return Native(self.impl / other.impl)
|
@classmethod
|
||||||
|
def _truediv_impl(cls, i1, i2):
|
||||||
|
"""Implements BaseNum._truediv_impl"""
|
||||||
|
return i1 / i2
|
||||||
|
|
||||||
|
@compatible_types
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
"""Implements BaseNum.__eq__"""
|
||||||
return self.impl == other.impl
|
return self.impl == other.impl
|
||||||
|
@compatible_types
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
|
"""Overrides BaseNum.__ne__"""
|
||||||
return self.impl != other.impl
|
return self.impl != other.impl
|
||||||
|
@compatible_types
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
|
"""Implements BaseNum.__gt__"""
|
||||||
return self.impl > other.impl
|
return self.impl > other.impl
|
||||||
|
@compatible_types
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
|
"""Implements BaseNum.__ge__"""
|
||||||
return self.impl >= other.impl
|
return self.impl >= other.impl
|
||||||
|
@compatible_types
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
|
"""Implements BaseNum.__lt__"""
|
||||||
return self.impl < other.impl
|
return self.impl < other.impl
|
||||||
|
@compatible_types
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
|
"""Implements BaseNum.__le__"""
|
||||||
return self.impl <= other.impl
|
return self.impl <= other.impl
|
||||||
|
|
||||||
def __floor__(self):
|
def __floor__(self):
|
||||||
|
"""Overrides BaseNum.__floor__"""
|
||||||
return Native(Math.floor(self.impl))
|
return Native(Math.floor(self.impl))
|
||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""Round to the specified number of decimal places, using the ROUND_* mode specified"""
|
"""Implements BaseNum.round"""
|
||||||
if mode == Native.ROUND_DOWN:
|
if mode == Native.ROUND_DOWN:
|
||||||
return Native(Math.floor(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
return Native(Math.floor(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||||
elif mode == Native.ROUND_HALF_UP:
|
elif mode == Native.ROUND_HALF_UP:
|
||||||
return Native(Math.round(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
return Native(Math.round(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||||
elif mode == Native.ROUND_HALF_EVEN:
|
elif mode == Native.ROUND_HALF_EVEN:
|
||||||
raise Exception('ROUND_HALF_EVEN is not implemented in JS Native context')
|
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
||||||
elif mode == Native.ROUND_UP:
|
elif mode == Native.ROUND_UP:
|
||||||
return Native(Math.ceil(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
return Native(Math.ceil(self.impl * Math.pow(10, dps)) / Math.pow(10, dps))
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid rounding mode')
|
raise ValueError('Invalid rounding mode')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# pyRCV2: Preferential vote counting
|
# pyRCV2: Preferential vote counting
|
||||||
# Copyright © 2020 Lee Yingtong Li (RunasSudo)
|
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -14,89 +14,27 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import functools
|
from pyRCV2.numbers.base import BasePyNum, compatible_types
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
def compatible_types(f):
|
class Native(BasePyNum):
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(self, other):
|
|
||||||
if not isinstance(other, Native):
|
|
||||||
raise ValueError('Attempt to operate on incompatible types')
|
|
||||||
return f(self, other)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
class Native:
|
|
||||||
"""
|
"""
|
||||||
Wrapper for Python float (naive floating-point arithmetic)
|
Wrapper for Python float (naive floating-point arithmetic)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROUND_DOWN = 0
|
_py_class = float # For BasePyNum
|
||||||
ROUND_HALF_UP = 1
|
|
||||||
ROUND_HALF_EVEN = 2
|
|
||||||
ROUND_UP = 3
|
|
||||||
|
|
||||||
def __init__(self, val):
|
|
||||||
if isinstance(val, Native):
|
|
||||||
self.impl = val.impl
|
|
||||||
else:
|
|
||||||
self.impl = float(val)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Native {}>'.format(str(self.impl))
|
|
||||||
def pp(self, dp):
|
|
||||||
"""Pretty print to specified number of decimal places"""
|
|
||||||
return format(self.impl, '.{}f'.format(dp))
|
|
||||||
|
|
||||||
def to_rational(self):
|
|
||||||
"""Convert to an instance of Rational"""
|
|
||||||
from pyRCV2.numbers import Rational
|
|
||||||
return Rational(self.impl)
|
|
||||||
|
|
||||||
@compatible_types
|
|
||||||
def __add__(self, other):
|
|
||||||
return Native(self.impl + other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __sub__(self, other):
|
|
||||||
return Native(self.impl - other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __mul__(self, other):
|
|
||||||
return Native(self.impl * other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __truediv__(self, other):
|
|
||||||
return Native(self.impl / other.impl)
|
|
||||||
|
|
||||||
@compatible_types
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.impl == other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __ne__(self, other):
|
|
||||||
return self.impl != other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __gt__(self, other):
|
|
||||||
return self.impl > other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __ge__(self, other):
|
|
||||||
return self.impl >= other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __lt__(self, other):
|
|
||||||
return self.impl < other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __le__(self, other):
|
|
||||||
return self.impl <= other.impl
|
|
||||||
|
|
||||||
def __floor__(self):
|
|
||||||
return Native(math.floor(self.impl))
|
|
||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""Round to the specified number of decimal places, using the ROUND_* mode specified"""
|
"""Implements BaseNum.round"""
|
||||||
factor = 10 ** dps
|
factor = 10 ** dps
|
||||||
if mode == Native.ROUND_DOWN:
|
if mode == Native.ROUND_DOWN:
|
||||||
return Native(math.floor(self.impl * factor) / factor)
|
return Native(math.floor(self.impl * factor) / factor)
|
||||||
elif mode == Native.ROUND_HALF_UP:
|
elif mode == Native.ROUND_HALF_UP:
|
||||||
raise Exception('ROUND_HALF_UP is not implemented in Python Native context')
|
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Native context')
|
||||||
elif mode == Native.ROUND_HALF_EVEN:
|
elif mode == Native.ROUND_HALF_EVEN:
|
||||||
return Native(round(self.impl * factor) / factor)
|
return Native(round(self.impl * factor) / factor)
|
||||||
elif mode == Native.ROUND_UP:
|
elif mode == Native.ROUND_UP:
|
||||||
return Native(math.ceil(self.impl * factor) / factor)
|
return Native(math.ceil(self.impl * factor) / factor)
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid rounding mode')
|
raise ValueError('Invalid rounding mode')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# pyRCV2: Preferential vote counting
|
# pyRCV2: Preferential vote counting
|
||||||
# Copyright © 2020 Lee Yingtong Li (RunasSudo)
|
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -14,76 +14,75 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
class Rational:
|
from pyRCV2.numbers.base import BaseNum, compatible_types
|
||||||
|
|
||||||
|
class Rational(BaseNum):
|
||||||
"""
|
"""
|
||||||
Wrapper for BigRational.js (rational arithmetic)
|
Wrapper for BigRational.js (rational arithmetic)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROUND_DOWN = 0
|
@classmethod
|
||||||
ROUND_HALF_UP = 1
|
def _to_impl(cls, value):
|
||||||
ROUND_HALF_EVEN = 2
|
"""Implements BaseNum._to_impl"""
|
||||||
ROUND_UP = 3
|
return bigRat(value)
|
||||||
|
|
||||||
def __init__(self, val):
|
|
||||||
if isinstance(val, Rational):
|
|
||||||
self.impl = val.impl
|
|
||||||
else:
|
|
||||||
self.impl = bigRat(val)
|
|
||||||
|
|
||||||
def pp(self, dp):
|
def pp(self, dp):
|
||||||
"""
|
"""Implements BaseNum.pp"""
|
||||||
Pretty print to specified number of decimal places
|
# FIXME: This will fail for numbers which cannot be represented as a JavaScript number
|
||||||
This will fail for numbers which cannot be represented as a JavaScript number
|
|
||||||
"""
|
|
||||||
return self.impl.valueOf().toFixed(dp)
|
return self.impl.valueOf().toFixed(dp)
|
||||||
|
|
||||||
def to_rational(self):
|
@classmethod
|
||||||
return self
|
def _add_impl(cls, i1, i2):
|
||||||
|
"""Implements BaseNum._add_impl"""
|
||||||
def to_num(self):
|
return i1.add(i2)
|
||||||
"""
|
@classmethod
|
||||||
Convert to an instance of Num
|
def _sub_impl(cls, i1, i2):
|
||||||
"""
|
"""Implements BaseNum._sub_impl"""
|
||||||
from pyRCV2.numbers import Num
|
return i1.subtract(i2)
|
||||||
__pragma__('opov')
|
@classmethod
|
||||||
return Num(self.impl.numerator.toString()) / Num(self.impl.denominator.toString())
|
def _mul_impl(cls, i1, i2):
|
||||||
__pragma__('noopov')
|
"""Implements BaseNum._mul_impl"""
|
||||||
|
return i1.multiply(i2)
|
||||||
def __add__(self, other):
|
@classmethod
|
||||||
return Rational(self.impl.add(other.impl))
|
def _truediv_impl(cls, i1, i2):
|
||||||
def __sub__(self, other):
|
"""Implements BaseNum._truediv_impl"""
|
||||||
return Rational(self.impl.subtract(other.impl))
|
return i1.divide(i2)
|
||||||
def __mul__(self, other):
|
|
||||||
return Rational(self.impl.multiply(other.impl))
|
|
||||||
def __div__(self, other):
|
|
||||||
return Rational(self.impl.divide(other.impl))
|
|
||||||
|
|
||||||
|
@compatible_types
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
"""Implements BaseNum.__eq__"""
|
||||||
return self.impl.equals(other.impl)
|
return self.impl.equals(other.impl)
|
||||||
def __ne__(self, other):
|
@compatible_types
|
||||||
return not self.impl.equals(other.impl)
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
|
"""Implements BaseNum.__gt__"""
|
||||||
return self.impl.greater(other.impl)
|
return self.impl.greater(other.impl)
|
||||||
|
@compatible_types
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
|
"""Implements BaseNum.__ge__"""
|
||||||
return self.impl.greaterOrEquals(other.impl)
|
return self.impl.greaterOrEquals(other.impl)
|
||||||
|
@compatible_types
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
|
"""Implements BaseNum.__lt__"""
|
||||||
return self.impl.lesser(other.impl)
|
return self.impl.lesser(other.impl)
|
||||||
|
@compatible_types
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
|
"""Implements BaseNum.__le__"""
|
||||||
return self.impl.lesserOrEquals(other.impl)
|
return self.impl.lesserOrEquals(other.impl)
|
||||||
|
|
||||||
def __floor__(self):
|
def __floor__(self):
|
||||||
|
"""Overrides BaseNum.__floor__"""
|
||||||
return Rational(self.impl.floor())
|
return Rational(self.impl.floor())
|
||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""Round to the specified number of decimal places, using the ROUND_* mode specified"""
|
"""Implements BaseNum.round"""
|
||||||
factor = bigRat(10).pow(dps)
|
factor = bigRat(10).pow(dps)
|
||||||
if mode == Rational.ROUND_DOWN:
|
if mode == Rational.ROUND_DOWN:
|
||||||
return Rational(self.impl.multiply(factor).floor().divide(factor))
|
return Rational(self.impl.multiply(factor).floor().divide(factor))
|
||||||
elif mode == Rational.ROUND_HALF_UP:
|
elif mode == Rational.ROUND_HALF_UP:
|
||||||
return Rational(self.impl.multiply(factor).round().divide(factor))
|
return Rational(self.impl.multiply(factor).round().divide(factor))
|
||||||
elif mode == Rational.ROUND_HALF_EVEN:
|
elif mode == Rational.ROUND_HALF_EVEN:
|
||||||
raise Exception('ROUND_HALF_EVEN is not implemented in JS Native context')
|
raise NotImplementedError('ROUND_HALF_EVEN is not implemented in JS Native context')
|
||||||
elif mode == Rational.ROUND_UP:
|
elif mode == Rational.ROUND_UP:
|
||||||
return Rational(self.impl.multiply(factor).ceil().divide(factor))
|
return Rational(self.impl.multiply(factor).ceil().divide(factor))
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid rounding mode')
|
raise ValueError('Invalid rounding mode')
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# pyRCV2: Preferential vote counting
|
# pyRCV2: Preferential vote counting
|
||||||
# Copyright © 2020 Lee Yingtong Li (RunasSudo)
|
# Copyright © 2020–2021 Lee Yingtong Li (RunasSudo)
|
||||||
#
|
#
|
||||||
# This program is free software: you can redistribute it and/or modify
|
# 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
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -14,98 +14,33 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License
|
# 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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from pyRCV2.numbers.base import BasePyNum, compatible_types
|
||||||
|
|
||||||
from fractions import Fraction
|
from fractions import Fraction
|
||||||
import functools
|
|
||||||
import math
|
import math
|
||||||
|
|
||||||
def compatible_types(f):
|
class Rational(BasePyNum):
|
||||||
@functools.wraps(f)
|
|
||||||
def wrapper(self, other):
|
|
||||||
if not isinstance(other, Rational):
|
|
||||||
raise ValueError('Attempt to operate on incompatible types')
|
|
||||||
return f(self, other)
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
class Rational:
|
|
||||||
"""
|
"""
|
||||||
Wrapper for Python Fraction (rational arithmetic)
|
Wrapper for Python Fraction (rational arithmetic)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ROUND_DOWN = 0
|
_py_class = Fraction # For BasePyNum
|
||||||
ROUND_HALF_UP = 1
|
|
||||||
ROUND_HALF_EVEN = 2
|
|
||||||
ROUND_UP = 3
|
|
||||||
|
|
||||||
def __init__(self, val):
|
|
||||||
if isinstance(val, Rational):
|
|
||||||
self.impl = val.impl
|
|
||||||
else:
|
|
||||||
self.impl = Fraction(val)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
return '<Rational {}>'.format(str(self.impl))
|
|
||||||
def pp(self, dp):
|
def pp(self, dp):
|
||||||
"""
|
"""Overrides BasePyNum.pp"""
|
||||||
Pretty print to specified number of decimal places
|
|
||||||
"""
|
|
||||||
# TODO: Work out if there is a better way of doing this
|
# TODO: Work out if there is a better way of doing this
|
||||||
return format(float(self.impl), '.{}f'.format(dp))
|
return format(float(self.impl), '.{}f'.format(dp))
|
||||||
|
|
||||||
def to_rational(self):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def to_num(self):
|
|
||||||
"""
|
|
||||||
Convert to an instance of Num
|
|
||||||
"""
|
|
||||||
from pyRCV2.numbers import Num
|
|
||||||
return Num(self.impl.numerator) / Num(self.impl.denominator)
|
|
||||||
|
|
||||||
@compatible_types
|
|
||||||
def __add__(self, other):
|
|
||||||
return Rational(self.impl + other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __sub__(self, other):
|
|
||||||
return Rational(self.impl - other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __mul__(self, other):
|
|
||||||
return Rational(self.impl * other.impl)
|
|
||||||
@compatible_types
|
|
||||||
def __truediv__(self, other):
|
|
||||||
return Rational(self.impl / other.impl)
|
|
||||||
|
|
||||||
@compatible_types
|
|
||||||
def __eq__(self, other):
|
|
||||||
return self.impl == other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __ne__(self, other):
|
|
||||||
return self.impl != other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __gt__(self, other):
|
|
||||||
return self.impl > other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __ge__(self, other):
|
|
||||||
return self.impl >= other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __lt__(self, other):
|
|
||||||
return self.impl < other.impl
|
|
||||||
@compatible_types
|
|
||||||
def __le__(self, other):
|
|
||||||
return self.impl <= other.impl
|
|
||||||
|
|
||||||
def __floor__(self):
|
|
||||||
return Rational(math.floor(self.impl))
|
|
||||||
|
|
||||||
def round(self, dps, mode):
|
def round(self, dps, mode):
|
||||||
"""Round to the specified number of decimal places, using the ROUND_* mode specified"""
|
"""Implements BaseNum.round"""
|
||||||
factor = Fraction(10) ** dps
|
factor = Fraction(10) ** dps
|
||||||
if mode == Rational.ROUND_DOWN:
|
if mode == Rational.ROUND_DOWN:
|
||||||
return Rational(math.floor(self.impl * factor) / factor)
|
return Rational(math.floor(self.impl * factor) / factor)
|
||||||
elif mode == Rational.ROUND_HALF_UP:
|
elif mode == Rational.ROUND_HALF_UP:
|
||||||
raise Exception('ROUND_HALF_UP is not implemented in Python Rational context')
|
raise NotImplementedError('ROUND_HALF_UP is not implemented in Python Rational context')
|
||||||
elif mode == Rational.ROUND_HALF_EVEN:
|
elif mode == Rational.ROUND_HALF_EVEN:
|
||||||
return Rational(round(self.impl * factor) / factor)
|
return Rational(round(self.impl * factor) / factor)
|
||||||
elif mode == Rational.ROUND_UP:
|
elif mode == Rational.ROUND_UP:
|
||||||
return Rational(math.ceil(self.impl * factor) / factor)
|
return Rational(math.ceil(self.impl * factor) / factor)
|
||||||
else:
|
else:
|
||||||
raise Exception('Invalid rounding mode')
|
raise ValueError('Invalid rounding mode')
|
||||||
|
Reference in New Issue
Block a user