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