You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

367 lines
9.0 KiB

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Humanizing functions for numbers."""
import re
from fractions import Fraction
from . import compat
from .i18n import gettext as _
from .i18n import gettext_noop as N_
from .i18n import pgettext as P_
from .i18n import thousands_separator
def ordinal(value):
"""Converts an integer to its ordinal as a string.
For example, 1 is "1st", 2 is "2nd", 3 is "3rd", etc. Works for any integer or
anything `int()` will turn into an integer. Anything other value will have nothing
done to it.
Examples:
```pycon
>>> ordinal(1)
'1st'
>>> ordinal(1002)
'1002nd'
>>> ordinal(103)
'103rd'
>>> ordinal(4)
'4th'
>>> ordinal(12)
'12th'
>>> ordinal(101)
'101st'
>>> ordinal(111)
'111th'
>>> ordinal("something else")
'something else'
>>> ordinal(None) is None
True
```
Args:
value (int, str, float): Integer to convert.
Returns:
str: Ordinal string.
"""
try:
value = int(value)
except (TypeError, ValueError):
return value
t = (
P_("0", "th"),
P_("1", "st"),
P_("2", "nd"),
P_("3", "rd"),
P_("4", "th"),
P_("5", "th"),
P_("6", "th"),
P_("7", "th"),
P_("8", "th"),
P_("9", "th"),
)
if value % 100 in (11, 12, 13): # special case
return "%d%s" % (value, t[0])
return "%d%s" % (value, t[value % 10])
def intcomma(value, ndigits=None):
"""Converts an integer to a string containing commas every three digits.
For example, 3000 becomes "3,000" and 45000 becomes "45,000". To maintain some
compatibility with Django's `intcomma`, this function also accepts floats.
Examples:
``pycon
>>> intcomma(100)
'100'
>>> intcomma("1000")
'1,000'
>>> intcomma(1_000_000)
'1,000,000'
>>> intcomma(1_234_567.25)
'1,234,567.25'
>>> intcomma(1234.5454545, 2)
'1,234.55'
>>> intcomma(14308.40, 1)
'14,308.4'
>>> intcomma(None) is None
True
```
Args:
value (int, float, str): Integer or float to convert.
ndigits (int, None): Digits of precision for rounding after the decimal point.
Returns:
str: string containing commas every three digits.
"""
sep = thousands_separator()
try:
if isinstance(value, compat.string_types):
float(value.replace(sep, ""))
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>%s\g<2>" % sep, orig)
if orig == new:
return new
else:
return intcomma(new)
powers = [10 ** x for x in (6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 100)]
human_powers = (
N_("million"),
N_("billion"),
N_("trillion"),
N_("quadrillion"),
N_("quintillion"),
N_("sextillion"),
N_("septillion"),
N_("octillion"),
N_("nonillion"),
N_("decillion"),
N_("googol"),
)
def intword(value, format="%.1f"):
"""Converts a large integer to a friendly text representation.
Works best for numbers over 1 million. For example, 1_000_000 becomes "1.0 million",
1200000 becomes "1.2 million" and "1_200_000_000" becomes "1.2 billion". Supports up
to decillion (33 digits) and googol (100 digits).
Examples:
```pycon
>>> intword("100")
'100'
>>> intword("1000000")
'1.0 million'
>>> intword(1_200_000_000)
'1.2 billion'
>>> intword(8100000000000000000000000000000000)
'8.1 decillion'
>>> intword(None) is None
True
>>> intword("1234000", "%0.3f")
'1.234 million'
```
Args:
value (int, float, str): Integer to convert.
format (str): To change the number of decimal or general format of the number
portion.
Returns:
str: Friendly text representation as a string, unless the value passed could not
be coaxed into an `int`.
"""
try:
value = int(value)
except (TypeError, ValueError):
return value
if value < powers[0]:
return str(value)
for ordinal, power in enumerate(powers[1:], 1):
if value < power:
chopped = value / float(powers[ordinal - 1])
if float(format % chopped) == float(10 ** 3):
chopped = value / float(powers[ordinal])
return (" ".join([format, _(human_powers[ordinal])])) % chopped
else:
return (" ".join([format, _(human_powers[ordinal - 1])])) % chopped
return str(value)
def apnumber(value):
"""Converts an integer to Associated Press style.
Examples:
```pycon
>>> apnumber(0)
'zero'
>>> apnumber(5)
'five'
>>> apnumber(10)
'10'
>>> apnumber("7")
'seven'
>>> apnumber("foo")
'foo'
>>> apnumber(None) is None
True
```
Args:
value (int, float, str): Integer to convert.
Returns:
str: For numbers 0-9, the number spelled out. Otherwise, the number. This always
returns a string unless the value was not `int`-able, unlike the Django filter.
"""
try:
value = int(value)
except (TypeError, ValueError):
return value
if not 0 <= value < 10:
return str(value)
return (
_("zero"),
_("one"),
_("two"),
_("three"),
_("four"),
_("five"),
_("six"),
_("seven"),
_("eight"),
_("nine"),
)[value]
def fractional(value):
"""Convert to fractional number.
There will be some cases where one might not want to show ugly decimal places for
floats and decimals.
This function returns a human-readable fractional number in form of fractions and
mixed fractions.
Pass in a string, or a number or a float, and this function returns:
* a string representation of a fraction
* or a whole number
* or a mixed fraction
Examples:
```pycon
>>> fractional(0.3)
'3/10'
>>> fractional(1.3)
'1 3/10'
>>> fractional(float(1/3))
'1/3'
>>> fractional(1)
'1'
>>> fractional("ten")
'ten'
>>> fractional(None) is None
True
```
Args:
value (int, float, str): Integer to convert.
Returns:
str: Fractional number as a string.
"""
try:
number = float(value)
except (TypeError, ValueError):
return value
whole_number = int(number)
frac = Fraction(number - whole_number).limit_denominator(1000)
numerator = frac._numerator
denominator = frac._denominator
if whole_number and not numerator and denominator == 1:
# this means that an integer was passed in
# (or variants of that integer like 1.0000)
return "%.0f" % whole_number
elif not whole_number:
return "{:.0f}/{:.0f}".format(numerator, denominator)
else:
return "{:.0f} {:.0f}/{:.0f}".format(whole_number, numerator, denominator)
def scientific(value, precision=2):
"""Return number in string scientific notation z.wq x 10ⁿ.
Examples:
```pycon
>>> scientific(float(0.3))
'3.00 x 10⁻¹'
>>> scientific(int(500))
'5.00 x 10²'
>>> scientific(-1000)
'1.00 x 10⁻³'
>>> scientific(1000, 1)
'1.0 x 10³'
>>> scientific(1000, 3)
'1.000 x 10³'
>>> scientific("99")
'9.90 x 10¹'
>>> scientific("foo")
'foo'
>>> scientific(None) is None
True
```
Args:
value (int, float, str): Input number.
precision (int): Number of decimal for first part of the number.
Returns:
str: Number in scientific notation z.wq 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")
if "-0" in part2:
part2 = part2.replace("-0", "-")
if "+0" in part2:
part2 = part2.replace("+0", "")
new_part2 = []
if negative:
new_part2.append(exponents["-"])
for char in part2:
new_part2.append(exponents[char])
final_str = part1 + " x 10" + "".join(new_part2)
return final_str