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

Reply via email to