-
Notifications
You must be signed in to change notification settings - Fork 14
/
notation.py
156 lines (125 loc) · 4.25 KB
/
notation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# encoding: utf-8
import decimal
from math import copysign, floor, log
from .statistics import median
from .utils import ceil as _ceil
from .utils import floor as _floor
from .utils import bound, isnan, repel, replace, unwrap, vectorize
def order(value, base=10):
if isnan(value):
return None
elif value == 0:
return 0
else:
power = log(repel(value), base)
return int(floor(power))
def e(exponent):
value = str(abs(exponent))
if exponent < 0:
return 'E-' + value
elif exponent > 0:
return 'E+' + value
else:
return ''
def upcast(new, old):
try:
import pandas
if isinstance(old, pandas.Series):
return pandas.Series(new, index=old.index)
else:
return new
except ImportError:
return new
def extent(values, precision=3, statistic=median):
l = min(values)
r = max(values)
exponent = order(statistic(values))
digits = precision - exponent
return (_floor(l, digits), _ceil(r, digits))
# TODO: just have this be an array and then zip this with
# range(-len(SI)//2*3, len(SI)//2*3, 3)
SI = {
24: 'Y',
21: 'Z',
18: 'E',
15: 'P',
12: 'T',
9: 'G',
6: 'M',
3: 'K',
0: '',
-3: 'm',
-6: 'µ',
-9: 'n',
-12: 'p',
-15: 'f',
-18: 'a',
-21: 'z',
-24: 'y',
}
@unwrap
@vectorize
def human(value, digits=2):
return '{:,}'.format(round(value, digits))
@unwrap
@vectorize
def scientific(value, precision=3):
display = decimal.Context(prec=precision)
value = decimal.Decimal(value).normalize(context=display)
return display.to_sci_string(value)
@unwrap
@vectorize
def engineering(value, precision=3, prefix=False, prefixes=SI):
""" Convert a number to engineering notation. """
display = decimal.Context(prec=precision)
value = decimal.Decimal(value).normalize(context=display)
string = value.to_eng_string()
if prefix:
prefixes = {e(exponent): prefix for exponent, prefix in prefixes.items()}
return replace(string, prefixes)
else:
return string
@unwrap
def business(values, precision=3, prefix=True, prefixes=SI, statistic=median, default=''):
"""
Convert a list of numbers to the engineering notation appropriate to a
reference point like the minimum, the median or the mean --
think of it as "business notation".
Any number will have at most the amount of significant digits of the
reference point, that is, the function will round beyond the
decimal point.
For example, if the reference is `233K`, this function will turn
1,175,125 into `1180K` and 11,234 into `11K` (instead of 1175K and
11.2K respectively.) This can help enormously with readability.
If the reference point is equal to or larger than E15 or
equal to or smaller than E-15, E12 and E-12 become the
reference point instead. (Petas and femtos are too
unfamiliar to people to be easily comprehended.)
"""
reference = statistic(values)
if not reference:
return upcast([''] * len(values), values)
exponent = order(reference)
e = bound(exponent - exponent % 3, -12, 12)
# the amount of decimals is the precision minus the amount of digits
# before the decimal point, which is one more than the relative order
# of magnitude (for example, 10^5 can be represented as 100K, with
# those three digits representing place values of 10^3, 10^4 and 10^5)
d = precision - (1 + exponent - e)
prefix = prefixes[e]
strings = []
for value in values:
if isnan(value):
strings.append('')
else:
normalized = value / 10.0 ** e
# use `round` for rounding (beyond the decimal point if necessary)
# use string formatting for padding to the right amount of decimals
# and to hide decimals when necessary (by default, floats are always
# displayed with a single decimal place, to distinguish them from
# integers)
relative_order = order(value) - exponent
places = min(d - relative_order, d)
normalized = round(normalized, places)
strings.append('{0:,.{1}f}'.format(normalized, d) + prefix)
return upcast(strings, values)