From a3d79e993abbdee8e34c47ce0c7a8dca6f74f665 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Sun, 3 Jan 2021 00:29:41 +1100 Subject: [PATCH] Implement advanced rounding functionality in API --- pyRCV2/numbers/fixed_js.py | 11 ++++++- pyRCV2/numbers/fixed_py.py | 51 +++++++++++++++++++------------- pyRCV2/numbers/native_js.py | 18 ++++++++++++ pyRCV2/numbers/native_py.py | 55 +++++++++++++++++++++++------------ pyRCV2/numbers/rational_js.py | 19 ++++++++++++ pyRCV2/numbers/rational_py.py | 55 +++++++++++++++++++++++------------ tests/test_numbers.py | 36 +++++++++++++++++++++++ 7 files changed, 187 insertions(+), 58 deletions(-) diff --git a/pyRCV2/numbers/fixed_js.py b/pyRCV2/numbers/fixed_js.py index c81b438..7347bfb 100644 --- a/pyRCV2/numbers/fixed_js.py +++ b/pyRCV2/numbers/fixed_js.py @@ -24,6 +24,11 @@ class Fixed: Wrapper for big.js (fixed-point arithmetic) """ + ROUND_DOWN = 0 + ROUND_HALF_UP = 1 + ROUND_HALF_EVEN = 2 + ROUND_UP = 3 + def __init__(self, val): if isinstance(val, Fixed): self.impl = val.impl @@ -60,4 +65,8 @@ class Fixed: return self.impl.lte(other.impl) def __floor__(self): - return Fixed(Math.floor(self.impl)) + return self.round(0, Fixed.ROUND_DOWN) + + def round(self, dps, mode): + """Round to the specified number of decimal places, using the ROUND_* mode specified""" + return Fixed(self.impl.round(dps, mode)) diff --git a/pyRCV2/numbers/fixed_py.py b/pyRCV2/numbers/fixed_py.py index 5c28ae3..d455d8b 100644 --- a/pyRCV2/numbers/fixed_py.py +++ b/pyRCV2/numbers/fixed_py.py @@ -14,25 +14,39 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from decimal import Decimal +import decimal +import functools import math _quantize_exp = 6 def set_dps(dps): global _quantize_exp - _quantize_exp = Decimal('10') ** -dps + _quantize_exp = decimal.Decimal('10') ** -dps + +def compatible_types(f): + @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) """ + ROUND_DOWN = decimal.ROUND_DOWN + ROUND_HALF_UP = decimal.ROUND_HALF_UP + ROUND_HALF_EVEN = decimal.ROUND_HALF_EVEN + ROUND_UP = decimal.ROUND_UP + def __init__(self, val): if isinstance(val, Fixed): self.impl = val.impl else: - self.impl = Decimal(val).quantize(_quantize_exp) + self.impl = decimal.Decimal(val).quantize(_quantize_exp) def __repr__(self): return ''.format(str(self.impl)) @@ -45,43 +59,38 @@ class Fixed: from pyRCV2.numbers import Rational return Rational(self.impl) + @compatible_types def __add__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return Fixed(self.impl + other.impl) + @compatible_types def __sub__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return Fixed(self.impl - other.impl) + @compatible_types def __mul__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return Fixed(self.impl * other.impl) + @compatible_types def __truediv__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return Fixed(self.impl / other.impl) + @compatible_types def __eq__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return self.impl == other.impl + @compatible_types def __gt__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return self.impl > other.impl + @compatible_types def __ge__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return self.impl >= other.impl + @compatible_types def __lt__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return self.impl < other.impl + @compatible_types def __le__(self, other): - if not isinstance(other, Fixed): - raise ValueError('Attempt to operate on incompatible types') return self.impl <= other.impl def __floor__(self): return Fixed(math.floor(self.impl)) + + def round(self, dps, mode): + """Round to the specified number of decimal places, using the ROUND_* mode specified""" + return Fixed(self.impl.quantize(decimal.Decimal('10') ** -dps, mode)) diff --git a/pyRCV2/numbers/native_js.py b/pyRCV2/numbers/native_js.py index ed0e0e3..a36e9eb 100644 --- a/pyRCV2/numbers/native_js.py +++ b/pyRCV2/numbers/native_js.py @@ -19,6 +19,11 @@ class Native: Wrapper for JS numbers (naive floating-point arithmetic) """ + ROUND_DOWN = 0 + ROUND_HALF_UP = 1 + ROUND_HALF_EVEN = 2 + ROUND_UP = 3 + def __init__(self, val): if isinstance(val, Native): self.impl = val.impl @@ -56,3 +61,16 @@ class Native: def __floor__(self): return Native(Math.floor(self.impl)) + + def round(self, dps, mode): + """Round to the specified number of decimal places, using the ROUND_* mode specified""" + if mode == Native.ROUND_DOWN: + return Native(Math.floor(self.impl * Math.pow(10, dps)) / Math.pow(10, dps)) + elif mode == Native.ROUND_HALF_UP: + return Native(Math.round(self.impl * Math.pow(10, dps)) / Math.pow(10, dps)) + elif mode == Native.ROUND_HALF_EVEN: + raise Exception('ROUND_HALF_EVEN is not implemented in JS Native context') + elif mode == Native.ROUND_UP: + return Native(Math.ceil(self.impl * Math.pow(10, dps)) / Math.pow(10, dps)) + else: + raise Exception('Invalid rounding mode') diff --git a/pyRCV2/numbers/native_py.py b/pyRCV2/numbers/native_py.py index cdbb764..e2a1933 100644 --- a/pyRCV2/numbers/native_py.py +++ b/pyRCV2/numbers/native_py.py @@ -14,13 +14,27 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . +import functools import math +def compatible_types(f): + @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) """ + ROUND_DOWN = 0 + ROUND_HALF_UP = 1 + ROUND_HALF_EVEN = 2 + ROUND_UP = 3 + def __init__(self, val): if isinstance(val, Native): self.impl = val.impl @@ -38,43 +52,48 @@ class Native: from pyRCV2.numbers import Rational return Rational(self.impl) + @compatible_types def __add__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return Native(self.impl + other.impl) + @compatible_types def __sub__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return Native(self.impl - other.impl) + @compatible_types def __mul__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return Native(self.impl * other.impl) + @compatible_types def __truediv__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return Native(self.impl / other.impl) + @compatible_types def __eq__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return self.impl == other.impl + @compatible_types def __gt__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return self.impl > other.impl + @compatible_types def __ge__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return self.impl >= other.impl + @compatible_types def __lt__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return self.impl < other.impl + @compatible_types def __le__(self, other): - if not isinstance(other, Native): - raise ValueError('Attempt to operate on incompatible types') return self.impl <= other.impl def __floor__(self): return Native(math.floor(self.impl)) + + def round(self, dps, mode): + """Round to the specified number of decimal places, using the ROUND_* mode specified""" + factor = 10 ** dps + if mode == Native.ROUND_DOWN: + return Native(math.floor(self.impl * factor) / factor) + elif mode == Native.ROUND_HALF_UP: + raise Exception('ROUND_HALF_UP is not implemented in Python Native context') + elif mode == Native.ROUND_HALF_EVEN: + return Native(round(self.impl * factor) / factor) + elif mode == Native.ROUND_UP: + return Native(math.ceil(self.impl * factor) / factor) + else: + raise Exception('Invalid rounding mode') diff --git a/pyRCV2/numbers/rational_js.py b/pyRCV2/numbers/rational_js.py index 02765fa..ef2e510 100644 --- a/pyRCV2/numbers/rational_js.py +++ b/pyRCV2/numbers/rational_js.py @@ -19,6 +19,11 @@ class Rational: Wrapper for BigRational.js (rational arithmetic) """ + ROUND_DOWN = 0 + ROUND_HALF_UP = 1 + ROUND_HALF_EVEN = 2 + ROUND_UP = 3 + def __init__(self, val): if isinstance(val, Rational): self.impl = val.impl @@ -66,3 +71,17 @@ class Rational: def __floor__(self): return Rational(self.impl.floor()) + + def round(self, dps, mode): + """Round to the specified number of decimal places, using the ROUND_* mode specified""" + factor = bigRat(10).pow(dps) + if mode == Rational.ROUND_DOWN: + return Rational(self.impl.multiply(factor).floor().divide(factor)) + elif mode == Rational.ROUND_HALF_UP: + return Rational(self.impl.multiply(factor).round().divide(factor)) + elif mode == Rational.ROUND_HALF_EVEN: + raise Exception('ROUND_HALF_EVEN is not implemented in JS Native context') + elif mode == Rational.ROUND_UP: + return Rational(self.impl.multiply(factor).ceil().divide(factor)) + else: + raise Exception('Invalid rounding mode') diff --git a/pyRCV2/numbers/rational_py.py b/pyRCV2/numbers/rational_py.py index 862915d..d3c1d3b 100644 --- a/pyRCV2/numbers/rational_py.py +++ b/pyRCV2/numbers/rational_py.py @@ -15,13 +15,27 @@ # along with this program. If not, see . from fractions import Fraction +import functools import math +def compatible_types(f): + @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) """ + ROUND_DOWN = 0 + ROUND_HALF_UP = 1 + ROUND_HALF_EVEN = 2 + ROUND_UP = 3 + def __init__(self, val): if isinstance(val, Rational): self.impl = val.impl @@ -47,43 +61,48 @@ class Rational: from pyRCV2.numbers import Num return Num(self.impl.numerator) / Num(self.impl.denominator) + @compatible_types def __add__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return Rational(self.impl + other.impl) + @compatible_types def __sub__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return Rational(self.impl - other.impl) + @compatible_types def __mul__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return Rational(self.impl * other.impl) + @compatible_types def __truediv__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return Rational(self.impl / other.impl) + @compatible_types def __eq__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return self.impl == other.impl + @compatible_types def __gt__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return self.impl > other.impl + @compatible_types def __ge__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return self.impl >= other.impl + @compatible_types def __lt__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return self.impl < other.impl + @compatible_types def __le__(self, other): - if not isinstance(other, Rational): - raise ValueError('Attempt to operate on incompatible types') return self.impl <= other.impl def __floor__(self): return Rational(math.floor(self.impl)) + + def round(self, dps, mode): + """Round to the specified number of decimal places, using the ROUND_* mode specified""" + factor = Fraction(10) ** dps + if mode == Rational.ROUND_DOWN: + return Rational(math.floor(self.impl * factor) / factor) + elif mode == Rational.ROUND_HALF_UP: + raise Exception('ROUND_HALF_UP is not implemented in Python Rational context') + elif mode == Rational.ROUND_HALF_EVEN: + return Rational(round(self.impl * factor) / factor) + elif mode == Rational.ROUND_UP: + return Rational(math.ceil(self.impl * factor) / factor) + else: + raise Exception('Invalid rounding mode') diff --git a/tests/test_numbers.py b/tests/test_numbers.py index 0021efe..d74ab06 100644 --- a/tests/test_numbers.py +++ b/tests/test_numbers.py @@ -56,3 +56,39 @@ test_fixed2_add_py, test_fixed2_add_js = maketst('Fixed', 2, '__add__', '356.57' test_fixed1_add_py, test_fixed1_add_js = maketst('Fixed', 1, '__add__', '356.6') test_fixed0_add_py, test_fixed0_add_js = maketst('Fixed', 0, '__add__', '356') test_rational_add_py, test_rational_add_js = maketst('Rational', 0, '__add__', '356.57') + +def maketst_round(numbers, dps, num, dps_round, mode_round, result): + def t_py(): + pyRCV2.numbers.set_numclass(getattr(pyRCV2.numbers, numbers)) + pyRCV2.numbers.set_dps(dps) + + num1 = Num(num) + assert num1.round(dps_round, getattr(num1, mode_round)) == Num(result) + + def t_js(): + ctx = py_mini_racer.MiniRacer() + + # Imports + with open('html/vendor/BigInt_BigRat-a5f89e2.min.js', 'r') as f: + ctx.eval(f.read()) + with open('html/vendor/big-6.0.0.min.js', 'r') as f: + ctx.eval(f.read()) + with open('html/vendor/sjcl-1.0.8.min.js', 'r') as f: + ctx.eval(f.read()) + with open('html/bundle.js', 'r') as f: + ctx.eval(f.read()) + + ctx.eval('py.pyRCV2.numbers.set_numclass(py.pyRCV2.numbers.{});'.format(numbers)) + ctx.eval('py.pyRCV2.numbers.set_dps({});'.format(dps)) + + ctx.eval('num1 = py.pyRCV2.numbers.Num("{}"); void(0);'.format(num)) + assert ctx.eval('num1.round({}, num1.{}).__eq__(py.pyRCV2.numbers.Num("{}"))'.format(dps_round, mode_round, result)) + + return t_py, t_js + +test_fixed_round1_py, test_fixed_round1_js = maketst_round('Fixed', 5, '3141.59', 1, 'ROUND_DOWN', '3141.5') +test_fixed_round2_py, test_fixed_round2_js = maketst_round('Fixed', 5, '3141.59', 1, 'ROUND_UP', '3141.6') +test_native_round1_py, test_native_round1_js = maketst_round('Native', 0, '3141.59', 1, 'ROUND_DOWN', '3141.5') +test_native_round2_py, test_native_round2_js = maketst_round('Native', 0, '3141.59', 1, 'ROUND_UP', '3141.6') +test_rational_round1_py, test_rational_round1_js = maketst_round('Rational', 0, '3141.59', 1, 'ROUND_DOWN', '3141.5') +test_rational_round2_py, test_rational_round2_js = maketst_round('Rational', 0, '3141.59', 1, 'ROUND_UP', '3141.6')