[Python-checkins] r83897 - python/branches/py3k-dtoa/Lib/test/test_dtoa.py
mark.dickinson
python-checkins at python.org
Mon Aug 9 20:50:22 CEST 2010
Author: mark.dickinson
Date: Mon Aug 9 20:50:22 2010
New Revision: 83897
Log:
Add framework for testing float repr and float formatting.
Added:
python/branches/py3k-dtoa/Lib/test/test_dtoa.py
Added: python/branches/py3k-dtoa/Lib/test/test_dtoa.py
==============================================================================
--- (empty file)
+++ python/branches/py3k-dtoa/Lib/test/test_dtoa.py Mon Aug 9 20:50:22 2010
@@ -0,0 +1,297 @@
+import random
+import struct
+import unittest
+import math
+import decimal
+import test.support
+import sys
+
+def bitfield(n, a, b):
+ """Extract bits a through b - 1 (inclusive) of an integer n."""
+ return (n >> a) & ((1 << b - a) - 1)
+
+# Some utility functions for manipulating Decimal instances.
+# XXX These are *slow*.
+
+def decimal_to_triple(d):
+ """Express a finite Decimal instance as a triple (s, c, e) of
+ integers, with value (-1)**s * c * 10**e."""
+ sign, digits, exp = d.as_tuple()
+ return sign, int(''.join(map(str, digits))), exp
+
+def decimal_from_triple(sign, coefficient, exponent):
+ """Create a Decimal instance from a triple; inverse of
+ decimal_to_triple."""
+ return decimal.Decimal((sign,
+ tuple(map(int, list(str(coefficient)))),
+ exponent))
+
+def next_fixed(d):
+ """Next decimal.Decimal up with the same exponent."""
+ s, c, e = decimal_to_triple(d)
+ return decimal_from_triple(s, c+1, e)
+
+def next_floating(d):
+ """Next decimal.Decimal up with the same coefficient length."""
+ s, c, e = decimal_to_triple(d)
+ # if c is one less than a power of 10...
+ strc = str(c)
+ if strc == '9' * len(strc):
+ return decimal_from_triple(s, (c + 1) // 10, e + 1)
+ else:
+ return decimal_from_triple(s, c + 1, e)
+
+class MockFloat(object):
+ """Mock float class, implemented using integer arithmetic.
+
+ Provides integer and Decimal-based formatting operations, for the
+ purposes of comparing with true float str, repr and formatting.
+
+ This class models IEEE 754 binary64 floating-point, except that:
+
+ - NaNs and infinities aren't represented, and
+ - There's no upper bound on the exponent.
+
+ Subnormals and signed zeros are supported.
+
+ """
+ # parameters of the system float format
+ MIN_EXP = sys.float_info.min_exp
+ MANT_DIG = sys.float_info.mant_dig
+ SPECIAL_EXP = sys.float_info.max_exp - MIN_EXP + 2
+ TOTAL_BITS = MANT_DIG + SPECIAL_EXP.bit_length()
+
+ def __new__(cls, x):
+ if isinstance(x, tuple):
+ # create from triple (s, m, e), representing the value (-1)**s * m
+ # * 2**e. The triple is required to be normalized, in the sense
+ # that:
+ #
+ # * e >= MIN_EXP - MANT_DIG
+ # * m.bit_length() <= MANT_DIG
+ # * either e == MIN_EXP - MANT_DIG or m.bit_length() == MANT_DIG
+ self = object.__new__(cls)
+ self.sign, self.coefficient, self.exponent = x
+ if not self.is_normalized():
+ raise ValueError("unnormalized MockFloat instance")
+ return self
+ elif isinstance(x, float):
+ return cls.from_float(x)
+ elif isinstance(x, decimal.Decimal):
+ return cls.from_decimal(x)
+ else:
+ raise TypeError("Don't know how to construct a MockFloat "
+ "from an object of type {}".format(type(x)))
+
+ def __bool__(self):
+ return bool(self.coefficient)
+
+ def __eq__(self, other):
+ return (self.sign == other.sign and
+ self.coefficient == other.coefficient and
+ self.exponent == other.exponent)
+
+ def __repr__(self):
+ if not self:
+ return '-0.0' if self.sign else '0.0'
+
+ sign, coeff, exp = decimal_to_triple(self.to_shortest_decimal())
+ digits = str(coeff)
+ k = len(str(coeff)) + exp
+ if k <= -4 or k > 16:
+ # Use scientific notation.
+ res = '{}{}e{:+03d}'.format(digits[:1],
+ '.' + digits[1:] if digits[1:] else '',
+ k - 1)
+ else:
+ # Non-scientific notation; may need to pad with zeros on either the
+ # left or the right of the digit string.
+ dotpos = exp + len(digits)
+ if dotpos <= 0:
+ res = '0.' + '0'*-dotpos + digits
+ elif dotpos >= len(digits):
+ res = digits + '0'*(dotpos-len(digits)) + '.0'
+ else:
+ res = digits[:dotpos] + '.' + digits[dotpos:]
+ return '-' + res if sign else res
+
+ def is_normalized(self):
+ m, e = self.coefficient, self.exponent
+ return (m >= 0 and
+ e >= self.MIN_EXP - self.MANT_DIG and
+ m.bit_length() <= self.MANT_DIG and
+ (e == self.MIN_EXP - self.MANT_DIG or
+ m.bit_length() == self.MANT_DIG))
+
+ @classmethod
+ def from_float(cls, x):
+ """Convert a finite float to an equivalent MockFloat."""
+
+ x_bits = struct.pack('<d', x)
+ n = int.from_bytes(x_bits, 'little')
+
+ # Break into fields: significand m, exponent e, and sign s.
+ coeff = bitfield(n, 0, cls.MANT_DIG - 1)
+ exp = bitfield(n, cls.MANT_DIG - 1, cls.TOTAL_BITS - 1)
+ sign = bitfield(n, cls.TOTAL_BITS - 1, cls.TOTAL_BITS)
+ if exp == cls.SPECIAL_EXP:
+ # NaNs and infinities.
+ raise ValueError("from_float expects a finite value, "
+ "not an infinity or nan.")
+
+ elif exp == 0:
+ # Zeros and subnormal values.
+ return cls((sign, coeff, cls.MIN_EXP - cls.MANT_DIG + exp))
+ else:
+ # Normal values.
+ return cls((sign, coeff + (1 << cls.MANT_DIG - 1),
+ cls.MIN_EXP - cls.MANT_DIG - 1 + exp))
+
+ @classmethod
+ def from_decimal(cls, x):
+ """Convert a finite Decimal instance to the nearest MockFloat."""
+
+ s, n, d = decimal_to_triple(x)
+ # Express abs(x) as an integer fraction, a / b
+ a, b = n * 10**max(d, 0), 10**max(0, -d)
+ if a:
+ # Identify exponent e such that 2**(e-1) <= a / b < 2**e.
+ # The difference 'e_test' between the bit lengths of a and b
+ # gives a value that's either correct, or one too small.
+ e_test = a.bit_length() - b.bit_length()
+ scaled_a = a >> e_test if e_test >= 0 else a << -e_test
+ e = e_test if scaled_a < b else e_test + 1
+ else:
+ e = cls.MIN_EXP
+
+ # Adjust e to give a result with MANT_DIG bits of precision for normal
+ # numbers, or such that e == MIN_EXP - MANT_DIG for subnormals.
+ e = max(e, cls.MIN_EXP) - cls.MANT_DIG
+
+ # Now approximate a / b by number of the form m * 2**e. For rounding
+ # purposes, we compute an extra 2 bits for m, hence the '2' in '2-e'.
+ p2 = 2 - e
+ a, b = a << max(p2, 0), b << max(0, -p2)
+ m, r = divmod(a, b)
+ # Absorb any remainder into the last bit of m, then find the nearest
+ # integer to m / 4, using round-half-to-even.
+ if r:
+ m |= 1
+ m = (m >> 2) + (bool(m & 2) and bool(m & 5))
+ if m.bit_length() == cls.MANT_DIG + 1:
+ m //= 2
+ e += 1
+
+ return cls((s, m, e))
+
+ def decimal_exponent(self):
+ """The unique integer d such that 10**(d-1) <= abs(self) < 10**d.
+
+ If self is zero, there's no such integer; a ValueError is raised in
+ this case.
+
+ """
+ if not self:
+ raise ValueError("decimal_exponent expects a nonzero argument")
+
+ e = self.exponent
+ # Express abs(self) as a fraction, a / b, and compute an approximation
+ # to log10(abs(self)) by counting decimal digits in a and b.
+ a, b = self.coefficient << max(e, 0), 1 << max(0, -e)
+ d = len(str(a)) - len(str(b))
+ # d is either correct, or one too small; compare a / 10**d
+ # with b to find out which.
+ scaled_a = a // 10**d if d >= 0 else a * 10**-d
+ if scaled_a >= b:
+ d += 1
+ return d
+
+ def to_decimal(self, d):
+ """Convert to a decimal with a particular exponent d.
+
+ Returns a pair x, r, where:
+
+ x is the largest decimal with exponent d that doesn't exceed self.
+ r is False if x is the *closest* decimal with exponent d to
+ self, else True.
+
+ """
+
+ p2 = self.exponent - d + 2
+ n, r = divmod(self.coefficient * 5**max(-d, 0) << max(p2, 0),
+ 5**max(d, 0) << max(-p2, 0))
+ if r:
+ n |= 1
+ round_up = bool(n & 2) and bool(n & 5)
+ return decimal_from_triple(self.sign, n >> 2, d), round_up
+
+ def to_fixed_precision(self, d):
+ """Round self to the nearest integral multiple of 10**d."""
+ n, r = self.to_decimal(d)
+ return next_fixed(n) if r else n
+
+ def to_significant_figures(self, f):
+ """Round self to the given number of significant figures.
+
+ self should be nonzero; if it's zero a ValueError is raised.
+
+ """
+ if f < 1:
+ raise ValueError("significant figures should be positive.")
+ if not self:
+ raise ValueError("Refusing to compute significant digits of 0.")
+
+ d = self.decimal_exponent() - f
+ n, r = self.to_decimal(d)
+ return next_floating(n) if r else n
+
+ def to_shortest_decimal(self):
+ """Find shortest Decimal value that rounds back to the original."""
+
+ if not self:
+ raise ValueError("Refusing to compute shortest decimal for 0.")
+
+ d = self.decimal_exponent() - 1
+ while True:
+ lower, round_up = self.to_decimal(d)
+ upper = next_floating(lower)
+
+ lower_ok = MockFloat(lower) == self
+ upper_ok = MockFloat(upper) == self
+
+ if upper_ok and (round_up or not lower_ok):
+ return upper
+ elif lower_ok:
+ return lower
+ d -= 1
+
+
+class ReprTests(unittest.TestCase):
+ def check_repr(self, x):
+ """Compare float.__repr__ with MockFloat.__repr__."""
+ expected = repr(MockFloat(x))
+ got = repr(x)
+ self.assertEqual(expected, got,
+ "Incorrect repr for {}: "
+ "expected {}, got {}".format(x, expected, got))
+
+ def test_random(self):
+ for _ in range(1000):
+ # bit pattern corresponding to a random finite positive float
+ bits = random.randrange(2047*2**52)
+ x = struct.unpack('<d', struct.pack('<Q', bits))[0]
+ self.check_repr(x)
+
+ def test_particular(self):
+ self.check_repr(0.0)
+ self.check_repr(-0.0)
+ self.check_repr(2.3)
+ self.check_repr(4.788141995955901e+131)
+ # case where it matters that we're using round half to even
+ self.check_repr(1e23)
+
+def test_main():
+ test.support.run_unittest(ReprTests)
+
+if __name__ == "__main__":
+ test_main()
More information about the Python-checkins
mailing list