Testing all of the already proposed solutions, I find they either
- convert to and from strings, which is inefficient
- can't handle negative numbers
- can't handle arrays
- have some numerical errors.
Here's my attempt at a solution which should handle all of these things. (Edit 2020-03-18: added np.asarray
as suggested by A. West.)
def signif(x, p):
x = np.asarray(x)
x_positive = np.where(np.isfinite(x) & (x != 0), np.abs(x), 10**(p-1))
mags = 10 ** (p - 1 - np.floor(np.log10(x_positive)))
return np.round(x * mags) / mags
Testing:
def scottgigante(x, p):
x_positive = np.where(np.isfinite(x) & (x != 0), np.abs(x), 10**(p-1))
mags = 10 ** (p - 1 - np.floor(np.log10(x_positive)))
return np.round(x * mags) / mags
def awest(x,p):
return float(f'%.{p-1}e'%x)
def denizb(x,p):
return float(('%.' + str(p-1) + 'e') % x)
def autumn(x, p):
return np.format_float_positional(x, precision=p, unique=False, fractional=False, trim='k')
def greg(x, p):
return round(x, -int(np.floor(np.sign(x) * np.log10(abs(x)))) + p-1)
def user11336338(x, p):
xr = (np.floor(np.log10(np.abs(x)))).astype(int)
xr=10.**xr*np.around(x/10.**xr,p-1)
return xr
def dmon(x, p):
if np.all(np.isfinite(x)):
eset = np.seterr(all='ignore')
mags = 10.0**np.floor(np.log10(np.abs(x))) # omag's
x = np.around(x/mags,p-1)*mags # round(val/omag)*omag
np.seterr(**eset)
x = np.where(np.isnan(x), 0.0, x) # 0.0 -> nan -> 0.0
return x
def seanlake(x, p):
__logBase10of2 = 3.010299956639811952137388947244930267681898814621085413104274611e-1
xsgn = np.sign(x)
absx = xsgn * x
mantissa, binaryExponent = np.frexp( absx )
decimalExponent = __logBase10of2 * binaryExponent
omag = np.floor(decimalExponent)
mantissa *= 10.0**(decimalExponent - omag)
if mantissa < 1.0:
mantissa *= 10.0
omag -= 1.0
return xsgn * np.around( mantissa, decimals=p - 1 ) * 10.0**omag
solns = [scottgigante, awest, denizb, autumn, greg, user11336338, dmon, seanlake]
xs = [
1.114, # positive, round down
1.115, # positive, round up
-1.114, # negative
1.114e-30, # extremely small
1.114e30, # extremely large
0, # zero
float('inf'), # infinite
[1.114, 1.115e-30], # array input
]
p = 3
print('input:', xs)
for soln in solns:
print(f'{soln.__name__}', end=': ')
for x in xs:
try:
print(soln(x, p), end=', ')
except Exception as e:
print(type(e).__name__, end=', ')
print()
Results:
input: [1.114, 1.115, -1.114, 1.114e-30, 1.114e+30, 0, inf, [1.114, 1.115e-30]]
scottgigante: 1.11, 1.12, -1.11, 1.11e-30, 1.11e+30, 0.0, inf, [1.11e+00 1.12e-30],
awest: 1.11, 1.11, -1.11, 1.11e-30, 1.11e+30, 0.0, inf, TypeError,
denizb: 1.11, 1.11, -1.11, 1.11e-30, 1.11e+30, 0.0, inf, TypeError,
autumn: 1.11, 1.11, -1.11, 0.00000000000000000000000000000111, 1110000000000000000000000000000., 0.00, inf, TypeError,
greg: 1.11, 1.11, -1.114, 1.11e-30, 1.11e+30, ValueError, OverflowError, TypeError,
user11336338: 1.11, 1.12, -1.11, 1.1100000000000002e-30, 1.1100000000000001e+30, nan, nan, [1.11e+00 1.12e-30],
dmon: 1.11, 1.12, -1.11, 1.1100000000000002e-30, 1.1100000000000001e+30, 0.0, inf, [1.11e+00 1.12e-30],
seanlake: 1.11, 1.12, -1.11, 1.1100000000000002e-30, 1.1100000000000001e+30, 0.0, inf, ValueError,
Timing:
def test_soln(soln):
try:
soln(np.linspace(1, 100, 1000), 3)
except Exception:
[soln(x, 3) for x in np.linspace(1, 100, 1000)]
for soln in solns:
print(soln.__name__)
%timeit test_soln(soln)
Results:
scottgigante
135 µs ± 15.3 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
awest
2.23 ms ± 430 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
denizb
2.18 ms ± 352 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
autumn
2.92 ms ± 206 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
greg
14.1 ms ± 1.21 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)
user11336338
157 µs ± 50.1 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
dmon
142 µs ± 8.52 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)
seanlake
20.7 ms ± 994 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)