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.
gristlabs_grist-core/sandbox/grist/functions/math.py

852 lines
16 KiB

# pylint: disable=unused-argument
from __future__ import absolute_import
import math as _math
import operator
import os
import random
import uuid
from functools import reduce
from six.moves import zip, xrange
import six
from functions.info import ISNUMBER, ISLOGICAL
from functions.unimplemented import unimplemented
import roman
if os.environ.get("DETERMINISTIC_MODE"):
random.seed(1)
# Iterates through elements of iterable arguments, or through individual args when not iterable.
def _chain(*values_or_iterables):
for v in values_or_iterables:
try:
for x in v:
yield x
except TypeError:
yield v
# Iterates through iterable or other arguments, skipping non-numeric ones.
def _chain_numeric(*values_or_iterables):
for v in _chain(*values_or_iterables):
if ISNUMBER(v) and not ISLOGICAL(v):
yield v
# Iterates through iterable or other arguments, replacing non-numeric ones with 0 (or True with 1).
def _chain_numeric_a(*values_or_iterables):
for v in _chain(*values_or_iterables):
yield int(v) if ISLOGICAL(v) else v if ISNUMBER(v) else 0
def _round_toward_zero(value):
return _math.floor(value) if value >= 0 else _math.ceil(value)
def _round_away_from_zero(value):
return _math.ceil(value) if value >= 0 else _math.floor(value)
def ABS(value):
"""
Returns the absolute value of a number.
>>> ABS(2)
2
>>> ABS(-2)
2
>>> ABS(-4)
4
"""
return abs(value)
def ACOS(value):
"""
Returns the inverse cosine of a value, in radians.
>>> round(ACOS(-0.5), 9)
2.094395102
>>> round(ACOS(-0.5)*180/PI(), 10)
120.0
"""
return _math.acos(value)
def ACOSH(value):
"""
Returns the inverse hyperbolic cosine of a number.
>>> ACOSH(1)
0.0
>>> round(ACOSH(10), 7)
2.9932228
"""
return _math.acosh(value)
def ARABIC(roman_numeral):
"""
Computes the value of a Roman numeral.
>>> ARABIC("LVII")
57
>>> ARABIC('mcmxii')
1912
"""
return roman.fromRoman(roman_numeral.upper())
def ASIN(value):
"""
Returns the inverse sine of a value, in radians.
>>> round(ASIN(-0.5), 9)
-0.523598776
>>> round(ASIN(-0.5)*180/PI(), 10)
-30.0
>>> round(DEGREES(ASIN(-0.5)), 10)
-30.0
"""
return _math.asin(value)
def ASINH(value):
"""
Returns the inverse hyperbolic sine of a number.
>>> round(ASINH(-2.5), 9)
-1.647231146
>>> round(ASINH(10), 9)
2.99822295
"""
return _math.asinh(value)
def ATAN(value):
"""
Returns the inverse tangent of a value, in radians.
>>> round(ATAN(1), 9)
0.785398163
>>> ATAN(1)*180/PI()
45.0
>>> DEGREES(ATAN(1))
45.0
"""
return _math.atan(value)
def ATAN2(x, y):
"""
Returns the angle between the x-axis and a line segment from the origin (0,0) to specified
coordinate pair (`x`,`y`), in radians.
>>> round(ATAN2(1, 1), 9)
0.785398163
>>> round(ATAN2(-1, -1), 9)
-2.35619449
>>> ATAN2(-1, -1)*180/PI()
-135.0
>>> DEGREES(ATAN2(-1, -1))
-135.0
>>> round(ATAN2(1,2), 9)
1.107148718
"""
return _math.atan2(y, x)
def ATANH(value):
"""
Returns the inverse hyperbolic tangent of a number.
>>> round(ATANH(0.76159416), 9)
1.00000001
>>> round(ATANH(-0.1), 9)
-0.100335348
"""
return _math.atanh(value)
def CEILING(value, factor=1):
"""
Rounds a number up to the nearest multiple of factor, or the nearest integer if the factor is
omitted or 1.
>>> CEILING(2.5, 1)
3
>>> CEILING(-2.5, -2)
-4
>>> CEILING(-2.5, 2)
-2
>>> CEILING(1.5, 0.1)
1.5
>>> CEILING(0.234, 0.01)
0.24
"""
return int(_math.ceil(float(value) / factor)) * factor
def COMBIN(n, k):
"""
Returns the number of ways to choose some number of objects from a pool of a given size of
objects.
>>> COMBIN(8,2)
28
>>> COMBIN(4,2)
6
>>> COMBIN(10,7)
120
"""
# From http://stackoverflow.com/a/4941932/328565
k = min(k, n-k)
if k == 0:
return 1
numer = reduce(operator.mul, xrange(n, n-k, -1))
denom = reduce(operator.mul, xrange(1, k+1))
return numer//denom
def COS(angle):
"""
Returns the cosine of an angle provided in radians.
>>> round(COS(1.047), 7)
0.5001711
>>> round(COS(60*PI()/180), 10)
0.5
>>> round(COS(RADIANS(60)), 10)
0.5
"""
return _math.cos(angle)
def COSH(value):
"""
Returns the hyperbolic cosine of any real number.
>>> round(COSH(4), 6)
27.308233
>>> round(COSH(EXP(1)), 7)
7.6101251
"""
return _math.cosh(value)
def DEGREES(angle):
"""
Converts an angle value in radians to degrees.
>>> round(DEGREES(ACOS(-0.5)), 10)
120.0
>>> DEGREES(PI())
180.0
"""
return _math.degrees(angle)
def EVEN(value):
"""
Rounds a number up to the nearest even integer, rounding away from zero.
>>> EVEN(1.5)
2
>>> EVEN(3)
4
>>> EVEN(2)
2
>>> EVEN(-1)
-2
"""
return int(_round_away_from_zero(float(value) / 2)) * 2
def EXP(exponent):
"""
Returns Euler's number, e (~2.718) raised to a power.
>>> round(EXP(1), 8)
2.71828183
>>> round(EXP(2), 7)
7.3890561
"""
return _math.exp(exponent)
def FACT(value):
"""
Returns the factorial of a number.
>>> FACT(5)
120
>>> FACT(1.9)
1
>>> FACT(0)
1
>>> FACT(1)
1
>>> FACT(-1)
Traceback (most recent call last):
...
ValueError: factorial() not defined for negative values
"""
return _math.factorial(int(value))
def FACTDOUBLE(value):
"""
Returns the "double factorial" of a number.
>>> FACTDOUBLE(6)
48
>>> FACTDOUBLE(7)
105
>>> FACTDOUBLE(3)
3
>>> FACTDOUBLE(4)
8
"""
return reduce(operator.mul, xrange(value, 1, -2))
def FLOOR(value, factor=1):
"""
Rounds a number down to the nearest integer multiple of specified significance.
>>> FLOOR(3.7,2)
2
>>> FLOOR(-2.5,-2)
-2
>>> FLOOR(2.5,-2)
Traceback (most recent call last):
...
ValueError: factor argument invalid
>>> FLOOR(1.58,0.1)
1.5
>>> FLOOR(0.234,0.01)
0.23
"""
if (factor < 0) != (value < 0):
raise ValueError("factor argument invalid")
return int(_math.floor(float(value) / factor)) * factor
def _gcd(a, b):
while a != 0:
if a > b:
a, b = b, a
a, b = b % a, a
return b
def GCD(value1, *more_values):
"""
Returns the greatest common divisor of one or more integers.
>>> GCD(5, 2)
1
>>> GCD(24, 36)
12
>>> GCD(7, 1)
1
>>> GCD(5, 0)
5
>>> GCD(0, 5)
5
>>> GCD(5)
5
>>> GCD(14, 42, 21)
7
"""
values = [v for v in (value1,) + more_values if v]
if not values:
return 0
if any(v < 0 for v in values):
raise ValueError("gcd requires non-negative values")
return reduce(_gcd, map(int, values))
def INT(value):
"""
Rounds a number down to the nearest integer that is less than or equal to it.
>>> INT(8.9)
8
>>> INT(-8.9)
-9
>>> 19.5-INT(19.5)
0.5
"""
return int(_math.floor(value))
def _lcm(a, b):
return a * b // _gcd(a, b)
def LCM(value1, *more_values):
"""
Returns the least common multiple of one or more integers.
>>> LCM(5, 2)
10
>>> LCM(24, 36)
72
>>> LCM(0, 5)
0
>>> LCM(5)
5
>>> LCM(10, 100)
100
>>> LCM(12, 18)
36
>>> LCM(12, 18, 24)
72
"""
values = (value1,) + more_values
if any(v < 0 for v in values):
raise ValueError("gcd requires non-negative values")
if any(v == 0 for v in values):
return 0
return reduce(_lcm, map(int, values))
def LN(value):
"""
Returns the the logarithm of a number, base e (Euler's number).
>>> round(LN(86), 7)
4.4543473
>>> round(LN(2.7182818), 7)
1.0
>>> round(LN(EXP(3)), 10)
3.0
"""
return _math.log(value)
def LOG(value, base=10):
"""
Returns the the logarithm of a number given a base.
>>> LOG(10)
1.0
>>> LOG(8, 2)
3.0
>>> round(LOG(86, 2.7182818), 7)
4.4543473
"""
return _math.log(value, base)
def LOG10(value):
"""
Returns the the logarithm of a number, base 10.
>>> round(LOG10(86), 9)
1.934498451
>>> LOG10(10)
1.0
>>> LOG10(100000)
5.0
>>> LOG10(10**5)
5.0
"""
return _math.log10(value)
def MOD(dividend, divisor):
"""
Returns the result of the modulo operator, the remainder after a division operation.
>>> MOD(3, 2)
1
>>> MOD(-3, 2)
1
>>> MOD(3, -2)
-1
>>> MOD(-3, -2)
-1
"""
return dividend % divisor
def MROUND(value, factor):
"""
Rounds one number to the nearest integer multiple of another.
>>> MROUND(10, 3)
9
>>> MROUND(-10, -3)
-9
>>> round(MROUND(1.3, 0.2), 10)
1.4
>>> MROUND(5, -2)
Traceback (most recent call last):
...
ValueError: factor argument invalid
"""
if (factor < 0) != (value < 0):
raise ValueError("factor argument invalid")
return int(_round_toward_zero(float(value) / factor + 0.5)) * factor
def MULTINOMIAL(value1, *more_values):
"""
Returns the factorial of the sum of values divided by the product of the values' factorials.
>>> MULTINOMIAL(2, 3, 4)
1260
>>> MULTINOMIAL(3)
1
>>> MULTINOMIAL(1,2,3)
60
>>> MULTINOMIAL(0,2,4,6)
13860
"""
s = value1
res = 1
for v in more_values:
s += v
res *= COMBIN(s, v)
return res
def ODD(value):
"""
Rounds a number up to the nearest odd integer.
>>> ODD(1.5)
3
>>> ODD(3)
3
>>> ODD(2)
3
>>> ODD(-1)
-1
>>> ODD(-2)
-3
"""
return int(_round_away_from_zero(float(value + 1) / 2)) * 2 - 1
def PI():
"""
Returns the value of Pi to 14 decimal places.
>>> round(PI(), 9)
3.141592654
>>> round(PI()/2, 9)
1.570796327
>>> round(PI()*9, 8)
28.27433388
"""
return _math.pi
def POWER(base, exponent):
"""
Returns a number raised to a power.
>>> POWER(5,2)
25.0
>>> round(POWER(98.6,3.2), 3)
2401077.222
>>> round(POWER(4,5.0/4), 9)
5.656854249
"""
return _math.pow(base, exponent)
def PRODUCT(factor1, *more_factors):
"""
Returns the result of multiplying a series of numbers together. Each argument may be a number or
an array.
>>> PRODUCT([5,15,30])
2250
>>> PRODUCT([5,15,30], 2)
4500
>>> PRODUCT(5,15,[30],[2])
4500
More tests:
>>> PRODUCT([2, True, None, "", False, "0", 5])
10
>>> PRODUCT([2, True, None, "", False, 0, 5])
0
"""
return reduce(operator.mul, _chain_numeric(factor1, *more_factors))
def QUOTIENT(dividend, divisor):
"""
Returns one number divided by another.
>>> QUOTIENT(5, 2)
2
>>> QUOTIENT(4.5, 3.1)
1
>>> QUOTIENT(-10, 3)
-3
"""
return TRUNC(float(dividend) / divisor)
def RADIANS(angle):
"""
Converts an angle value in degrees to radians.
>>> round(RADIANS(270), 6)
4.712389
"""
return _math.radians(angle)
def RAND():
"""
Returns a random number between 0 inclusive and 1 exclusive.
"""
return random.random()
def RANDBETWEEN(low, high):
"""
Returns a uniformly random integer between two values, inclusive.
"""
return random.randrange(low, high + 1)
def ROMAN(number, form_unused=None):
"""
Formats a number in Roman numerals. The second argument is ignored in this implementation.
>>> ROMAN(499,0)
'CDXCIX'
>>> ROMAN(499.2,0)
'CDXCIX'
>>> ROMAN(57)
'LVII'
>>> ROMAN(1912)
'MCMXII'
"""
# TODO: Maybe we should support the second argument.
return roman.toRoman(int(number))
def ROUND(value, places=0):
"""
Rounds a number to a certain number of decimal places according to standard rules.
>>> ROUND(2.15, 1) # Excel actually gives the more correct 2.2
2.1
>>> ROUND(2.149, 1)
2.1
>>> ROUND(-1.475, 2)
-1.48
>>> ROUND(21.5, -1)
20.0
>>> ROUND(626.3,-3)
1000.0
>>> ROUND(1.98,-1)
0.0
>>> ROUND(-50.55,-2)
-100.0
"""
# TODO: Excel manages to round 2.15 to 2.2, but Python sees 2.149999... and rounds to 2.1
# (see Python notes in documentation of `round()`).
return round(value, places)
def ROUNDDOWN(value, places=0):
"""
Rounds a number to a certain number of decimal places, always rounding down towards zero.
>>> ROUNDDOWN(3.2, 0)
3
>>> ROUNDDOWN(76.9,0)
76
>>> ROUNDDOWN(3.14159, 3)
3.141
>>> ROUNDDOWN(-3.14159, 1)
-3.1
>>> ROUNDDOWN(31415.92654, -2)
31400
"""
factor = 10**-places
return int(_round_toward_zero(float(value) / factor)) * factor
def ROUNDUP(value, places=0):
"""
Rounds a number to a certain number of decimal places, always rounding up away from zero.
>>> ROUNDUP(3.2,0)
4
>>> ROUNDUP(76.9,0)
77
>>> ROUNDUP(3.14159, 3)
3.142
>>> ROUNDUP(-3.14159, 1)
-3.2
>>> ROUNDUP(31415.92654, -2)
31500
"""
factor = 10**-places
return int(_round_away_from_zero(float(value) / factor)) * factor
def SERIESSUM(x, n, m, a):
"""
Given parameters x, n, m, and a, returns the power series sum a_1*x^n + a_2*x^(n+m)
+ ... + a_i*x^(n+(i-1)m), where i is the number of entries in range `a`.
>>> SERIESSUM(1,0,1,1)
1
>>> SERIESSUM(2,1,0,[1,2,3])
12
>>> SERIESSUM(-3,1,1,[2,4,6])
-132
>>> round(SERIESSUM(PI()/4,0,2,[1,-1./FACT(2),1./FACT(4),-1./FACT(6)]), 6)
0.707103
"""
return sum(coef*pow(x, n+i*m) for i, coef in enumerate(_chain(a)))
def SIGN(value):
"""
Given an input number, returns `-1` if it is negative, `1` if positive, and `0` if it is zero.
>>> SIGN(10)
1
>>> SIGN(4.0-4.0)
0
>>> SIGN(-0.00001)
-1
"""
return 0 if value == 0 else int(_math.copysign(1, value))
def SIN(angle):
"""
Returns the sine of an angle provided in radians.
>>> round(SIN(PI()), 10)
0.0
>>> SIN(PI()/2)
1.0
>>> round(SIN(30*PI()/180), 10)
0.5
>>> round(SIN(RADIANS(30)), 10)
0.5
"""
return _math.sin(angle)
def SINH(value):
"""
Returns the hyperbolic sine of any real number.
>>> round(2.868*SINH(0.0342*1.03), 7)
0.1010491
"""
return _math.sinh(value)
def SQRT(value):
"""
Returns the positive square root of a positive number.
>>> SQRT(16)
4.0
>>> SQRT(-16)
Traceback (most recent call last):
...
ValueError: math domain error
>>> SQRT(ABS(-16))
4.0
"""
return _math.sqrt(value)
def SQRTPI(value):
"""
Returns the positive square root of the product of Pi and the given positive number.
>>> round(SQRTPI(1), 6)
1.772454
>>> round(SQRTPI(2), 6)
2.506628
"""
return _math.sqrt(_math.pi * value)
@unimplemented
def SUBTOTAL(function_code, range1, range2):
"""
Returns a subtotal for a vertical range of cells using a specified aggregation function.
"""
raise NotImplementedError()
def SUM(value1, *more_values):
"""
Returns the sum of a series of numbers. Each argument may be a number or an array.
Non-numeric values are ignored.
>>> SUM([5,15,30])
50
>>> SUM([5.,15,30], 2)
52.0
>>> SUM(5,15,[30],[2])
52
More tests:
>>> SUM([10.25, None, "", False, "other", 20.5])
30.75
>>> SUM([True, "3", 4], True)
6
"""
return sum(_chain_numeric_a(value1, *more_values))
@unimplemented
def SUMIF(records, criterion, sum_range):
"""
Returns a conditional sum across a range.
"""
raise NotImplementedError()
@unimplemented
def SUMIFS(sum_range, criteria_range1, criterion1, *args):
"""
Returns the sum of a range depending on multiple criteria.
"""
raise NotImplementedError()
def SUMPRODUCT(array1, *more_arrays):
"""
Multiplies corresponding components in the given arrays, and returns the sum of those products.
>>> SUMPRODUCT([3,8,1,4,6,9], [2,6,5,7,7,3])
156
>>> SUMPRODUCT([], [], [])
0
>>> SUMPRODUCT([-0.25], [-2], [-3])
-1.5
>>> SUMPRODUCT([-0.25, -0.25], [-2, -2], [-3, -3])
-3.0
"""
return sum(reduce(operator.mul, values) for values in zip(array1, *more_arrays))
@unimplemented
def SUMSQ(value1, value2):
"""
Returns the sum of the squares of a series of numbers and/or cells.
"""
raise NotImplementedError()
def TAN(angle):
"""
Returns the tangent of an angle provided in radians.
>>> round(TAN(0.785), 8)
0.99920399
>>> round(TAN(45*PI()/180), 10)
1.0
>>> round(TAN(RADIANS(45)), 10)
1.0
"""
return _math.tan(angle)
def TANH(value):
"""
Returns the hyperbolic tangent of any real number.
>>> round(TANH(-2), 6)
-0.964028
>>> TANH(0)
0.0
>>> round(TANH(0.5), 6)
0.462117
"""
return _math.tanh(value)
def TRUNC(value, places=0):
"""
Truncates a number to a certain number of significant digits by omitting less significant
digits.
>>> TRUNC(8.9)
8
>>> TRUNC(-8.9)
-8
>>> TRUNC(0.45)
0
"""
# TRUNC seems indistinguishable from ROUNDDOWN.
return ROUNDDOWN(value, places)
def UUID():
"""Generate a random UUID-formatted string identifier."""
if six.PY2:
return str(uuid.UUID(bytes=[chr(random.randrange(0, 256)) for _ in xrange(0, 16)], version=4))
else:
return str(uuid.UUID(bytes=bytes([random.randrange(0, 256) for _ in range(0, 16)]), version=4))