474: Actually support floor division and modulo r=hgrecco
Floor division is currently broken in pint. It is implemented like the normal division, just calling the floor division operator on the magnitude.

This is wrong. Floor division with units serves an important use case: imagine the question "I have a 1 m long log, how may pieces of 1 in can I make from that?". This question has a clear answer (it's 39), but pint gives the weird answer "1 meter / inch", which is just wrong.

Python gives a clear definition of what is expected for the divmod function, which calculates floor division and the modulo: "For floating point numbers the result is (q, a % b), where q is usually math.floor(a / b) but may be 1 less than that. In any case q * b + a % b is very close to a, if a % b is non-zero it has the same sign as b, and 0 <= abs(a % b) < abs(b)".

This statement also works well in the united case. The implications are: a // b must be unitless, because the floor of a united value does not make sense. a % b must have the same unit (or at least dimensionality) as a (and b), as otherwise the above sum looses sense.

This pull request implements and tests all of the above.
This commit is contained in:
bors[bot] 2017-03-31 04:00:21 +00:00
commit db336319ce
2 changed files with 143 additions and 30 deletions

View File

@ -794,15 +794,6 @@ class _Quantity(SharedRegistryObject):
def __truediv__(self, other):
return self._mul_div(other, operator.truediv)
def __ifloordiv__(self, other):
if not isinstance(self._magnitude, ndarray):
return self._mul_div(other, operator.floordiv, units_op=operator.itruediv)
else:
return self._imul_div(other, operator.ifloordiv, units_op=operator.itruediv)
def __floordiv__(self, other):
return self._mul_div(other, operator.floordiv, units_op=operator.truediv)
def __rtruediv__(self, other):
try:
other_magnitude = _to_magnitude(other, self.force_ndarray)
@ -816,25 +807,78 @@ class _Quantity(SharedRegistryObject):
self = self.to_root_units()
return self.__class__(other_magnitude / self._magnitude, 1 / self._units)
def __rfloordiv__(self, other):
try:
other_magnitude = _to_magnitude(other, self.force_ndarray)
except TypeError:
return NotImplemented
no_offset_units_self = len(self._get_non_multiplicative_units())
if not self._ok_for_muldiv(no_offset_units_self):
raise OffsetUnitCalculusError(self._units, '')
elif no_offset_units_self == 1 and len(self._units) == 1:
self = self.to_root_units()
return self.__class__(other_magnitude // self._magnitude, 1 / self._units)
__div__ = __truediv__
__rdiv__ = __rtruediv__
__idiv__ = __itruediv__
def __ifloordiv__(self, other):
if self._check(other):
self._magnitude //= other.to(self._units)._magnitude
elif self.dimensionless:
self._magnitude = self.to('')._magnitude // other
else:
raise DimensionalityError(self._units, 'dimensionless')
self._units = UnitsContainer({})
return self
def __floordiv__(self, other):
if self._check(other):
magnitude = self._magnitude // other.to(self._units)._magnitude
elif self.dimensionless:
magnitude = self.to('')._magnitude // other
else:
raise DimensionalityError(self._units, 'dimensionless')
return self.__class__(magnitude, UnitsContainer({}))
def __rfloordiv__(self, other):
if self._check(other):
magnitude = other._magnitude // self.to(other._units)._magnitude
elif self.dimensionless:
magnitude = other // self.to('')._magnitude
else:
raise DimensionalityError(self._units, 'dimensionless')
return self.__class__(magnitude, UnitsContainer({}))
def __imod__(self, other):
if not self._check(other):
other = self.__class__(other, UnitsContainer({}))
self._magnitude %= other.to(self._units)._magnitude
return self
def __mod__(self, other):
if not self._check(other):
other = self.__class__(other, UnitsContainer({}))
magnitude = self._magnitude % other.to(self._units)._magnitude
return self.__class__(magnitude, self._units)
def __rmod__(self, other):
if self._check(other):
magnitude = other._magnitude % self.to(other._units)._magnitude
return self.__class__(magnitude, other._units)
elif self.dimensionless:
magnitude = other % self.to('')._magnitude
return self.__class__(magnitude, UnitsContainer({}))
else:
raise DimensionalityError(self._units, 'dimensionless')
def __divmod__(self, other):
if not self._check(other):
other = self.__class__(other, UnitsContainer({}))
q, r = divmod(self._magnitude, other.to(self._units)._magnitude)
return (self.__class__(q, UnitsContainer({})),
self.__class__(r, self._units))
def __rdivmod__(self, other):
if self._check(other):
q, r = divmod(other._magnitude, self.to(other._units)._magnitude)
unit = other._units
elif self.dimensionless:
q, r = divmod(other, self.to('')._magnitude)
unit = UnitsContainer({})
else:
raise DimensionalityError(self._units, 'dimensionless')
return (self.__class__(q, UnitsContainer({})), self.__class__(r, unit))
def __ipow__(self, other):
if not isinstance(self._magnitude, ndarray):
return self.__pow__(other)

View File

@ -216,6 +216,11 @@ class TestQuantity(QuantityTestCase):
self.assertEqual(self.Q_(1, 'meter')/self.Q_(1, 'meter'), 1)
self.assertEqual((self.Q_(1, 'meter')/self.Q_(1, 'mm')).to(''), 1000)
self.assertEqual(self.Q_(10) // self.Q_(360, 'degree'), 1)
self.assertEqual(self.Q_(400, 'degree') // self.Q_(2 * math.pi), 1)
self.assertEqual(self.Q_(400, 'degree') // (2 * math.pi), 1)
self.assertEqual(7 // self.Q_(360, 'degree'), 1)
def test_offset(self):
self.assertQuantityAlmostEqual(self.Q_(0, 'kelvin').to('kelvin'), self.Q_(0, 'kelvin'))
self.assertQuantityAlmostEqual(self.Q_(0, 'degC').to('kelvin'), self.Q_(273.15, 'kelvin'))
@ -444,14 +449,76 @@ class TestQuantityBasicMath(QuantityTestCase):
func(op.itruediv, '4.2*meter', '10*inch', '0.42*meter/inch', unit)
def _test_quantity_floordiv(self, unit, func):
func(op.floordiv, unit * 10.0, '4.2*meter', '2/meter', unit)
func(op.floordiv, '24*meter', unit * 10.0, '2*meter', unit)
func(op.floordiv, '10*meter', '4.2*inch', '2*meter/inch', unit)
a = self.Q_('10*meter')
b = self.Q_('3*second')
self.assertRaises(DimensionalityError, op.floordiv, a, b)
self.assertRaises(DimensionalityError, op.floordiv, 3, b)
self.assertRaises(DimensionalityError, op.floordiv, a, 3)
self.assertRaises(DimensionalityError, op.ifloordiv, a, b)
self.assertRaises(DimensionalityError, op.ifloordiv, 3, b)
self.assertRaises(DimensionalityError, op.ifloordiv, a, 3)
func(op.floordiv, unit * 10.0, '4.2*meter/meter', 2, unit)
func(op.floordiv, '10*meter', '4.2*inch', 93, unit)
def _test_quantity_mod(self, unit, func):
a = self.Q_('10*meter')
b = self.Q_('3*second')
self.assertRaises(DimensionalityError, op.mod, a, b)
self.assertRaises(DimensionalityError, op.mod, 3, b)
self.assertRaises(DimensionalityError, op.mod, a, 3)
self.assertRaises(DimensionalityError, op.imod, a, b)
self.assertRaises(DimensionalityError, op.imod, 3, b)
self.assertRaises(DimensionalityError, op.imod, a, 3)
func(op.mod, unit * 10.0, '4.2*meter/meter', 1.6, unit)
def _test_quantity_ifloordiv(self, unit, func):
func(op.ifloordiv, 10.0, '4.2*meter', '2/meter', unit)
func(op.ifloordiv, '24*meter', 10.0, '2*meter', unit)
func(op.ifloordiv, '10*meter', '4.2*inch', '2*meter/inch', unit)
func(op.ifloordiv, 10.0, '4.2*meter/meter', 2, unit)
func(op.ifloordiv, '10*meter', '4.2*inch', 93, unit)
def _test_quantity_divmod_one(self, a, b):
if isinstance(a, string_types):
a = self.Q_(a)
if isinstance(b, string_types):
b = self.Q_(b)
q, r = divmod(a, b)
self.assertEqual(q, a // b)
self.assertEqual(r, a % b)
self.assertEqual(a, (q * b) + r)
self.assertEqual(q, math.floor(q))
if b > (0 * b):
self.assertTrue((0 * b) <= r < b)
else:
self.assertTrue((0 * b) >= r > b)
if isinstance(a, self.Q_):
self.assertEqual(r.units, a.units)
else:
self.assertTrue(r.unitless)
self.assertTrue(q.unitless)
copy_a = copy.copy(a)
a %= b
self.assertEqual(a, r)
copy_a //= b
self.assertEqual(copy_a, q)
def _test_quantity_divmod(self):
self._test_quantity_divmod_one('10*meter', '4.2*inch')
self._test_quantity_divmod_one('-10*meter', '4.2*inch')
self._test_quantity_divmod_one('-10*meter', '-4.2*inch')
self._test_quantity_divmod_one('10*meter', '-4.2*inch')
self._test_quantity_divmod_one('400*degree', '3')
self._test_quantity_divmod_one('4', '180 degree')
self._test_quantity_divmod_one(4, '180 degree')
self._test_quantity_divmod_one('20', 4)
self._test_quantity_divmod_one('300*degree', '100 degree')
a = self.Q_('10*meter')
b = self.Q_('3*second')
self.assertRaises(DimensionalityError, divmod, a, b)
self.assertRaises(DimensionalityError, divmod, 3, b)
self.assertRaises(DimensionalityError, divmod, a, 3)
def _test_numeric(self, unit, ifunc):
self._test_quantity_add_sub(unit, self._test_not_inplace)
@ -459,6 +526,8 @@ class TestQuantityBasicMath(QuantityTestCase):
self._test_quantity_mul_div(unit, self._test_not_inplace)
self._test_quantity_imul_idiv(unit, ifunc)
self._test_quantity_floordiv(unit, self._test_not_inplace)
self._test_quantity_mod(unit, self._test_not_inplace)
self._test_quantity_divmod()
#self._test_quantity_ifloordiv(unit, ifunc)
def test_float(self):