gristlabs_grist-core/sandbox/grist/functions/math.py
Paul Fitzpatrick b82eec714a (core) move data engine code to core
Summary:
this moves sandbox/grist to core, and adds a requirements.txt
file for reconstructing the content of sandbox/thirdparty.

Test Plan:
existing tests pass.
Tested core functionality manually.  Tested docker build manually.

Reviewers: dsagal

Reviewed By: dsagal

Differential Revision: https://phab.getgrist.com/D2563
2020-07-29 08:57:25 -04:00

831 lines
16 KiB
Python

# pylint: disable=unused-argument
from __future__ import absolute_import
import itertools
import math as _math
import operator
import random
from functions.info import ISNUMBER, ISLOGICAL
import roman
# 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)
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))
def SUMIF(records, criterion, sum_range):
"""
Returns a conditional sum across a range.
"""
raise NotImplementedError()
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 itertools.izip(array1, *more_arrays))
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)