diff --git a/Doc/library/fractions.rst b/Doc/library/fractions.rst index c2c7401..767299f 100644 --- a/Doc/library/fractions.rst +++ b/Doc/library/fractions.rst @@ -164,6 +164,25 @@ another rational number, or from a string. method can also be accessed through the :func:`round` function. + .. method:: __format__(specifier) + + Format Fraction instance according to given specifier. + + If the specifier ends with one of ``'e'``, ``'E'``, ``'f'``, + ``'F'``, ``'g'``, ``'G'``, ``'n'`` or ``'%'``, then the Fraction + is formatted as a decimal number and all the formatting options + that work with :class:`Decimals ` are available. + + Otherwise the Fraction is formatted as a string + ``'numerator/denominator'``. See :ref:`formatspec` for available + formatting options. Also the sign options ``'-'``, ``'+'`` or + ``' '`` are supported. + + >>> '{:.2f}'.format(Fraction(1, 2)) + '0.50' + >>> '{:_^+8}'.format(Fraction(1, 2)) + '__+1/2__' + .. function:: gcd(a, b) Return the greatest common divisor of the integers *a* and *b*. If either diff --git a/Lib/_pydecimal.py b/Lib/_pydecimal.py index 88222be..f796922 100644 --- a/Lib/_pydecimal.py +++ b/Lib/_pydecimal.py @@ -6264,7 +6264,9 @@ def _group_lengths(grouping): from itertools import chain, repeat if not grouping: return [] - elif grouping[-1] == 0 and len(grouping) >= 2: + if isinstance(grouping, str): + grouping = list(grouping.encode('utf-8', 'ignore')) + if grouping[-1] == 0 and len(grouping) >= 2: return chain(grouping[:-1], repeat(grouping[-2])) elif grouping[-1] == _locale.CHAR_MAX: return grouping[:-1] diff --git a/Lib/fractions.py b/Lib/fractions.py index 5ddc84c..89492d8 100644 --- a/Lib/fractions.py +++ b/Lib/fractions.py @@ -4,6 +4,7 @@ """Fraction, infinite-precision, real numbers.""" from decimal import Decimal +import decimal import math import numbers import operator @@ -298,6 +299,37 @@ class Fraction(numbers.Rational): else: return '%s/%s' % (self._numerator, self._denominator) + def __format__(self, specifier): + """Format Fraction instance according to given specifier. + + >>> '{:.2f}'.format(Fraction(1, 2)) + '0.50' + >>> '{:_^+8}'.format(Fraction(1, 2)) + '__+1/2__' + + """ + if not isinstance(specifier, str): + raise TypeError("must be str, not %s" % type(specifier).__name__) + + parts = _parse_format_specifier(specifier) + + if parts['type'] in ('', 's'): # Format as fraction + sign = parts['sign'] + modified_specifier = _make_format_specifier(dict(parts, sign='')) + prefix = (sign if (sign in ('+', ' ') and self >= 0) else '') + return format(prefix + str(self), modified_specifier) + elif parts['type'] in 'eEfFgGn%': # Format as decimal + prec = int(parts.get('precision') or 6) + length = len(str(self.numerator // self.denominator)) + modified_parts = dict(parts, precision=prec) + modified_specifier = _make_format_specifier(modified_parts) + dec_ctx = decimal.Context( + prec=(length + prec + 1), rounding=decimal.ROUND_HALF_EVEN) + decimal_self = dec_ctx.divide(self.numerator, self.denominator) + return decimal_self.__format__(modified_specifier, context=dec_ctx) + raise ValueError('Unknown format code {!r} for {} object'.format( + parts['type'], type(self).__name__)) + def _operator_fallbacks(monomorphic_operator, fallback_operator): """Generates forward and reverse operators given a purely-rational operator and a function from the operator module. @@ -644,3 +676,33 @@ class Fraction(numbers.Rational): if type(self) == Fraction: return self # My components are also immutable return self.__class__(self._numerator, self._denominator) + + +_FORMAT_SPECIFIER_REGEX = re.compile( + r'\A' + r'((?P.)?(?P[<>=^]))?' + r'(?P[-+ ]?)' + r'(?P#?)' + r'(?P0?)' + r'(?P\d*)' + r'(?P,?)' + r'(\.(?P\d+))?' + r'(?P[bcdeEfFgGnosxX%]?)' + r'\Z', + re.DOTALL) +_FORMAT_SPECIFIER_TEMPLATE = ( + '{fill}{align}{sign}{alt}{zeropad}{width}{thousands_sep}{precision}{type}') + + +def _parse_format_specifier(specifier): + match = _FORMAT_SPECIFIER_REGEX.match(specifier) + if match: + return match.groupdict() + raise ValueError('Invalid format specifier: ' + specifier) + + +def _make_format_specifier(parts): + parts = {k: (v or '') for (k, v) in parts.items()} + if parts['precision']: + parts['precision'] = '.' + str(parts['precision']) + return _FORMAT_SPECIFIER_TEMPLATE.format(**parts) diff --git a/Lib/test/test_decimal.py b/Lib/test/test_decimal.py index 137aaa5..aaf0310 100644 --- a/Lib/test/test_decimal.py +++ b/Lib/test/test_decimal.py @@ -1082,10 +1082,7 @@ class FormatTest(unittest.TestCase): return ''.join([chr(x) for x in lst]) if self.decimal == C else lst def get_fmt(x, override=None, fmt='n'): - if self.decimal == C: - return Decimal(x).__format__(fmt, override) - else: - return Decimal(x).__format__(fmt, _localeconv=override) + return Decimal(x).__format__(fmt, _localeconv=override) # Set up some localeconv-like dictionaries en_US = { @@ -1159,6 +1156,47 @@ class FormatTest(unittest.TestCase): self.assertEqual(get_fmt(Decimal('-1.5'), dotsep_wide, '020n'), '-0\u00b4000\u00b4000\u00b4000\u00b4001\u00bf5') + def test_n_format_extra_args(self): + # context and _localeconv as positional or keyword args + + ctxd = self.decimal.DefaultContext.copy() + ctxd.rounding = C.ROUND_HALF_DOWN + ctxu = self.decimal.DefaultContext.copy() + ctxu.rounding = C.ROUND_HALF_UP + + pipe_conv = { + 'decimal_point': '|', + 'thousands_sep': '', + 'grouping': [3, 3, 0], + } + + str_grouping_conv = { + 'decimal_point': '|', + 'thousands_sep': '', + 'grouping': '\3\3\0', + } + + d = Decimal = self.decimal.Decimal('1.5') + + self.assertEqual(d.__format__(""), "1.5") + + # context as positional argument + self.assertEqual(d.__format__(".0f", ctxd), "1") + self.assertEqual(d.__format__(".0f", ctxu), "2") + + # context as keyword argument + self.assertEqual(d.__format__(".0f", context=ctxd), "1") + self.assertEqual(d.__format__(".0f", context=ctxu), "2") + + # _localeconv as positional argument + self.assertEqual(d.__format__("n", ctxd, pipe_conv), "1|5") + + # _localeconv as keyword argument + self.assertEqual(d.__format__("n", _localeconv=pipe_conv), "1|5") + + # _localeconv with grouping as str + self.assertEqual(d.__format__("n", _localeconv=str_grouping_conv), "1|5") + @run_with_locale('LC_ALL', 'ps_AF') def test_wide_char_separator_decimal_point(self): # locale with wide char separator and decimal point @@ -4984,11 +5022,24 @@ class CWhitebox(unittest.TestCase): def test_c_format(self): # Restricted input Decimal = C.Decimal + ctx = C.getcontext() HAVE_CONFIG_64 = (C.MAX_PREC > 425000000) self.assertRaises(TypeError, Decimal(1).__format__, "=10.10", [], 9) self.assertRaises(TypeError, Decimal(1).__format__, "=10.10", 9) self.assertRaises(TypeError, Decimal(1).__format__, []) + d = Decimal(1) + self.assertRaises(TypeError, d.__format__, "", 1, [], 9) + self.assertRaises(TypeError, d.__format__, "", 1, 9) + self.assertRaises(TypeError, d.__format__, "", _localeconv=[]) + self.assertRaises(TypeError, d.__format__, "", _localeconv=9) + self.assertRaises(ValueError, d.__format__, "", _localeconv={}) + self.assertRaises(TypeError, d.__format__, "", context=[]) + self.assertRaises(TypeError, d.__format__, "", context=42) + self.assertRaises(TypeError, d.__format__, "", context={}) + self.assertRaises(TypeError, d.__format__, "", {}) + self.assertRaises(TypeError, d.__format__, "", ctx, 9) + self.assertRaises(TypeError, d.__format__, "", ctx, []) self.assertRaises(ValueError, Decimal(1).__format__, "<>=10.10") maxsize = 2**63-1 if HAVE_CONFIG_64 else 2**31-1 @@ -5285,7 +5336,7 @@ class CWhitebox(unittest.TestCase): return ''.join([chr(x) for x in lst]) def get_fmt(x, override=None, fmt='n'): - return Decimal(x).__format__(fmt, override) + return Decimal(x).__format__(fmt, _localeconv=override) invalid_grouping = { 'decimal_point' : ',', diff --git a/Lib/test/test_fractions.py b/Lib/test/test_fractions.py index e86d5ce..4a4d3da 100644 --- a/Lib/test/test_fractions.py +++ b/Lib/test/test_fractions.py @@ -552,6 +552,165 @@ class FractionTest(unittest.TestCase): self.assertEqual("7/3", str(F(7, 3))) self.assertEqual("7", str(F(7, 1))) + def testFormattingAsFraction(self): + self.assertEqual("1/2", format(F(1, 2), "")) + self.assertEqual("1/2 ", format(F(1, 2), "5")) + self.assertEqual(" 1/2", format(F(1, 2), ">5")) + self.assertEqual("1/2 ", format(F(1, 2), "<5")) + self.assertEqual(" 1/2 ", format(F(1, 2), "^5")) + self.assertEqual("+1/2", format(F(1, 2), "+")) + self.assertEqual("-1/2", format(F(-1, 2), "+")) + self.assertEqual("1/2", format(F(1, 2), "-")) + self.assertEqual("-1/2", format(F(-1, 2), "-")) + self.assertEqual(" 1/2", format(F(1, 2), " ")) + self.assertEqual("-1/2", format(F(-1, 2), " ")) + self.assertEqual("__+1/2__", format(F(1, 2), "_^+8")) + + def testFormattingAsDecimal(self): + self.assertEqual("0.500000", format(F(1, 2), "f")) + self.assertEqual("0.148148", format(F(4, 27), 'f')) + self.assertEqual("0.42857", format(F(3, 7), ".5f")) + self.assertEqual("0.43", format(F(3, 7), ".2f")) + self.assertEqual("0.5", format(F(1, 2), "g")) + self.assertEqual("+0.5", format(F(1, 2), "+g")) + self.assertEqual(" 0.5", format(F(1, 2), " g")) + self.assertEqual("-0.5", format(F(-1, 2), " g")) + self.assertEqual("0.5", format(F(1, 2), "n")) + self.assertEqual("50.000000%", format(F(1, 2), "%")) + self.assertEqual("50%", format(F(1, 2), ".0%")) + self.assertEqual(" 50%", format(F(1, 2), " .0%")) + self.assertEqual("+50%", format(F(1, 2), "+.0%")) + self.assertEqual("2e+3", format(F(5000, 3), '.1g')) + self.assertEqual("1.7e+3", format(F(5000, 3), '.2g')) + self.assertEqual("1.67e+3", format(F(5000, 3), '.3g')) + self.assertEqual("1667", format(F(5000, 3), '.4g')) + + def testFormattingTruncation(self): + self.assertEqual("0.500000", format(F(1, 2), "f")) + self.assertEqual("0.250000", format(F(1, 4), "f")) + self.assertEqual("0.125000", format(F(1, 8), "f")) + self.assertEqual("0.062500", format(F(1, 16), "f")) + self.assertEqual("0.031250", format(F(1, 32), "f")) + self.assertEqual("0.015625", format(F(1, 64), "f")) + self.assertEqual("0.0078125", format(F(1, 128), ".7f")) + self.assertEqual("0.007812", format(F(1, 128), "f")) + self.assertEqual("0.00390625", format(F(1, 256), ".8f")) + self.assertEqual("0.003906", format(F(1, 256), "f")) + self.assertEqual("0.001953125", format(F(1, 512), ".9f")) + self.assertEqual("0.001953", format(F(1, 512), "f")) + self.assertEqual("0.0009765625", format(F(1, 1024), ".10f")) + self.assertEqual("0.000977", format(F(1, 1024), "f")) + self.assertEqual("0.00048828125", format(F(1, 2048), ".11f")) + self.assertEqual("0.000488", format(F(1, 2048), "f")) + self.assertEqual("0.000244140625", format(F(1, 4096), ".12f")) + self.assertEqual("0.000244", format(F(1, 4096), "f")) + self.assertEqual("0.000122", format(F(1, 8192), "f")) + self.assertEqual("0.000061", format(F(1, 2**14), "f")) + self.assertEqual("0.000031", format(F(1, 2**15), "f")) + self.assertEqual("0.000015", format(F(1, 2**16), "f")) + self.assertEqual("0.000008", format(F(1, 2**17), "f")) + self.assertEqual("0.000004", format(F(1, 2**18), "f")) + self.assertEqual("0.000002", format(F(1, 2**19), "f")) + self.assertEqual("0.000001", format(F(1, 2**20), "f")) + self.assertEqual("0.000000", format(F(1, 2**21), "f")) + self.assertEqual("0.000000", format(F(1, 10**1000), "f")) + + def testFormattingPrecision(self): + for k in [1, -1]: + sign = '-' if k < 0 else '' + self.assertEqual(sign + "0.1", format(k * F(4, 27), '.1f')) + self.assertEqual(sign + "4200.000000000100000", + format(k * (4200 + F(1, 10**10)), ".15f")) + self.assertEqual(sign + "0." + 30 * "3", + format(k * F(1, 3), ".30f")) + self.assertEqual(sign + "0." + 200 * "3", + format(k * F(1, 3), ".200f")) + self.assertEqual(sign + "0." + 99 * "6" + "7", + format(k * F(2, 3), ".100f")) + self.assertEqual(sign + "1" + (200 * "0") + "." + (99 * "6") + "7", + format(k * (10**200 + F(2, 3)), ".100f")) + + self.assertEqual(sign + "4200.0000000001", + format(k * (4200 + F(1, 10**10)), ".15g")) + self.assertEqual(sign + "0." + 30 * "3", + format(k * F(1, 3), ".30g")) + self.assertEqual(sign + "0." + 200 * "3", + format(k * F(1, 3), ".200g")) + self.assertEqual(sign + "0." + 99 * "6" + "7", + format(k * F(2, 3), ".100g")) + self.assertEqual(sign + "1" + (200 * "0") + "." + (98 * "6") + "7", + format(k * (10**200 + F(2, 3)), ".300g")) + + self.assertEqual(sign + "1.0006666666666666666666666666666667e+35", + format(k * (1 + F(2, 3000)) * 10**35, '.35g')) + + self.assertEqual(sign + "1." + (99 * "6") + "7e+100", + format(k * (1 + F(2, 3)) * 10**100, '.100e')) + + def testFormattingWithDecimalContext(self): + # Decimal context should not affect formatting of Fractions + import decimal + round_half_up_ctx = decimal.Context(rounding=decimal.ROUND_HALF_UP) + round_half_down_ctx = decimal.Context(rounding=decimal.ROUND_HALF_DOWN) + round_half_even_ctx = decimal.Context(rounding=decimal.ROUND_HALF_EVEN) + d0 = decimal.Decimal("0.05") + d1 = decimal.Decimal("0.15") + d2 = decimal.Decimal("0.25") + d3 = decimal.Decimal("0.35") + f0 = F(5, 100) + f1 = F(15, 100) + f2 = F(25, 100) + f3 = F(35, 100) + self.assertEqual("0.05", format(f0, ".2f")) + self.assertEqual("0.15", format(f1, ".2f")) + self.assertEqual("0.25", format(f2, ".2f")) + self.assertEqual("0.35", format(f3, ".2f")) + self.assertEqual("0.0", format(f0, ".1f")) + self.assertEqual("0.2", format(f1, ".1f")) + self.assertEqual("0.2", format(f2, ".1f")) + self.assertEqual("0.4", format(f3, ".1f")) + with decimal.localcontext(round_half_up_ctx): + self.assertEqual("0.0", format(f0, ".1f")) + self.assertEqual("0.2", format(f1, ".1f")) + self.assertEqual("0.2", format(f2, ".1f")) + self.assertEqual("0.4", format(f3, ".1f")) + self.assertEqual("0.1", format(d0, ".1f")) + self.assertEqual("0.2", format(d1, ".1f")) + self.assertEqual("0.3", format(d2, ".1f")) + self.assertEqual("0.4", format(d3, ".1f")) + with decimal.localcontext(round_half_down_ctx): + self.assertEqual("0.0", format(f0, ".1f")) + self.assertEqual("0.2", format(f1, ".1f")) + self.assertEqual("0.2", format(f2, ".1f")) + self.assertEqual("0.4", format(f3, ".1f")) + self.assertEqual("0.0", format(d0, ".1f")) + self.assertEqual("0.1", format(d1, ".1f")) + self.assertEqual("0.2", format(d2, ".1f")) + self.assertEqual("0.3", format(d3, ".1f")) + with decimal.localcontext(round_half_even_ctx): + self.assertEqual("0.0", format(f0, ".1f")) + self.assertEqual("0.2", format(f1, ".1f")) + self.assertEqual("0.2", format(f2, ".1f")) + self.assertEqual("0.4", format(f3, ".1f")) + self.assertEqual("0.0", format(d0, ".1f")) + self.assertEqual("0.2", format(d1, ".1f")) + self.assertEqual("0.2", format(d2, ".1f")) + self.assertEqual("0.4", format(d3, ".1f")) + + + def testFormattingErrors(self): + data = [ + (tuple('bcdoxX'), + 'Unknown format code {!r} for Fraction object'), + (('z', '1.2.3', '--', 'foobar'), + 'Invalid format specifier: {}'), + ] + for (specifiers, error_msg_template) in data: + for specifier in specifiers: + error_msg = error_msg_template.format(specifier) + with self.assertRaisesRegexp(ValueError, error_msg): + format(F(1, 2), specifier) + def testHash(self): hmod = sys.hash_info.modulus hinf = sys.hash_info.inf diff --git a/Modules/_decimal/_decimal.c b/Modules/_decimal/_decimal.c index 169914c..a8d6411 100644 --- a/Modules/_decimal/_decimal.c +++ b/Modules/_decimal/_decimal.c @@ -3159,15 +3159,16 @@ dotsep_as_utf8(const char *s) /* Formatted representation of a PyDecObject. */ static PyObject * -dec_format(PyObject *dec, PyObject *args) +dec_format(PyObject *dec, PyObject *args, PyObject *kwargs) { PyObject *result = NULL; + static char *_keywords[] = {"specifier", "context", "_localeconv", NULL}; PyObject *override = NULL; PyObject *dot = NULL; PyObject *sep = NULL; PyObject *grouping = NULL; PyObject *fmtarg; - PyObject *context; + PyObject *context = Py_None; mpd_spec_t spec; char *fmt; char *decstring = NULL; @@ -3175,11 +3176,11 @@ dec_format(PyObject *dec, PyObject *args) int replace_fillchar = 0; Py_ssize_t size; - - CURRENT_CONTEXT(context); - if (!PyArg_ParseTuple(args, "O|O", &fmtarg, &override)) { + if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O|OO", _keywords, + &fmtarg, &context, &override)) { return NULL; } + CONTEXT_CHECK_VA(context); if (PyUnicode_Check(fmtarg)) { fmt = PyUnicode_AsUTF8AndSize(fmtarg, &size); @@ -3240,8 +3241,15 @@ dec_format(PyObject *dec, PyObject *args) spec.sep = PyBytes_AS_STRING(sep); } if ((grouping = PyDict_GetItemString(override, "grouping"))) { - if ((grouping = PyUnicode_AsUTF8String(grouping)) == NULL) { - goto finish; + if (PyUnicode_Check(grouping)) { + if ((grouping = PyUnicode_AsUTF8String(grouping)) == NULL) { + goto finish; + } + } + else { + if ((grouping = PyBytes_FromObject(grouping)) == NULL) { + goto finish; + } } spec.grouping = PyBytes_AS_STRING(grouping); } @@ -4692,7 +4700,7 @@ static PyMethodDef dec_methods [] = /* Special methods */ { "__copy__", dec_copy, METH_NOARGS, NULL }, { "__deepcopy__", dec_copy, METH_O, NULL }, - { "__format__", dec_format, METH_VARARGS, NULL }, + { "__format__", (PyCFunction)dec_format, METH_VARARGS|METH_KEYWORDS, NULL }, { "__reduce__", dec_reduce, METH_NOARGS, NULL }, { "__round__", PyDec_Round, METH_VARARGS, NULL }, { "__ceil__", dec_ceil, METH_NOARGS, NULL },