@@ -38,13 +38,14 @@ cdef extern from *:
38
38
cdef long long PY_LLONG_MIN, PY_LLONG_MAX
39
39
cdef long long MAX_SMALL_NUMBER " (PY_LLONG_MAX / 100)"
40
40
41
- cdef object Rational, Integral, Real, Complex, Decimal, math, operator, sys
41
+ cdef object Rational, Integral, Real, Complex, Decimal, math, operator, re, sys
42
42
cdef object PY_MAX_LONG_LONG = PY_LLONG_MAX
43
43
44
44
from numbers import Rational, Integral, Real, Complex
45
45
from decimal import Decimal
46
46
import math
47
47
import operator
48
+ import re
48
49
import sys
49
50
50
51
cdef bint _decimal_supports_integer_ratio = hasattr (Decimal, " as_integer_ratio" ) # Py3.6+
@@ -237,6 +238,99 @@ except AttributeError: # pre Py3.2
237
238
_PyHASH_INF = hash (float (' +inf' ))
238
239
239
240
241
+ # Helpers for formatting
242
+
243
+ cdef _round_to_exponent(n, d, exponent, bint no_neg_zero = False ):
244
+ """ Round a rational number to the nearest multiple of a given power of 10.
245
+
246
+ Rounds the rational number n/d to the nearest integer multiple of
247
+ 10**exponent, rounding to the nearest even integer multiple in the case of
248
+ a tie. Returns a pair (sign: bool, significand: int) representing the
249
+ rounded value (-1)**sign * significand * 10**exponent.
250
+
251
+ If no_neg_zero is true, then the returned sign will always be False when
252
+ the significand is zero. Otherwise, the sign reflects the sign of the
253
+ input.
254
+
255
+ d must be positive, but n and d need not be relatively prime.
256
+ """
257
+ if exponent >= 0 :
258
+ d *= 10 ** exponent
259
+ else :
260
+ n *= 10 **- exponent
261
+
262
+ # The divmod quotient is correct for round-ties-towards-positive-infinity;
263
+ # In the case of a tie, we zero out the least significant bit of q.
264
+ q, r = divmod (n + (d >> 1 ), d)
265
+ if r == 0 and d & 1 == 0 :
266
+ q &= - 2
267
+
268
+ cdef bint sign = q < 0 if no_neg_zero else n < 0
269
+ return sign, abs (q)
270
+
271
+
272
+ cdef _round_to_figures(n, d, Py_ssize_t figures):
273
+ """ Round a rational number to a given number of significant figures.
274
+
275
+ Rounds the rational number n/d to the given number of significant figures
276
+ using the round-ties-to-even rule, and returns a triple
277
+ (sign: bool, significand: int, exponent: int) representing the rounded
278
+ value (-1)**sign * significand * 10**exponent.
279
+
280
+ In the special case where n = 0, returns a significand of zero and
281
+ an exponent of 1 - figures, for compatibility with formatting.
282
+ Otherwise, the returned significand satisfies
283
+ 10**(figures - 1) <= significand < 10**figures.
284
+
285
+ d must be positive, but n and d need not be relatively prime.
286
+ figures must be positive.
287
+ """
288
+ # Special case for n == 0.
289
+ if n == 0 :
290
+ return False , 0 , 1 - figures
291
+
292
+ cdef bint sign
293
+
294
+ # Find integer m satisfying 10**(m - 1) <= abs(n)/d <= 10**m. (If abs(n)/d
295
+ # is a power of 10, either of the two possible values for m is fine.)
296
+ str_n, str_d = str (abs (n)), str (d)
297
+ cdef Py_ssize_t m = len (str_n) - len (str_d) + (str_d <= str_n)
298
+
299
+ # Round to a multiple of 10**(m - figures). The significand we get
300
+ # satisfies 10**(figures - 1) <= significand <= 10**figures.
301
+ exponent = m - figures
302
+ sign, significand = _round_to_exponent(n, d, exponent)
303
+
304
+ # Adjust in the case where significand == 10**figures, to ensure that
305
+ # 10**(figures - 1) <= significand < 10**figures.
306
+ if len (str (significand)) == figures + 1 :
307
+ significand //= 10
308
+ exponent += 1
309
+
310
+ return sign, significand, exponent
311
+
312
+
313
+ # Pattern for matching float-style format specifications;
314
+ # supports 'e', 'E', 'f', 'F', 'g', 'G' and '%' presentation types.
315
+ cdef object _FLOAT_FORMAT_SPECIFICATION_MATCHER = re.compile(r """
316
+ (?:
317
+ ( ?P<fill> . ) ?
318
+ ( ?P<align> [<>=^ ])
319
+ ) ?
320
+ ( ?P<sign> [-+ ]? )
321
+ ( ?P<no_neg_zero> z) ?
322
+ ( ?P<alt> \# ) ?
323
+ # A '0' that's * not* followed by another digit is parsed as a minimum width
324
+ # rather than a zeropad flag.
325
+ ( ?P<zeropad> 0(?= [0-9 ]) ) ?
326
+ ( ?P<minimumwidth> 0| [1-9 ][0-9 ]* ) ?
327
+ ( ?P<thousands_sep> [,_ ]) ?
328
+ (?: \. ( ?P<precision> 0| [1-9 ][0-9 ]* ) ) ?
329
+ ( ?P<presentation_type> [eEfFgG% ])
330
+ $
331
+ """ , re.DOTALL | re.VERBOSE).match
332
+
333
+
240
334
cdef class Fraction:
241
335
""" A Rational number.
242
336
@@ -495,9 +589,132 @@ cdef class Fraction:
495
589
""" str(self)"""
496
590
if self ._denominator == 1 :
497
591
return str (self ._numerator)
592
+ elif PY_MAJOR_VERSION > 2 :
593
+ return f' {self._numerator}/{self._denominator}'
498
594
else :
499
595
return ' %s /%s ' % (self ._numerator, self ._denominator)
500
596
597
+ def __format__ (self , format_spec , /):
598
+ """ Format this fraction according to the given format specification."""
599
+
600
+ # Backwards compatibility with existing formatting.
601
+ if not format_spec:
602
+ return str (self )
603
+
604
+ # Validate and parse the format specifier.
605
+ match = _FLOAT_FORMAT_SPECIFICATION_MATCHER(format_spec)
606
+ if match is None :
607
+ raise ValueError (
608
+ f" Invalid format specifier {format_spec!r} "
609
+ f" for object of type {type(self).__name__!r}"
610
+ )
611
+ match = match.groupdict() # Py2
612
+ if match[" align" ] is not None and match[" zeropad" ] is not None :
613
+ # Avoid the temptation to guess.
614
+ raise ValueError (
615
+ f" Invalid format specifier {format_spec!r} "
616
+ f" for object of type {type(self).__name__!r}; "
617
+ " can't use explicit alignment when zero-padding"
618
+ )
619
+ fill = match[" fill" ] or " "
620
+ align = match[" align" ] or " >"
621
+ pos_sign = " " if match[" sign" ] == " -" else match[" sign" ]
622
+ cdef bint no_neg_zero = match[" no_neg_zero" ]
623
+ cdef bint alternate_form = match[" alt" ]
624
+ cdef bint zeropad = match[" zeropad" ]
625
+ cdef Py_ssize_t minimumwidth = int (match[" minimumwidth" ] or " 0" )
626
+ thousands_sep = match[" thousands_sep" ]
627
+ cdef Py_ssize_t precision = int (match[" precision" ] or " 6" )
628
+ cdef Py_UCS4 presentation_type = ord (match[" presentation_type" ])
629
+ cdef bint trim_zeros = presentation_type in u " gG" and not alternate_form
630
+ cdef bint trim_point = not alternate_form
631
+ exponent_indicator = " E" if presentation_type in u " EFG" else " e"
632
+
633
+ cdef bint negative, scientific
634
+ cdef Py_ssize_t exponent, figures
635
+
636
+ # Round to get the digits we need, figure out where to place the point,
637
+ # and decide whether to use scientific notation. 'point_pos' is the
638
+ # relative to the _end_ of the digit string: that is, it's the number
639
+ # of digits that should follow the point.
640
+ if presentation_type in u " fF%" :
641
+ exponent = - precision
642
+ if presentation_type == u " %" :
643
+ exponent -= 2
644
+ negative, significand = _round_to_exponent(
645
+ self ._numerator, self ._denominator, exponent, no_neg_zero)
646
+ scientific = False
647
+ point_pos = precision
648
+ else : # presentation_type in "eEgG"
649
+ figures = (
650
+ max (precision, 1 )
651
+ if presentation_type in u " gG"
652
+ else precision + 1
653
+ )
654
+ negative, significand, exponent = _round_to_figures(
655
+ self ._numerator, self ._denominator, figures)
656
+ scientific = (
657
+ presentation_type in u " eE"
658
+ or exponent > 0
659
+ or exponent + figures <= - 4
660
+ )
661
+ point_pos = figures - 1 if scientific else - exponent
662
+
663
+ # Get the suffix - the part following the digits, if any.
664
+ if presentation_type == u " %" :
665
+ suffix = " %"
666
+ elif scientific:
667
+ # suffix = f"{exponent_indicator}{exponent + point_pos:+03d}"
668
+ suffix = " %s%+03d " % (exponent_indicator, exponent + point_pos)
669
+ else :
670
+ suffix = " "
671
+
672
+ # String of output digits, padded sufficiently with zeros on the left
673
+ # so that we'll have at least one digit before the decimal point.
674
+ digits = f" {significand:0{point_pos + 1}d}"
675
+
676
+ # Before padding, the output has the form f"{sign}{leading}{trailing}",
677
+ # where `leading` includes thousands separators if necessary and
678
+ # `trailing` includes the decimal separator where appropriate.
679
+ sign = " -" if negative else pos_sign
680
+ leading = digits[: len (digits) - point_pos]
681
+ frac_part = digits[len (digits) - point_pos :]
682
+ if trim_zeros:
683
+ frac_part = frac_part.rstrip(" 0" )
684
+ separator = " " if trim_point and not frac_part else " ."
685
+ trailing = separator + frac_part + suffix
686
+
687
+ # Do zero padding if required.
688
+ if zeropad:
689
+ min_leading = minimumwidth - len (sign) - len (trailing)
690
+ # When adding thousands separators, they'll be added to the
691
+ # zero-padded portion too, so we need to compensate.
692
+ leading = leading.zfill(
693
+ 3 * min_leading // 4 + 1 if thousands_sep else min_leading
694
+ )
695
+
696
+ # Insert thousands separators if required.
697
+ if thousands_sep:
698
+ first_pos = 1 + (len (leading) - 1 ) % 3
699
+ leading = leading[:first_pos] + " " .join([
700
+ thousands_sep + leading[pos : pos + 3 ]
701
+ for pos in range (first_pos, len (leading), 3 )
702
+ ])
703
+
704
+ # We now have a sign and a body. Pad with fill character if necessary
705
+ # and return.
706
+ body = leading + trailing
707
+ padding = fill * (minimumwidth - len (sign) - len (body))
708
+ if align == " >" :
709
+ return padding + sign + body
710
+ elif align == " <" :
711
+ return sign + body + padding
712
+ elif align == " ^" :
713
+ half = len (padding) // 2
714
+ return padding[:half] + sign + body + padding[half:]
715
+ else : # align == "="
716
+ return sign + padding + body
717
+
501
718
def __add__ (a , b ):
502
719
""" a + b"""
503
720
return forward(a, b, _add, _math_op_add)
@@ -1211,7 +1428,9 @@ cdef enum ParserState:
1211
1428
1212
1429
cdef _raise_invalid_input(s):
1213
1430
s = repr (s)
1214
- if s[0 ] == ' b' :
1431
+ if s[:2 ] in (' b"' , " b'" ):
1432
+ s = s[1 :]
1433
+ elif PY_MAJOR_VERSION == 2 and s[:2 ] in (' u"' , " u'" ):
1215
1434
s = s[1 :]
1216
1435
raise ValueError (f' Invalid literal for Fraction: {s}' ) from None
1217
1436
0 commit comments