Implement advanced rounding functionality in API

This commit is contained in:
RunasSudo 2021-01-03 00:29:41 +11:00
parent 7b52f8f06d
commit a3d79e993a
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
7 changed files with 187 additions and 58 deletions

View File

@ -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))

View File

@ -14,25 +14,39 @@
# 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/>.
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 '<Fixed {}>'.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))

View File

@ -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')

View File

@ -14,13 +14,27 @@
# 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/>.
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')

View File

@ -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')

View File

@ -15,13 +15,27 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
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')

View File

@ -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')