Implement advanced rounding functionality in API
This commit is contained in:
parent
7b52f8f06d
commit
a3d79e993a
@ -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))
|
||||
|
@ -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))
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
@ -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')
|
||||
|
Reference in New Issue
Block a user