"""Humanizing functions for numbers.""" import math import re from fractions import Fraction powers = [10**x for x in (3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)] human_powers = ( "thousand", "million", "billion", "trillion", "quadrillion", "quintillion", "sextillion", "septillion", "octillion", "nonillion", "decillion", "googol", ) def ordinal(value): """Convert an integer to its ordinal string (1 → '1st', 2 → '2nd', etc.). Examples: >>> ordinal(1) '1st' >>> ordinal(12) '12th' >>> ordinal(103) '103rd' """ try: value = int(value) except (TypeError, ValueError): return value t = ("th", "st", "nd", "rd", "th", "th", "th", "th", "th", "th") if value % 100 in (11, 12, 13): return f"{value}th" return f"{value}{t[value % 10]}" def intcomma(value, ndigits=None): """Convert an integer to a string with commas every three digits. Examples: >>> intcomma(1000000) '1,000,000' >>> intcomma(1234567.25) '1,234,567.25' """ try: if isinstance(value, str): float(value.replace(",", "")) else: float(value) except (TypeError, ValueError): return value if ndigits: orig = "{0:.{1}f}".format(value, ndigits) else: orig = str(value) new = re.sub(r"^(-?\d+)(\d{3})", r"\g<1>,\g<2>", orig) if orig == new: return new return intcomma(new) def intword(value, format="%.1f"): """Convert a large integer to a friendly text representation. Examples: >>> intword(1000000) '1.0 million' >>> intword(1200000000) '1.2 billion' """ try: value = int(value) except (TypeError, ValueError): return value if value < powers[0]: return str(value) for ordinal_idx, power in enumerate(powers[1:], 1): if value < power: chopped = value / float(powers[ordinal_idx - 1]) count = math.ceil(chopped) label = human_powers[ordinal_idx - 1] plural = label + "s" if count != 1 else label if float(format % chopped) == float(10**3): chopped = value / float(powers[ordinal_idx]) count = math.ceil(chopped) label = human_powers[ordinal_idx] plural = label + "s" if count != 1 else label return (format + " %s") % (chopped, plural) return (format + " %s") % (chopped, plural) return str(value) def apnumber(value): """Convert integers 0–9 to their AP-style word equivalents. Examples: >>> apnumber(5) 'five' >>> apnumber(10) '10' """ words = ("zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine") try: value = int(value) except (TypeError, ValueError): return value if not 0 <= value < 10: return str(value) return words[value] def fractional(value): """Convert a float to a human-readable fractional string. Examples: >>> fractional(0.3) '3/10' >>> fractional(1.3) '1 3/10' >>> fractional(1) '1' """ try: number = float(value) except (TypeError, ValueError): return value whole = int(number) frac = Fraction(number - whole).limit_denominator(1000) n, d = frac.numerator, frac.denominator if whole and not n and d == 1: return f"{whole:.0f}" elif not whole: return f"{n:.0f}/{d:.0f}" return f"{whole:.0f} {n:.0f}/{d:.0f}" def scientific(value, precision=2): """Return a number in scientific notation (e.g. 5.00 x 10²). Examples: >>> scientific(500) '5.00 x 10²' >>> scientific(0.3) '3.00 x 10⁻¹' """ exponents = { "0": "⁰", "1": "¹", "2": "²", "3": "³", "4": "⁴", "5": "⁵", "6": "⁶", "7": "⁷", "8": "⁸", "9": "⁹", "+": "⁺", "-": "⁻", } negative = False try: if "-" in str(value): value = str(value).replace("-", "") negative = True if isinstance(value, str): value = float(value) fmt = "{:.%se}" % str(int(precision)) n = fmt.format(value) except (ValueError, TypeError): return value part1, part2 = n.split("e") part2 = part2.replace("-0", "-").replace("+0", "") new_part2 = [] if negative: new_part2.append(exponents["-"]) for char in part2: new_part2.append(exponents[char]) return part1 + " x 10" + "".join(new_part2) def clamp(value, format="{:}", floor=None, ceil=None, floor_token="<", ceil_token=">"): """Return a number formatted and clamped between floor and ceil. Examples: >>> clamp(123.456) '123.456' >>> clamp(0.001, floor=0.01) '<0.01' >>> clamp(999, ceil=100) '>100' """ if value is None: return None if floor is not None and value < floor: value, token = floor, floor_token elif ceil is not None and value > ceil: value, token = ceil, ceil_token else: token = "" if isinstance(format, str): return token + format.format(value) elif callable(format): return token + format(value) raise ValueError("format must be a string or callable")