Author: Amaury Forgeot d'Arc <[email protected]>
Branch: decimal-libmpdec
Changeset: r73799:874ecb09ba3b
Date: 2014-09-24 08:46 +0200
http://bitbucket.org/pypy/pypy/changeset/874ecb09ba3b/
Log: Implement Decimal.__format__.
diff --git a/pypy/module/_decimal/interp_decimal.py
b/pypy/module/_decimal/interp_decimal.py
--- a/pypy/module/_decimal/interp_decimal.py
+++ b/pypy/module/_decimal/interp_decimal.py
@@ -262,6 +262,104 @@
ctx, status_ptr)
return w_result
+ def _recode_to_utf8(self, ptr):
+ s = rffi.charp2str(ptr)
+ if len(s) == 0 or (len(s) == 1 and 32 <= ord(s[0]) < 128):
+ return None, ptr
+ u = locale_decode(s)
+ s = u.encode('utf-8')
+ ptr = rffi.str2charp(s)
+ return ptr, ptr
+
+ @unwrap_spec(fmt=unicode)
+ def descr_format(self, space, fmt, w_override=None):
+ fmt = fmt.encode('utf-8')
+ context = interp_context.getcontext(space)
+
+ replace_fillchar = False
+ if fmt and fmt[0] == '\0':
+ # NUL fill character: must be replaced with a valid UTF-8 char
+ # before calling mpd_parse_fmt_str().
+ replace_fillchar = True
+ fmt = '_' + fmt[1:]
+
+ dot_buf = sep_buf = grouping_buf = lltype.nullptr(rffi.CCHARP.TO)
+ spec = lltype.malloc(rmpdec.MPD_SPEC_PTR.TO, flavor='raw')
+ try:
+ if not rmpdec.mpd_parse_fmt_str(spec, fmt, context.capitals):
+ raise oefmt(space.w_ValueError, "invalid format string")
+ if replace_fillchar:
+ # In order to avoid clobbering parts of UTF-8 thousands
+ # separators or decimal points when the substitution is
+ # reversed later, the actual placeholder must be an invalid
+ # UTF-8 byte.
+ spec.c_fill[0] = '\xff'
+ spec.c_fill[1] = '\0'
+
+ if w_override:
+ # Values for decimal_point, thousands_sep and grouping can
+ # be explicitly specified in the override dict. These values
+ # take precedence over the values obtained from localeconv()
+ # in mpd_parse_fmt_str(). The feature is not documented and
+ # is only used in test_decimal.
+ try:
+ w_dot = space.getitem(
+ w_override, space.wrap("decimal_point"))
+ except OperationError as e:
+ if not e.match(space, space.w_KeyError):
+ raise
+ else:
+ dot_buf = rffi.str2charp(space.str_w(w_dot))
+ spec.c_dot = dot_buf
+ try:
+ w_sep = space.getitem(
+ w_override, space.wrap("thousands_sep"))
+ except OperationError as e:
+ if not e.match(space, space.w_KeyError):
+ raise
+ else:
+ sep_buf = rffi.str2charp(space.str_w(w_sep))
+ spec.c_sep = sep_buf
+ try:
+ w_grouping = space.getitem(
+ w_override, space.wrap("grouping"))
+ except OperationError as e:
+ if not e.match(space, space.w_KeyError):
+ raise
+ else:
+ grouping_buf = rffi.str2charp(space.str_w(w_grouping))
+ spec.c_grouping = grouping_buf
+ if rmpdec.mpd_validate_lconv(spec) < 0:
+ raise oefmt(space.w_ValueError, "invalid override dict")
+ else:
+ dot_buf, spec.c_dot = self._recode_to_utf8(spec.c_dot)
+ sep_buf, spec.c_sep = self._recode_to_utf8(spec.c_sep)
+
+ with context.catch_status(space) as (ctx, status_ptr):
+ decstring = rmpdec.mpd_qformat_spec(
+ self.mpd, spec, context.ctx, status_ptr)
+ status = rffi.cast(lltype.Signed, status_ptr[0])
+ if not decstring:
+ if status & rmpdec.MPD_Malloc_error:
+ raise OperationError(space.w_MemoryError, space.w_None)
+ else:
+ raise oefmt(space.w_ValueError,
+ "format specification exceeds "
+ "internal limits of _decimal")
+ finally:
+ lltype.free(spec, flavor='raw')
+ if dot_buf:
+ lltype.free(dot_buf, flavor='raw')
+ if sep_buf:
+ lltype.free(sep_buf, flavor='raw')
+ if grouping_buf:
+ lltype.free(grouping_buf, flavor='raw')
+
+ ret = rffi.charp2str(decstring)
+ if replace_fillchar:
+ ret = ret.replace('\xff', '\0')
+ return space.wrap(ret.decode('utf-8'))
+
def compare(self, space, w_other, op):
context = interp_context.getcontext(space)
w_err, w_self, w_other = convert_binop_cmp(
@@ -954,6 +1052,7 @@
__floor__ = interp2app(W_Decimal.descr_floor),
__ceil__ = interp2app(W_Decimal.descr_ceil),
__round__ = interp2app(W_Decimal.descr_round),
+ __format__ = interp2app(W_Decimal.descr_format),
#
__eq__ = interp2app(W_Decimal.descr_eq),
__ne__ = interp2app(W_Decimal.descr_ne),
diff --git a/pypy/module/_decimal/test/test_decimal.py
b/pypy/module/_decimal/test/test_decimal.py
--- a/pypy/module/_decimal/test/test_decimal.py
+++ b/pypy/module/_decimal/test/test_decimal.py
@@ -1044,3 +1044,243 @@
c.traps[Inexact] = True
raises(Inexact, Decimal("999.9").to_integral_exact, ROUND_UP)
+ def test_formatting(self):
+ Decimal = self.decimal.Decimal
+
+ # triples giving a format, a Decimal, and the expected result
+ test_values = [
+ ('e', '0E-15', '0e-15'),
+ ('e', '2.3E-15', '2.3e-15'),
+ ('e', '2.30E+2', '2.30e+2'), # preserve significant zeros
+ ('e', '2.30000E-15', '2.30000e-15'),
+ ('e', '1.23456789123456789e40', '1.23456789123456789e+40'),
+ ('e', '1.5', '1.5e+0'),
+ ('e', '0.15', '1.5e-1'),
+ ('e', '0.015', '1.5e-2'),
+ ('e', '0.0000000000015', '1.5e-12'),
+ ('e', '15.0', '1.50e+1'),
+ ('e', '-15', '-1.5e+1'),
+ ('e', '0', '0e+0'),
+ ('e', '0E1', '0e+1'),
+ ('e', '0.0', '0e-1'),
+ ('e', '0.00', '0e-2'),
+ ('.6e', '0E-15', '0.000000e-9'),
+ ('.6e', '0', '0.000000e+6'),
+ ('.6e', '9.999999', '9.999999e+0'),
+ ('.6e', '9.9999999', '1.000000e+1'),
+ ('.6e', '-1.23e5', '-1.230000e+5'),
+ ('.6e', '1.23456789e-3', '1.234568e-3'),
+ ('f', '0', '0'),
+ ('f', '0.0', '0.0'),
+ ('f', '0E-2', '0.00'),
+ ('f', '0.00E-8', '0.0000000000'),
+ ('f', '0E1', '0'), # loses exponent information
+ ('f', '3.2E1', '32'),
+ ('f', '3.2E2', '320'),
+ ('f', '3.20E2', '320'),
+ ('f', '3.200E2', '320.0'),
+ ('f', '3.2E-6', '0.0000032'),
+ ('.6f', '0E-15', '0.000000'), # all zeros treated equally
+ ('.6f', '0E1', '0.000000'),
+ ('.6f', '0', '0.000000'),
+ ('.0f', '0', '0'), # no decimal point
+ ('.0f', '0e-2', '0'),
+ ('.0f', '3.14159265', '3'),
+ ('.1f', '3.14159265', '3.1'),
+ ('.4f', '3.14159265', '3.1416'),
+ ('.6f', '3.14159265', '3.141593'),
+ ('.7f', '3.14159265', '3.1415926'), # round-half-even!
+ ('.8f', '3.14159265', '3.14159265'),
+ ('.9f', '3.14159265', '3.141592650'),
+
+ ('g', '0', '0'),
+ ('g', '0.0', '0.0'),
+ ('g', '0E1', '0e+1'),
+ ('G', '0E1', '0E+1'),
+ ('g', '0E-5', '0.00000'),
+ ('g', '0E-6', '0.000000'),
+ ('g', '0E-7', '0e-7'),
+ ('g', '-0E2', '-0e+2'),
+ ('.0g', '3.14159265', '3'), # 0 sig fig -> 1 sig fig
+ ('.0n', '3.14159265', '3'), # same for 'n'
+ ('.1g', '3.14159265', '3'),
+ ('.2g', '3.14159265', '3.1'),
+ ('.5g', '3.14159265', '3.1416'),
+ ('.7g', '3.14159265', '3.141593'),
+ ('.8g', '3.14159265', '3.1415926'), # round-half-even!
+ ('.9g', '3.14159265', '3.14159265'),
+ ('.10g', '3.14159265', '3.14159265'), # don't pad
+
+ ('%', '0E1', '0%'),
+ ('%', '0E0', '0%'),
+ ('%', '0E-1', '0%'),
+ ('%', '0E-2', '0%'),
+ ('%', '0E-3', '0.0%'),
+ ('%', '0E-4', '0.00%'),
+
+ ('.3%', '0', '0.000%'), # all zeros treated equally
+ ('.3%', '0E10', '0.000%'),
+ ('.3%', '0E-10', '0.000%'),
+ ('.3%', '2.34', '234.000%'),
+ ('.3%', '1.234567', '123.457%'),
+ ('.0%', '1.23', '123%'),
+
+ ('e', 'NaN', 'NaN'),
+ ('f', '-NaN123', '-NaN123'),
+ ('+g', 'NaN456', '+NaN456'),
+ ('.3e', 'Inf', 'Infinity'),
+ ('.16f', '-Inf', '-Infinity'),
+ ('.0g', '-sNaN', '-sNaN'),
+
+ ('', '1.00', '1.00'),
+
+ # test alignment and padding
+ ('6', '123', ' 123'),
+ ('<6', '123', '123 '),
+ ('>6', '123', ' 123'),
+ ('^6', '123', ' 123 '),
+ ('=+6', '123', '+ 123'),
+ ('#<10', 'NaN', 'NaN#######'),
+ ('#<10', '-4.3', '-4.3######'),
+ ('#<+10', '0.0130', '+0.0130###'),
+ ('#< 10', '0.0130', ' 0.0130###'),
+ ('@>10', '-Inf', '@-Infinity'),
+ ('#>5', '-Inf', '-Infinity'),
+ ('?^5', '123', '?123?'),
+ ('%^6', '123', '%123%%'),
+ (' ^6', '-45.6', '-45.6 '),
+ ('/=10', '-45.6', '-/////45.6'),
+ ('/=+10', '45.6', '+/////45.6'),
+ ('/= 10', '45.6', ' /////45.6'),
+ ('\x00=10', '-inf', '-\x00Infinity'),
+ ('\x00^16', '-inf', '\x00\x00\x00-Infinity\x00\x00\x00\x00'),
+ ('\x00>10', '1.2345', '\x00\x00\x00\x001.2345'),
+ ('\x00<10', '1.2345', '1.2345\x00\x00\x00\x00'),
+
+ # thousands separator
+ (',', '1234567', '1,234,567'),
+ (',', '123456', '123,456'),
+ (',', '12345', '12,345'),
+ (',', '1234', '1,234'),
+ (',', '123', '123'),
+ (',', '12', '12'),
+ (',', '1', '1'),
+ (',', '0', '0'),
+ (',', '-1234567', '-1,234,567'),
+ (',', '-123456', '-123,456'),
+ ('7,', '123456', '123,456'),
+ ('8,', '123456', ' 123,456'),
+ ('08,', '123456', '0,123,456'), # special case: extra 0 needed
+ ('+08,', '123456', '+123,456'), # but not if there's a sign
+ (' 08,', '123456', ' 123,456'),
+ ('08,', '-123456', '-123,456'),
+ ('+09,', '123456', '+0,123,456'),
+ # ... with fractional part...
+ ('07,', '1234.56', '1,234.56'),
+ ('08,', '1234.56', '1,234.56'),
+ ('09,', '1234.56', '01,234.56'),
+ ('010,', '1234.56', '001,234.56'),
+ ('011,', '1234.56', '0,001,234.56'),
+ ('012,', '1234.56', '0,001,234.56'),
+ ('08,.1f', '1234.5', '01,234.5'),
+ # no thousands separators in fraction part
+ (',', '1.23456789', '1.23456789'),
+ (',%', '123.456789', '12,345.6789%'),
+ (',e', '123456', '1.23456e+5'),
+ (',E', '123456', '1.23456E+5'),
+
+ # issue 6850
+ ('a=-7.0', '0.12345', 'aaaa0.1'),
+ ]
+ for fmt, d, result in test_values:
+ self.assertEqual(format(Decimal(d), fmt), result)
+
+ # bytes format argument
+ self.assertRaises(TypeError, Decimal(1).__format__, b'-020')
+
+ def test_n_format(self):
+ Decimal = self.decimal.Decimal
+
+ try:
+ from locale import CHAR_MAX
+ except ImportError:
+ self.skipTest('locale.CHAR_MAX not available')
+
+ def make_grouping(lst):
+ return ''.join([chr(x) for x in lst])
+
+ def get_fmt(x, override=None, fmt='n'):
+ return Decimal(x).__format__(fmt, override)
+
+ # Set up some localeconv-like dictionaries
+ en_US = {
+ 'decimal_point' : '.',
+ 'grouping' : make_grouping([3, 3, 0]),
+ 'thousands_sep' : ','
+ }
+
+ fr_FR = {
+ 'decimal_point' : ',',
+ 'grouping' : make_grouping([CHAR_MAX]),
+ 'thousands_sep' : ''
+ }
+
+ ru_RU = {
+ 'decimal_point' : ',',
+ 'grouping': make_grouping([3, 3, 0]),
+ 'thousands_sep' : ' '
+ }
+
+ crazy = {
+ 'decimal_point' : '&',
+ 'grouping': make_grouping([1, 4, 2, CHAR_MAX]),
+ 'thousands_sep' : '-'
+ }
+
+ dotsep_wide = {
+ 'decimal_point' : b'\xc2\xbf'.decode('utf-8'),
+ 'grouping': make_grouping([3, 3, 0]),
+ 'thousands_sep' : b'\xc2\xb4'.decode('utf-8')
+ }
+
+ self.assertEqual(get_fmt(Decimal('12.7'), en_US), '12.7')
+ self.assertEqual(get_fmt(Decimal('12.7'), fr_FR), '12,7')
+ self.assertEqual(get_fmt(Decimal('12.7'), ru_RU), '12,7')
+ self.assertEqual(get_fmt(Decimal('12.7'), crazy), '1-2&7')
+
+ self.assertEqual(get_fmt(123456789, en_US), '123,456,789')
+ self.assertEqual(get_fmt(123456789, fr_FR), '123456789')
+ self.assertEqual(get_fmt(123456789, ru_RU), '123 456 789')
+ self.assertEqual(get_fmt(1234567890123, crazy), '123456-78-9012-3')
+
+ self.assertEqual(get_fmt(123456789, en_US, '.6n'), '1.23457e+8')
+ self.assertEqual(get_fmt(123456789, fr_FR, '.6n'), '1,23457e+8')
+ self.assertEqual(get_fmt(123456789, ru_RU, '.6n'), '1,23457e+8')
+ self.assertEqual(get_fmt(123456789, crazy, '.6n'), '1&23457e+8')
+
+ # zero padding
+ self.assertEqual(get_fmt(1234, fr_FR, '03n'), '1234')
+ self.assertEqual(get_fmt(1234, fr_FR, '04n'), '1234')
+ self.assertEqual(get_fmt(1234, fr_FR, '05n'), '01234')
+ self.assertEqual(get_fmt(1234, fr_FR, '06n'), '001234')
+
+ self.assertEqual(get_fmt(12345, en_US, '05n'), '12,345')
+ self.assertEqual(get_fmt(12345, en_US, '06n'), '12,345')
+ self.assertEqual(get_fmt(12345, en_US, '07n'), '012,345')
+ self.assertEqual(get_fmt(12345, en_US, '08n'), '0,012,345')
+ self.assertEqual(get_fmt(12345, en_US, '09n'), '0,012,345')
+ self.assertEqual(get_fmt(12345, en_US, '010n'), '00,012,345')
+
+ self.assertEqual(get_fmt(123456, crazy, '06n'), '1-2345-6')
+ self.assertEqual(get_fmt(123456, crazy, '07n'), '1-2345-6')
+ self.assertEqual(get_fmt(123456, crazy, '08n'), '1-2345-6')
+ self.assertEqual(get_fmt(123456, crazy, '09n'), '01-2345-6')
+ self.assertEqual(get_fmt(123456, crazy, '010n'), '0-01-2345-6')
+ self.assertEqual(get_fmt(123456, crazy, '011n'), '0-01-2345-6')
+ self.assertEqual(get_fmt(123456, crazy, '012n'), '00-01-2345-6')
+ self.assertEqual(get_fmt(123456, crazy, '013n'), '000-01-2345-6')
+
+ # wide char separator and decimal point
+ self.assertEqual(get_fmt(Decimal('-1.5'), dotsep_wide, '020n'),
+ '-0\u00b4000\u00b4000\u00b4000\u00b4001\u00bf5')
+
diff --git a/rpython/rlib/rmpdec.py b/rpython/rlib/rmpdec.py
--- a/rpython/rlib/rmpdec.py
+++ b/rpython/rlib/rmpdec.py
@@ -64,6 +64,7 @@
"mpd_qand", "mpd_qor", "mpd_qxor",
"mpd_qcopy_sign", "mpd_qcopy_abs", "mpd_qcopy_negate",
"mpd_qround_to_int", "mpd_qround_to_intx",
+ "mpd_parse_fmt_str", "mpd_qformat_spec", "mpd_validate_lconv",
"mpd_version",
],
compile_extra=compile_extra,
@@ -140,6 +141,13 @@
('allcr', lltype.Signed),
])
+ MPD_SPEC_T = platform.Struct('mpd_spec_t',
+ [('dot', rffi.CCHARP),
+ ('sep', rffi.CCHARP),
+ ('grouping', rffi.CCHARP),
+ ('fill', rffi.CFixedArray(rffi.CHAR, 5)),
+ ])
+
globals().update(platform.configure(CConfig))
@@ -150,6 +158,7 @@
MPD_PTR = lltype.Ptr(MPD_T)
MPD_CONTEXT_PTR = lltype.Ptr(MPD_CONTEXT_T)
+MPD_SPEC_PTR = lltype.Ptr(MPD_SPEC_T)
# Initialization
mpd_qset_ssize = external(
@@ -395,4 +404,12 @@
'mpd_qround_to_intx', [MPD_PTR, MPD_PTR, MPD_CONTEXT_PTR, rffi.UINTP],
lltype.Void)
+mpd_parse_fmt_str = external(
+ 'mpd_parse_fmt_str', [MPD_SPEC_PTR, rffi.CCHARP, rffi.INT], rffi.INT)
+mpd_qformat_spec = external(
+ 'mpd_qformat_spec', [MPD_PTR, MPD_SPEC_PTR, MPD_CONTEXT_PTR, rffi.UINTP],
+ rffi.CCHARP)
+mpd_validate_lconv = external(
+ 'mpd_validate_lconv', [MPD_SPEC_PTR], rffi.INT)
+
mpd_version = external('mpd_version', [], rffi.CCHARP, macro=True)
_______________________________________________
pypy-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pypy-commit