mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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
This commit is contained in:
12
sandbox/grist/functions/__init__.py
Normal file
12
sandbox/grist/functions/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# pylint: disable=wildcard-import
|
||||
from date import *
|
||||
from info import *
|
||||
from logical import *
|
||||
from lookup import *
|
||||
from math import *
|
||||
from stats import *
|
||||
from text import *
|
||||
from schedule import *
|
||||
|
||||
# Export all uppercase names, for use with `from functions import *`.
|
||||
__all__ = [k for k in dir() if not k.startswith('_') and k.isupper()]
|
||||
773
sandbox/grist/functions/date.py
Normal file
773
sandbox/grist/functions/date.py
Normal file
@@ -0,0 +1,773 @@
|
||||
import calendar
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import moment
|
||||
import docmodel
|
||||
|
||||
# pylint: disable=no-member
|
||||
|
||||
_excel_date_zero = datetime.datetime(1899, 12, 30)
|
||||
|
||||
|
||||
def _make_datetime(value):
|
||||
if isinstance(value, datetime.datetime):
|
||||
return value
|
||||
elif isinstance(value, datetime.date):
|
||||
return datetime.datetime.combine(value, datetime.time())
|
||||
elif isinstance(value, datetime.time):
|
||||
return datetime.datetime.combine(datetime.date.today(), value)
|
||||
elif isinstance(value, basestring):
|
||||
return dateutil.parser.parse(value)
|
||||
else:
|
||||
raise ValueError('Invalid date %r' % (value,))
|
||||
|
||||
def _get_global_tz():
|
||||
if docmodel.global_docmodel:
|
||||
return docmodel.global_docmodel.doc_info.lookupOne(id=1).tzinfo
|
||||
return moment.TZ_UTC
|
||||
|
||||
def _get_tzinfo(zonelabel):
|
||||
"""
|
||||
A helper that returns a `datetime.tzinfo` instance for zonelabel. Returns the global
|
||||
document timezone if zonelabel is None.
|
||||
"""
|
||||
return moment.tzinfo(zonelabel) if zonelabel else _get_global_tz()
|
||||
|
||||
def DTIME(value, tz=None):
|
||||
"""
|
||||
Returns the value converted to a python `datetime` object. The value may be a
|
||||
`string`, `date` (interpreted as midnight on that day), `time` (interpreted as a
|
||||
time-of-day today), or an existing `datetime`.
|
||||
|
||||
The returned `datetime` will have its timezone set to the `tz` argument, or the
|
||||
document's default timezone when `tz` is omitted or None. If the input is itself a
|
||||
`datetime` with the timezone set, it is returned unchanged (no changes to its timezone).
|
||||
|
||||
>>> DTIME(datetime.date(2017, 1, 1))
|
||||
datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DTIME(datetime.date(2017, 1, 1), 'Europe/Paris')
|
||||
datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('Europe/Paris'))
|
||||
>>> DTIME(datetime.datetime(2017, 1, 1))
|
||||
datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC')))
|
||||
datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC'))
|
||||
>>> DTIME(datetime.datetime(2017, 1, 1, tzinfo=moment.tzinfo('UTC')), 'Europe/Paris')
|
||||
datetime.datetime(2017, 1, 1, 0, 0, tzinfo=moment.tzinfo('UTC'))
|
||||
>>> DTIME("1/1/2008")
|
||||
datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
"""
|
||||
value = _make_datetime(value)
|
||||
return value if value.tzinfo else value.replace(tzinfo=_get_tzinfo(tz))
|
||||
|
||||
|
||||
def XL_TO_DATE(value, tz=None):
|
||||
"""
|
||||
Converts a provided Excel serial number representing a date into a `datetime` object.
|
||||
Value is interpreted as the number of days since December 30, 1899.
|
||||
|
||||
(This corresponds to Google Sheets interpretation. Excel starts with Dec. 31, 1899 but wrongly
|
||||
considers 1900 to be a leap year. Excel for Mac should be configured to use 1900 date system,
|
||||
i.e. uncheck "Use the 1904 date system" option.)
|
||||
|
||||
The returned `datetime` will have its timezone set to the `tz` argument, or the
|
||||
document's default timezone when `tz` is omitted or None.
|
||||
|
||||
>>> XL_TO_DATE(41100.1875)
|
||||
datetime.datetime(2012, 7, 10, 4, 30, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> XL_TO_DATE(39448)
|
||||
datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> XL_TO_DATE(40982.0625)
|
||||
datetime.datetime(2012, 3, 14, 1, 30, tzinfo=moment.tzinfo('America/New_York'))
|
||||
|
||||
More tests:
|
||||
>>> XL_TO_DATE(0)
|
||||
datetime.datetime(1899, 12, 30, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> XL_TO_DATE(-1)
|
||||
datetime.datetime(1899, 12, 29, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> XL_TO_DATE(1)
|
||||
datetime.datetime(1899, 12, 31, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> XL_TO_DATE(1.5)
|
||||
datetime.datetime(1899, 12, 31, 12, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> XL_TO_DATE(61.0)
|
||||
datetime.datetime(1900, 3, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
"""
|
||||
return DTIME(_excel_date_zero, tz) + datetime.timedelta(days=value)
|
||||
|
||||
|
||||
def DATE_TO_XL(date_value):
|
||||
"""
|
||||
Converts a Python `date` or `datetime` object to the serial number as used by
|
||||
Excel, with December 30, 1899 as serial number 1.
|
||||
|
||||
See XL_TO_DATE for more explanation.
|
||||
|
||||
>>> DATE_TO_XL(datetime.date(2008, 1, 1))
|
||||
39448.0
|
||||
>>> DATE_TO_XL(datetime.date(2012, 3, 14))
|
||||
40982.0
|
||||
>>> DATE_TO_XL(datetime.datetime(2012, 3, 14, 1, 30))
|
||||
40982.0625
|
||||
|
||||
More tests:
|
||||
>>> DATE_TO_XL(datetime.date(1900, 1, 1))
|
||||
2.0
|
||||
>>> DATE_TO_XL(datetime.datetime(1900, 1, 1))
|
||||
2.0
|
||||
>>> DATE_TO_XL(datetime.datetime(1900, 1, 1, 12, 0))
|
||||
2.5
|
||||
>>> DATE_TO_XL(datetime.datetime(1900, 1, 1, 12, 0, tzinfo=moment.tzinfo('America/New_York')))
|
||||
2.5
|
||||
>>> DATE_TO_XL(datetime.date(1900, 3, 1))
|
||||
61.0
|
||||
>>> DATE_TO_XL(datetime.datetime(2008, 1, 1))
|
||||
39448.0
|
||||
>>> DATE_TO_XL(XL_TO_DATE(39488))
|
||||
39488.0
|
||||
>>> dt_ny = XL_TO_DATE(39488)
|
||||
>>> dt_paris = moment.tz(dt_ny, 'America/New_York').tz('Europe/Paris').datetime()
|
||||
>>> DATE_TO_XL(dt_paris)
|
||||
39488.0
|
||||
"""
|
||||
# If date_value is `naive` it's ok to pass tz to both DTIME as it won't affect the
|
||||
# result.
|
||||
return (DTIME(date_value) - DTIME(_excel_date_zero)).total_seconds() / 86400.
|
||||
|
||||
|
||||
def DATE(year, month, day):
|
||||
"""
|
||||
Returns the `datetime.datetime` object that represents a particular date.
|
||||
The DATE function is most useful in formulas where year, month, and day are formulas, not
|
||||
constants.
|
||||
|
||||
If year is between 0 and 1899 (inclusive), adds 1900 to calculate the year.
|
||||
>>> DATE(108, 1, 2)
|
||||
datetime.date(2008, 1, 2)
|
||||
>>> DATE(2008, 1, 2)
|
||||
datetime.date(2008, 1, 2)
|
||||
|
||||
If month is greater than 12, rolls into the following year.
|
||||
>>> DATE(2008, 14, 2)
|
||||
datetime.date(2009, 2, 2)
|
||||
|
||||
If month is less than 1, subtracts that many months plus 1, from the first month in the year.
|
||||
>>> DATE(2008, -3, 2)
|
||||
datetime.date(2007, 9, 2)
|
||||
|
||||
If day is greater than the number of days in the given month, rolls into the following months.
|
||||
>>> DATE(2008, 1, 35)
|
||||
datetime.date(2008, 2, 4)
|
||||
|
||||
If day is less than 1, subtracts that many days plus 1, from the first day of the given month.
|
||||
>>> DATE(2008, 1, -15)
|
||||
datetime.date(2007, 12, 16)
|
||||
|
||||
More tests:
|
||||
>>> DATE(1900, 1, 1)
|
||||
datetime.date(1900, 1, 1)
|
||||
>>> DATE(1900, 0, 0)
|
||||
datetime.date(1899, 11, 30)
|
||||
"""
|
||||
if year < 1900:
|
||||
year += 1900
|
||||
norm_month = (month - 1) % 12 + 1
|
||||
norm_year = year + (month - 1) // 12
|
||||
return datetime.date(norm_year, norm_month, 1) + datetime.timedelta(days=day - 1)
|
||||
|
||||
|
||||
def DATEDIF(start_date, end_date, unit):
|
||||
"""
|
||||
Calculates the number of days, months, or years between two dates.
|
||||
Unit indicates the type of information that you want returned:
|
||||
|
||||
- "Y": The number of complete years in the period.
|
||||
- "M": The number of complete months in the period.
|
||||
- "D": The number of days in the period.
|
||||
- "MD": The difference between the days in start_date and end_date. The months and years of the
|
||||
dates are ignored.
|
||||
- "YM": The difference between the months in start_date and end_date. The days and years of the
|
||||
dates are ignored.
|
||||
- "YD": The difference between the days of start_date and end_date. The years of the dates are
|
||||
ignored.
|
||||
|
||||
Two complete years in the period (2)
|
||||
>>> DATEDIF(DATE(2001, 1, 1), DATE(2003, 1, 1), "Y")
|
||||
2
|
||||
|
||||
440 days between June 1, 2001, and August 15, 2002 (440)
|
||||
>>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), "D")
|
||||
440
|
||||
|
||||
75 days between June 1 and August 15, ignoring the years of the dates (75)
|
||||
>>> DATEDIF(DATE(2001, 6, 1), DATE(2012, 8, 15), "YD")
|
||||
75
|
||||
|
||||
The difference between 1 and 15, ignoring the months and the years of the dates (14)
|
||||
>>> DATEDIF(DATE(2001, 6, 1), DATE(2002, 8, 15), "MD")
|
||||
14
|
||||
|
||||
More tests:
|
||||
>>> DATEDIF(DATE(1969, 7, 16), DATE(1969, 7, 24), "D")
|
||||
8
|
||||
>>> DATEDIF(DATE(2014, 1, 1), DATE(2015, 1, 1), "M")
|
||||
12
|
||||
>>> DATEDIF(DATE(2014, 1, 2), DATE(2015, 1, 1), "M")
|
||||
11
|
||||
>>> DATEDIF(DATE(2014, 1, 1), DATE(2024, 1, 1), "Y")
|
||||
10
|
||||
>>> DATEDIF(DATE(2014, 1, 2), DATE(2024, 1, 1), "Y")
|
||||
9
|
||||
>>> DATEDIF(DATE(1906, 10, 16), DATE(2004, 2, 3), "YM")
|
||||
3
|
||||
>>> DATEDIF(DATE(2016, 2, 14), DATE(2016, 3, 14), "YM")
|
||||
1
|
||||
>>> DATEDIF(DATE(2016, 2, 14), DATE(2016, 3, 13), "YM")
|
||||
0
|
||||
>>> DATEDIF(DATE(2008, 10, 16), DATE(2019, 12, 3), "MD")
|
||||
17
|
||||
>>> DATEDIF(DATE(2008, 11, 16), DATE(2019, 1, 3), "MD")
|
||||
18
|
||||
>>> DATEDIF(DATE(2016, 2, 29), DATE(2017, 2, 28), "Y")
|
||||
0
|
||||
>>> DATEDIF(DATE(2016, 2, 29), DATE(2017, 2, 29), "Y")
|
||||
1
|
||||
"""
|
||||
if isinstance(start_date, datetime.datetime):
|
||||
start_date = start_date.date()
|
||||
if isinstance(end_date, datetime.datetime):
|
||||
end_date = end_date.date()
|
||||
if unit == 'D':
|
||||
return (end_date - start_date).days
|
||||
elif unit == 'M':
|
||||
months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month)
|
||||
month_delta = 0 if start_date.day <= end_date.day else 1
|
||||
return months - month_delta
|
||||
elif unit == 'Y':
|
||||
years = end_date.year - start_date.year
|
||||
year_delta = 0 if (start_date.month, start_date.day) <= (end_date.month, end_date.day) else 1
|
||||
return years - year_delta
|
||||
elif unit == 'MD':
|
||||
month_delta = 0 if start_date.day <= end_date.day else 1
|
||||
return (end_date - DATE(end_date.year, end_date.month - month_delta, start_date.day)).days
|
||||
elif unit == 'YM':
|
||||
month_delta = 0 if start_date.day <= end_date.day else 1
|
||||
return (end_date.month - start_date.month - month_delta) % 12
|
||||
elif unit == 'YD':
|
||||
year_delta = 0 if (start_date.month, start_date.day) <= (end_date.month, end_date.day) else 1
|
||||
return (end_date - DATE(end_date.year - year_delta, start_date.month, start_date.day)).days
|
||||
else:
|
||||
raise ValueError('Invalid unit %s' % (unit,))
|
||||
|
||||
|
||||
def DATEVALUE(date_string, tz=None):
|
||||
"""
|
||||
Converts a date that is stored as text to a `datetime` object.
|
||||
|
||||
>>> DATEVALUE("1/1/2008")
|
||||
datetime.datetime(2008, 1, 1, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DATEVALUE("30-Jan-2008")
|
||||
datetime.datetime(2008, 1, 30, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DATEVALUE("2008-12-11")
|
||||
datetime.datetime(2008, 12, 11, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DATEVALUE("5-JUL").replace(year=2000)
|
||||
datetime.datetime(2000, 7, 5, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
|
||||
In case of ambiguity, prefer M/D/Y format.
|
||||
>>> DATEVALUE("1/2/3")
|
||||
datetime.datetime(2003, 1, 2, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
|
||||
More tests:
|
||||
>>> DATEVALUE("8/22/2011")
|
||||
datetime.datetime(2011, 8, 22, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DATEVALUE("22-MAY-2011")
|
||||
datetime.datetime(2011, 5, 22, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DATEVALUE("2011/02/23")
|
||||
datetime.datetime(2011, 2, 23, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DATEVALUE("11/3/2011")
|
||||
datetime.datetime(2011, 11, 3, 0, 0, tzinfo=moment.tzinfo('America/New_York'))
|
||||
>>> DATE_TO_XL(DATEVALUE("11/3/2011"))
|
||||
40850.0
|
||||
>>> DATEVALUE("asdf")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: Unknown string format
|
||||
"""
|
||||
return dateutil.parser.parse(date_string).replace(tzinfo=_get_tzinfo(tz))
|
||||
|
||||
|
||||
def DAY(date):
|
||||
"""
|
||||
Returns the day of a date, as an integer ranging from 1 to 31. Same as `date.day`.
|
||||
|
||||
>>> DAY(DATE(2011, 4, 15))
|
||||
15
|
||||
>>> DAY("5/31/2012")
|
||||
31
|
||||
>>> DAY(datetime.datetime(1900, 1, 1))
|
||||
1
|
||||
"""
|
||||
return _make_datetime(date).day
|
||||
|
||||
|
||||
def DAYS(end_date, start_date):
|
||||
"""
|
||||
Returns the number of days between two dates. Same as `(end_date - start_date).days`.
|
||||
|
||||
>>> DAYS("3/15/11","2/1/11")
|
||||
42
|
||||
>>> DAYS(DATE(2011, 12, 31), DATE(2011, 1, 1))
|
||||
364
|
||||
>>> DAYS("2/1/11", "3/15/11")
|
||||
-42
|
||||
"""
|
||||
return (_make_datetime(end_date) - _make_datetime(start_date)).days
|
||||
|
||||
|
||||
def EDATE(start_date, months):
|
||||
"""
|
||||
Returns the date that is the given number of months before or after `start_date`. Use
|
||||
EDATE to calculate maturity dates or due dates that fall on the same day of the month as the
|
||||
date of issue.
|
||||
|
||||
>>> EDATE(DATE(2011, 1, 15), 1)
|
||||
datetime.date(2011, 2, 15)
|
||||
>>> EDATE(DATE(2011, 1, 15), -1)
|
||||
datetime.date(2010, 12, 15)
|
||||
>>> EDATE(DATE(2011, 1, 15), 2)
|
||||
datetime.date(2011, 3, 15)
|
||||
>>> EDATE(DATE(2012, 3, 1), 10)
|
||||
datetime.date(2013, 1, 1)
|
||||
>>> EDATE(DATE(2012, 5, 1), -2)
|
||||
datetime.date(2012, 3, 1)
|
||||
"""
|
||||
return DATE(start_date.year, start_date.month + months, start_date.day)
|
||||
|
||||
|
||||
def DATEADD(start_date, days=0, months=0, years=0, weeks=0):
|
||||
"""
|
||||
Returns the date a given number of days, months, years, or weeks away from `start_date`. You may
|
||||
specify arguments in any order if you specify argument names. Use negative values to subtract.
|
||||
|
||||
For example, `DATEADD(date, 1)` is the same as `DATEADD(date, days=1)`, ands adds one day to
|
||||
`date`. `DATEADD(date, years=1, days=-1)` adds one year minus one day.
|
||||
|
||||
>>> DATEADD(DATE(2011, 1, 15), 1)
|
||||
datetime.date(2011, 1, 16)
|
||||
>>> DATEADD(DATE(2011, 1, 15), months=1, days=-1)
|
||||
datetime.date(2011, 2, 14)
|
||||
>>> DATEADD(DATE(2011, 1, 15), years=-2, months=1, days=3, weeks=2)
|
||||
datetime.date(2009, 3, 4)
|
||||
>>> DATEADD(DATE(1975, 4, 30), years=50, weeks=-5)
|
||||
datetime.date(2025, 3, 26)
|
||||
"""
|
||||
return DATE(start_date.year + years, start_date.month + months,
|
||||
start_date.day + days + weeks * 7)
|
||||
|
||||
|
||||
def EOMONTH(start_date, months):
|
||||
"""
|
||||
Returns the date for the last day of the month that is the indicated number of months before or
|
||||
after start_date. Use EOMONTH to calculate maturity dates or due dates that fall on the last day
|
||||
of the month.
|
||||
|
||||
>>> EOMONTH(DATE(2011, 1, 1), 1)
|
||||
datetime.date(2011, 2, 28)
|
||||
>>> EOMONTH(DATE(2011, 1, 15), -3)
|
||||
datetime.date(2010, 10, 31)
|
||||
>>> EOMONTH(DATE(2012, 3, 1), 10)
|
||||
datetime.date(2013, 1, 31)
|
||||
>>> EOMONTH(DATE(2012, 5, 1), -2)
|
||||
datetime.date(2012, 3, 31)
|
||||
"""
|
||||
return DATE(start_date.year, start_date.month + months + 1, 1) - datetime.timedelta(days=1)
|
||||
|
||||
|
||||
def HOUR(time):
|
||||
"""
|
||||
Returns the hour of a `datetime`, as an integer from 0 (12:00 A.M.) to 23 (11:00 P.M.).
|
||||
Same as `time.hour`.
|
||||
|
||||
>>> HOUR(XL_TO_DATE(0.75))
|
||||
18
|
||||
>>> HOUR("7/18/2011 7:45")
|
||||
7
|
||||
>>> HOUR("4/21/2012")
|
||||
0
|
||||
"""
|
||||
return _make_datetime(time).hour
|
||||
|
||||
|
||||
def ISOWEEKNUM(date):
|
||||
"""
|
||||
Returns the ISO week number of the year for a given date.
|
||||
|
||||
>>> ISOWEEKNUM("3/9/2012")
|
||||
10
|
||||
>>> [ISOWEEKNUM(DATE(2000 + y, 1, 1)) for y in [0,1,2,3,4,5,6,7,8]]
|
||||
[52, 1, 1, 1, 1, 53, 52, 1, 1]
|
||||
"""
|
||||
return _make_datetime(date).isocalendar()[1]
|
||||
|
||||
|
||||
def MINUTE(time):
|
||||
"""
|
||||
Returns the minutes of `datetime`, as an integer from 0 to 59.
|
||||
Same as `time.minute`.
|
||||
|
||||
>>> MINUTE(XL_TO_DATE(0.75))
|
||||
0
|
||||
>>> MINUTE("7/18/2011 7:45")
|
||||
45
|
||||
>>> MINUTE("12:59:00 PM")
|
||||
59
|
||||
>>> MINUTE(datetime.time(12, 58, 59))
|
||||
58
|
||||
"""
|
||||
return _make_datetime(time).minute
|
||||
|
||||
|
||||
def MONTH(date):
|
||||
"""
|
||||
Returns the month of a date represented, as an integer from from 1 (January) to 12 (December).
|
||||
Same as `date.month`.
|
||||
|
||||
>>> MONTH(DATE(2011, 4, 15))
|
||||
4
|
||||
>>> MONTH("5/31/2012")
|
||||
5
|
||||
>>> MONTH(datetime.datetime(1900, 1, 1))
|
||||
1
|
||||
"""
|
||||
return _make_datetime(date).month
|
||||
|
||||
|
||||
def NOW(tz=None):
|
||||
"""
|
||||
Returns the `datetime` object for the current time.
|
||||
"""
|
||||
return datetime.datetime.now(_get_tzinfo(tz))
|
||||
|
||||
|
||||
|
||||
def SECOND(time):
|
||||
"""
|
||||
Returns the seconds of `datetime`, as an integer from 0 to 59.
|
||||
Same as `time.second`.
|
||||
|
||||
>>> SECOND(XL_TO_DATE(0.75))
|
||||
0
|
||||
>>> SECOND("7/18/2011 7:45:13")
|
||||
13
|
||||
>>> SECOND(datetime.time(12, 58, 59))
|
||||
59
|
||||
"""
|
||||
|
||||
return _make_datetime(time).second
|
||||
|
||||
|
||||
def TODAY():
|
||||
"""
|
||||
Returns the `date` object for the current date.
|
||||
"""
|
||||
return datetime.date.today()
|
||||
|
||||
|
||||
_weekday_type_map = {
|
||||
# type: (first day of week (according to date.weekday()), number to return for it)
|
||||
1: (6, 1),
|
||||
2: (0, 1),
|
||||
3: (0, 0),
|
||||
11: (0, 1),
|
||||
12: (1, 1),
|
||||
13: (2, 1),
|
||||
14: (3, 1),
|
||||
15: (4, 1),
|
||||
16: (5, 1),
|
||||
17: (6, 1),
|
||||
}
|
||||
|
||||
def WEEKDAY(date, return_type=1):
|
||||
"""
|
||||
Returns the day of the week corresponding to a date. The day is given as an integer, ranging
|
||||
from 1 (Sunday) to 7 (Saturday), by default.
|
||||
|
||||
Return_type determines the type of the returned value.
|
||||
|
||||
- 1 (default) - Returns 1 (Sunday) through 7 (Saturday).
|
||||
- 2 - Returns 1 (Monday) through 7 (Sunday).
|
||||
- 3 - Returns 0 (Monday) through 6 (Sunday).
|
||||
- 11 - Returns 1 (Monday) through 7 (Sunday).
|
||||
- 12 - Returns 1 (Tuesday) through 7 (Monday).
|
||||
- 13 - Returns 1 (Wednesday) through 7 (Tuesday).
|
||||
- 14 - Returns 1 (Thursday) through 7 (Wednesday).
|
||||
- 15 - Returns 1 (Friday) through 7 (Thursday).
|
||||
- 16 - Returns 1 (Saturday) through 7 (Friday).
|
||||
- 17 - Returns 1 (Sunday) through 7 (Saturday).
|
||||
|
||||
>>> WEEKDAY(DATE(2008, 2, 14))
|
||||
5
|
||||
>>> WEEKDAY(DATE(2012, 3, 1))
|
||||
5
|
||||
>>> WEEKDAY(DATE(2012, 3, 1), 1)
|
||||
5
|
||||
>>> WEEKDAY(DATE(2012, 3, 1), 2)
|
||||
4
|
||||
>>> WEEKDAY("3/1/2012", 3)
|
||||
3
|
||||
|
||||
More tests:
|
||||
>>> WEEKDAY(XL_TO_DATE(10000), 1)
|
||||
4
|
||||
>>> WEEKDAY(DATE(1901, 1, 1))
|
||||
3
|
||||
>>> WEEKDAY(DATE(1901, 1, 1), 2)
|
||||
2
|
||||
>>> [WEEKDAY(DATE(2008, 2, d)) for d in [10, 11, 12, 13, 14, 15, 16, 17]]
|
||||
[1, 2, 3, 4, 5, 6, 7, 1]
|
||||
>>> [WEEKDAY(DATE(2008, 2, d), 1) for d in [10, 11, 12, 13, 14, 15, 16, 17]]
|
||||
[1, 2, 3, 4, 5, 6, 7, 1]
|
||||
>>> [WEEKDAY(DATE(2008, 2, d), 17) for d in [10, 11, 12, 13, 14, 15, 16, 17]]
|
||||
[1, 2, 3, 4, 5, 6, 7, 1]
|
||||
>>> [WEEKDAY(DATE(2008, 2, d), 2) for d in [10, 11, 12, 13, 14, 15, 16, 17]]
|
||||
[7, 1, 2, 3, 4, 5, 6, 7]
|
||||
>>> [WEEKDAY(DATE(2008, 2, d), 3) for d in [10, 11, 12, 13, 14, 15, 16, 17]]
|
||||
[6, 0, 1, 2, 3, 4, 5, 6]
|
||||
"""
|
||||
if return_type not in _weekday_type_map:
|
||||
raise ValueError("Invalid return type %s" % (return_type,))
|
||||
(first, index) = _weekday_type_map[return_type]
|
||||
return (_make_datetime(date).weekday() - first) % 7 + index
|
||||
|
||||
|
||||
def WEEKNUM(date, return_type=1):
|
||||
"""
|
||||
Returns the week number of a specific date. For example, the week containing January 1 is the
|
||||
first week of the year, and is numbered week 1.
|
||||
|
||||
Return_type determines which week is considered the first week of the year.
|
||||
|
||||
- 1 (default) - Week 1 is the first week starting Sunday that contains January 1.
|
||||
- 2 - Week 1 is the first week starting Monday that contains January 1.
|
||||
- 11 - Week 1 is the first week starting Monday that contains January 1.
|
||||
- 12 - Week 1 is the first week starting Tuesday that contains January 1.
|
||||
- 13 - Week 1 is the first week starting Wednesday that contains January 1.
|
||||
- 14 - Week 1 is the first week starting Thursday that contains January 1.
|
||||
- 15 - Week 1 is the first week starting Friday that contains January 1.
|
||||
- 16 - Week 1 is the first week starting Saturday that contains January 1.
|
||||
- 17 - Week 1 is the first week starting Sunday that contains January 1.
|
||||
- 21 - ISO 8601 Approach: Week 1 is the first week starting Monday that contains January 4.
|
||||
Equivalently, it is the week that contains the first Thursday of the year.
|
||||
|
||||
>>> WEEKNUM(DATE(2012, 3, 9))
|
||||
10
|
||||
>>> WEEKNUM(DATE(2012, 3, 9), 2)
|
||||
11
|
||||
>>> WEEKNUM('1/1/1900')
|
||||
1
|
||||
>>> WEEKNUM('2/1/1900')
|
||||
5
|
||||
|
||||
More tests:
|
||||
>>> WEEKNUM('2/1/1909', 2)
|
||||
6
|
||||
>>> WEEKNUM('1/1/1901', 21)
|
||||
1
|
||||
>>> [WEEKNUM(DATE(2012, 3, 9), t) for t in [1,2,11,12,13,14,15,16,17,21]]
|
||||
[10, 11, 11, 11, 11, 11, 11, 10, 10, 10]
|
||||
"""
|
||||
if return_type == 21:
|
||||
return ISOWEEKNUM(date)
|
||||
if return_type not in _weekday_type_map:
|
||||
raise ValueError("Invalid return type %s" % (return_type,))
|
||||
(first, index) = _weekday_type_map[return_type]
|
||||
date = _make_datetime(date)
|
||||
jan1 = datetime.datetime(date.year, 1, 1)
|
||||
week1_start = jan1 - datetime.timedelta(days=(jan1.weekday() - first) % 7)
|
||||
return (date - week1_start).days // 7 + 1
|
||||
|
||||
|
||||
def YEAR(date):
|
||||
"""
|
||||
Returns the year corresponding to a date as an integer.
|
||||
Same as `date.year`.
|
||||
|
||||
>>> YEAR(DATE(2011, 4, 15))
|
||||
2011
|
||||
>>> YEAR("5/31/2030")
|
||||
2030
|
||||
>>> YEAR(datetime.datetime(1900, 1, 1))
|
||||
1900
|
||||
"""
|
||||
return _make_datetime(date).year
|
||||
|
||||
|
||||
def _date_360(y, m, d):
|
||||
return y * 360 + m * 30 + d
|
||||
|
||||
def _last_of_feb(date):
|
||||
return date.month == 2 and (date + datetime.timedelta(days=1)).month == 3
|
||||
|
||||
def YEARFRAC(start_date, end_date, basis=0):
|
||||
"""
|
||||
Calculates the fraction of the year represented by the number of whole days between two dates.
|
||||
|
||||
Basis is the type of day count basis to use.
|
||||
|
||||
* `0` (default) - US (NASD) 30/360
|
||||
* `1` - Actual/actual
|
||||
* `2` - Actual/360
|
||||
* `3` - Actual/365
|
||||
* `4` - European 30/360
|
||||
* `-1` - Actual/actual (Google Sheets variation)
|
||||
|
||||
This function is useful for financial calculations. For compatibility with Excel, it defaults to
|
||||
using the NASD standard calendar. For use in non-financial settings, option `-1` is
|
||||
likely the best choice.
|
||||
|
||||
See <https://en.wikipedia.org/wiki/360-day_calendar> for explanation of
|
||||
the US 30/360 and European 30/360 methods. See <http://www.dwheeler.com/yearfrac/> for analysis of
|
||||
Excel's particular implementation.
|
||||
|
||||
Basis `-1` is similar to `1`, but differs from Excel when dates span both leap and non-leap years.
|
||||
It matches the calculation in Google Sheets, counting the days in each year as a fraction of
|
||||
that year's length.
|
||||
|
||||
Fraction of the year between 1/1/2012 and 7/30/12, omitting the Basis argument.
|
||||
>>> "%.8f" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30))
|
||||
'0.58055556'
|
||||
|
||||
Fraction between same dates, using the Actual/Actual basis argument. Because 2012 is a Leap
|
||||
year, it has a 366 day basis.
|
||||
>>> "%.8f" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 1)
|
||||
'0.57650273'
|
||||
|
||||
Fraction between same dates, using the Actual/365 basis argument. Uses a 365 day basis.
|
||||
>>> "%.8f" % YEARFRAC(DATE(2012, 1, 1), DATE(2012, 7, 30), 3)
|
||||
'0.57808219'
|
||||
|
||||
More tests:
|
||||
>>> round(YEARFRAC(DATE(2012, 1, 1), DATE(2012, 6, 30)), 10)
|
||||
0.4972222222
|
||||
>>> round(YEARFRAC(DATE(2012, 1, 1), DATE(2012, 6, 30), 0), 10)
|
||||
0.4972222222
|
||||
>>> round(YEARFRAC(DATE(2012, 1, 1), DATE(2012, 6, 30), 1), 10)
|
||||
0.4945355191
|
||||
>>> round(YEARFRAC(DATE(2012, 1, 1), DATE(2012, 6, 30), 2), 10)
|
||||
0.5027777778
|
||||
>>> round(YEARFRAC(DATE(2012, 1, 1), DATE(2012, 6, 30), 3), 10)
|
||||
0.495890411
|
||||
>>> round(YEARFRAC(DATE(2012, 1, 1), DATE(2012, 6, 30), 4), 10)
|
||||
0.4972222222
|
||||
>>> [YEARFRAC(DATE(2012, 1, 1), DATE(2012, 1, 1), t) for t in [0, 1, -1, 2, 3, 4]]
|
||||
[0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
||||
>>> [round(YEARFRAC(DATE(1985, 3, 15), DATE(2016, 2, 29), t), 6) for t in [0, 1, -1, 2, 3, 4]]
|
||||
[30.955556, 30.959617, 30.961202, 31.411111, 30.980822, 30.955556]
|
||||
>>> [round(YEARFRAC(DATE(2001, 2, 28), DATE(2016, 3, 31), t), 6) for t in [0, 1, -1, 2, 3, 4]]
|
||||
[15.086111, 15.085558, 15.086998, 15.305556, 15.09589, 15.088889]
|
||||
>>> [round(YEARFRAC(DATE(1968, 4, 7), DATE(2011, 2, 14), t), 6) for t in [0, 1, -1, 2, 3, 4]]
|
||||
[42.852778, 42.855578, 42.855521, 43.480556, 42.884932, 42.852778]
|
||||
|
||||
Here we test "basis 1" on leap and non-leap years.
|
||||
>>> [round(YEARFRAC(DATE(2015, 1, 1), DATE(2015, 3, 1), t), 6) for t in [1, -1]]
|
||||
[0.161644, 0.161644]
|
||||
>>> [round(YEARFRAC(DATE(2016, 1, 1), DATE(2016, 3, 1), t), 6) for t in [1, -1]]
|
||||
[0.163934, 0.163934]
|
||||
>>> [round(YEARFRAC(DATE(2015, 1, 1), DATE(2016, 1, 1), t), 6) for t in [1, -1]]
|
||||
[1.0, 1.0]
|
||||
>>> [round(YEARFRAC(DATE(2016, 1, 1), DATE(2017, 1, 1), t), 6) for t in [1, -1]]
|
||||
[1.0, 1.0]
|
||||
>>> [round(YEARFRAC(DATE(2016, 2, 29), DATE(2017, 1, 1), t), 6) for t in [1, -1]]
|
||||
[0.838798, 0.838798]
|
||||
>>> [round(YEARFRAC(DATE(2014, 12, 15), DATE(2015, 3, 15), t), 6) for t in [1, -1]]
|
||||
[0.246575, 0.246575]
|
||||
|
||||
For these examples, Google Sheets differs from Excel, and we match Excel here.
|
||||
>>> [round(YEARFRAC(DATE(2015, 12, 15), DATE(2016, 3, 15), t), 6) for t in [1, -1]]
|
||||
[0.248634, 0.248761]
|
||||
>>> [round(YEARFRAC(DATE(2015, 1, 1), DATE(2016, 2, 29), t), 6) for t in [1, -1]]
|
||||
[1.160055, 1.161202]
|
||||
>>> [round(YEARFRAC(DATE(2015, 1, 1), DATE(2016, 2, 28), t), 6) for t in [1, -1]]
|
||||
[1.157319, 1.15847]
|
||||
>>> [round(YEARFRAC(DATE(2015, 3, 1), DATE(2016, 2, 29), t), 6) for t in [1, -1]]
|
||||
[0.997268, 0.999558]
|
||||
>>> [round(YEARFRAC(DATE(2015, 3, 1), DATE(2016, 2, 28), t), 6) for t in [1, -1]]
|
||||
[0.99726, 0.996826]
|
||||
>>> [round(YEARFRAC(DATE(2016, 3, 1), DATE(2017, 1, 1), t), 6) for t in [1, -1]]
|
||||
[0.838356, 0.836066]
|
||||
>>> [round(YEARFRAC(DATE(2015, 1, 1), DATE(2017, 1, 1), t), 6) for t in [1, -1]]
|
||||
[2.000912, 2.0]
|
||||
"""
|
||||
# pylint: disable=too-many-return-statements
|
||||
# This function is actually completely crazy. The rules are strange too. We'll follow the logic
|
||||
# in http://www.dwheeler.com/yearfrac/excel-ooxml-yearfrac.pdf
|
||||
if start_date == end_date:
|
||||
return 0.0
|
||||
if start_date > end_date:
|
||||
start_date, end_date = end_date, start_date
|
||||
|
||||
d1, m1, y1 = start_date.day, start_date.month, start_date.year
|
||||
d2, m2, y2 = end_date.day, end_date.month, end_date.year
|
||||
|
||||
if basis == 0:
|
||||
if d1 == 31:
|
||||
d1 = 30
|
||||
if d1 == 30 and d2 == 31:
|
||||
d2 = 30
|
||||
if _last_of_feb(start_date):
|
||||
d1 = 30
|
||||
if _last_of_feb(end_date):
|
||||
d2 = 30
|
||||
return (_date_360(y2, m2, d2) - _date_360(y1, m1, d1)) / 360.0
|
||||
|
||||
elif basis == 1:
|
||||
# This implements Excel's convoluted logic.
|
||||
if (y1 + 1, m1, d1) >= (y2, m2, d2):
|
||||
# Less than or equal to one year.
|
||||
if y1 == y2 and calendar.isleap(y1):
|
||||
year_length = 366.0
|
||||
elif (y1, m1, d1) < (y2, 2, 29) <= (y2, m2, d2) and calendar.isleap(y2):
|
||||
year_length = 366.0
|
||||
elif (y1, m1, d1) <= (y1, 2, 29) < (y2, m2, d2) and calendar.isleap(y1):
|
||||
year_length = 366.0
|
||||
else:
|
||||
year_length = 365.0
|
||||
else:
|
||||
year_length = (datetime.date(y2 + 1, 1, 1) - datetime.date(y1, 1, 1)).days / (y2 + 1.0 - y1)
|
||||
return (end_date - start_date).days / year_length
|
||||
|
||||
elif basis == -1:
|
||||
# This is Google Sheets implementation. Call it an overkill, but I think it's more sensible.
|
||||
#
|
||||
# Excel's logic has the unfortunate property that YEARFRAC(a, b) + YEARFRAC(b, c) is not
|
||||
# always equal to YEARFRAC(a, c). Google Sheets implements a variation that does have this
|
||||
# property, counting the days in each year as a fraction of that year's length (as if each day
|
||||
# is counted as 1/365 or 1/366 depending on the year).
|
||||
#
|
||||
# The one redeeming quality of Excel's logic is that YEARFRAC for two days that differ by
|
||||
# exactly one year is 1.0 (not always true for GS). But in GS version, YEARFRAC between any
|
||||
# two Jan 1 is always a whole number (not always true in Excel).
|
||||
if y1 == y2:
|
||||
return _one_year_frac(start_date, end_date)
|
||||
return (
|
||||
+ _one_year_frac(start_date, datetime.date(y1 + 1, 1, 1))
|
||||
+ (y2 - y1 - 1)
|
||||
+ _one_year_frac(datetime.date(y2, 1, 1), end_date)
|
||||
)
|
||||
|
||||
elif basis == 2:
|
||||
return (end_date - start_date).days / 360.0
|
||||
|
||||
elif basis == 3:
|
||||
return (end_date - start_date).days / 365.0
|
||||
|
||||
elif basis == 4:
|
||||
if d1 == 31:
|
||||
d1 = 30
|
||||
if d2 == 31:
|
||||
d2 = 30
|
||||
return (_date_360(y2, m2, d2) - _date_360(y1, m1, d1)) / 360.0
|
||||
|
||||
raise ValueError('Invalid basis argument %r' % (basis,))
|
||||
|
||||
def _one_year_frac(start_date, end_date):
|
||||
year_length = 366.0 if calendar.isleap(start_date.year) else 365.0
|
||||
return (end_date - start_date).days / year_length
|
||||
520
sandbox/grist/functions/info.py
Normal file
520
sandbox/grist/functions/info.py
Normal file
@@ -0,0 +1,520 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
# pylint: disable=unused-argument
|
||||
|
||||
from __future__ import absolute_import
|
||||
import datetime
|
||||
import math
|
||||
import numbers
|
||||
import re
|
||||
|
||||
from functions import date # pylint: disable=import-error
|
||||
from usertypes import AltText # pylint: disable=import-error
|
||||
from records import Record # pylint: disable=import-error
|
||||
|
||||
def ISBLANK(value):
|
||||
"""
|
||||
Returns whether a value refers to an empty cell. It isn't implemented in Grist. To check for an
|
||||
empty string, use `value == ""`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def ISERR(value):
|
||||
"""
|
||||
Checks whether a value is an error. In other words, it returns true
|
||||
if using `value` directly would raise an exception.
|
||||
|
||||
NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation.
|
||||
|
||||
A more Pythonic approach to checking for errors is:
|
||||
```
|
||||
try:
|
||||
... value ...
|
||||
except Exception, err:
|
||||
... do something about the error ...
|
||||
```
|
||||
|
||||
For example:
|
||||
|
||||
>>> ISERR("Hello")
|
||||
False
|
||||
|
||||
More tests:
|
||||
>>> ISERR(lambda: (1/0.1))
|
||||
False
|
||||
>>> ISERR(lambda: (1/0.0))
|
||||
True
|
||||
>>> ISERR(lambda: "test".bar())
|
||||
True
|
||||
>>> ISERR(lambda: "test".upper())
|
||||
False
|
||||
>>> ISERR(lambda: AltText("A"))
|
||||
False
|
||||
>>> ISERR(lambda: float('nan'))
|
||||
False
|
||||
>>> ISERR(lambda: None)
|
||||
False
|
||||
"""
|
||||
return lazy_value_or_error(value) is _error_sentinel
|
||||
|
||||
|
||||
def ISERROR(value):
|
||||
"""
|
||||
Checks whether a value is an error or an invalid value. It is similar to `ISERR`, but also
|
||||
returns true for an invalid value such as NaN or a text value in a Numeric column.
|
||||
|
||||
NOTE: Grist implements this by automatically wrapping the argument to use lazy evaluation.
|
||||
|
||||
>>> ISERROR("Hello")
|
||||
False
|
||||
>>> ISERROR(AltText("fail"))
|
||||
True
|
||||
>>> ISERROR(float('nan'))
|
||||
True
|
||||
|
||||
More tests:
|
||||
>>> ISERROR(AltText(""))
|
||||
True
|
||||
>>> [ISERROR(v) for v in [0, None, "", "Test", 17.0]]
|
||||
[False, False, False, False, False]
|
||||
>>> ISERROR(lambda: (1/0.1))
|
||||
False
|
||||
>>> ISERROR(lambda: (1/0.0))
|
||||
True
|
||||
>>> ISERROR(lambda: "test".bar())
|
||||
True
|
||||
>>> ISERROR(lambda: "test".upper())
|
||||
False
|
||||
>>> ISERROR(lambda: AltText("A"))
|
||||
True
|
||||
>>> ISERROR(lambda: float('nan'))
|
||||
True
|
||||
>>> ISERROR(lambda: None)
|
||||
False
|
||||
"""
|
||||
return is_error(lazy_value_or_error(value))
|
||||
|
||||
|
||||
def ISLOGICAL(value):
|
||||
"""
|
||||
Checks whether a value is `True` or `False`.
|
||||
|
||||
>>> ISLOGICAL(True)
|
||||
True
|
||||
>>> ISLOGICAL(False)
|
||||
True
|
||||
>>> ISLOGICAL(0)
|
||||
False
|
||||
>>> ISLOGICAL(None)
|
||||
False
|
||||
>>> ISLOGICAL("Test")
|
||||
False
|
||||
"""
|
||||
return isinstance(value, bool)
|
||||
|
||||
|
||||
def ISNA(value):
|
||||
"""
|
||||
Checks whether a value is the error `#N/A`.
|
||||
|
||||
>>> ISNA(float('nan'))
|
||||
True
|
||||
>>> ISNA(0.0)
|
||||
False
|
||||
>>> ISNA('text')
|
||||
False
|
||||
>>> ISNA(float('-inf'))
|
||||
False
|
||||
"""
|
||||
return isinstance(value, float) and math.isnan(value)
|
||||
|
||||
|
||||
def ISNONTEXT(value):
|
||||
"""
|
||||
Checks whether a value is non-textual.
|
||||
|
||||
>>> ISNONTEXT("asdf")
|
||||
False
|
||||
>>> ISNONTEXT("")
|
||||
False
|
||||
>>> ISNONTEXT(AltText("text"))
|
||||
False
|
||||
>>> ISNONTEXT(17.0)
|
||||
True
|
||||
>>> ISNONTEXT(None)
|
||||
True
|
||||
>>> ISNONTEXT(datetime.date(2011, 1, 1))
|
||||
True
|
||||
"""
|
||||
return not ISTEXT(value)
|
||||
|
||||
|
||||
def ISNUMBER(value):
|
||||
"""
|
||||
Checks whether a value is a number.
|
||||
|
||||
>>> ISNUMBER(17)
|
||||
True
|
||||
>>> ISNUMBER(-123.123423)
|
||||
True
|
||||
>>> ISNUMBER(False)
|
||||
True
|
||||
>>> ISNUMBER(float('nan'))
|
||||
True
|
||||
>>> ISNUMBER(float('inf'))
|
||||
True
|
||||
>>> ISNUMBER('17')
|
||||
False
|
||||
>>> ISNUMBER(None)
|
||||
False
|
||||
>>> ISNUMBER(datetime.date(2011, 1, 1))
|
||||
False
|
||||
|
||||
More tests:
|
||||
>>> ISNUMBER(AltText("text"))
|
||||
False
|
||||
>>> ISNUMBER('')
|
||||
False
|
||||
"""
|
||||
return isinstance(value, numbers.Number)
|
||||
|
||||
|
||||
def ISREF(value):
|
||||
"""
|
||||
Checks whether a value is a table record.
|
||||
|
||||
For example, if a column person is of type Reference to the People table, then ISREF($person)
|
||||
is True.
|
||||
Similarly, ISREF(People.lookupOne(name=$name)) is True. For any other type of value,
|
||||
ISREF() would evaluate to False.
|
||||
|
||||
>>> ISREF(17)
|
||||
False
|
||||
>>> ISREF("Roger")
|
||||
False
|
||||
|
||||
"""
|
||||
return isinstance(value, Record)
|
||||
|
||||
|
||||
def ISTEXT(value):
|
||||
"""
|
||||
Checks whether a value is text.
|
||||
|
||||
>>> ISTEXT("asdf")
|
||||
True
|
||||
>>> ISTEXT("")
|
||||
True
|
||||
>>> ISTEXT(AltText("text"))
|
||||
True
|
||||
>>> ISTEXT(17.0)
|
||||
False
|
||||
>>> ISTEXT(None)
|
||||
False
|
||||
>>> ISTEXT(datetime.date(2011, 1, 1))
|
||||
False
|
||||
"""
|
||||
return isinstance(value, (basestring, AltText))
|
||||
|
||||
|
||||
# Regexp for matching email. See ISEMAIL for justification.
|
||||
_email_regexp = re.compile(
|
||||
r"""
|
||||
^\w # Start with an alphanumeric character
|
||||
[\w%+/='-]* (\.[\w%+/='-]+)* # Elsewhere allow also a few other special characters
|
||||
# But no two consecutive periods
|
||||
@
|
||||
([A-Za-z0-9] # Each part of hostname must start with alphanumeric
|
||||
([A-Za-z0-9-]*[A-Za-z0-9])?\. # May have dashes inside, but end in alphanumeric
|
||||
)+
|
||||
[A-Za-z]{2,6}$ # Restrict top-level domain to length {2,6}. Google seems
|
||||
# to use a whitelist for TLDs longer than 2 characters.
|
||||
""", re.UNICODE | re.VERBOSE)
|
||||
|
||||
|
||||
# Regexp for matching hostname part of URLs (see also ISURL). Duplicates part of _email_regexp.
|
||||
_hostname_regexp = re.compile(
|
||||
r"""^
|
||||
([A-Za-z0-9] # Each part of hostname must start with alphanumeric
|
||||
([A-Za-z0-9-]*[A-Za-z0-9])?\. # May have dashes inside, but end in alphanumeric
|
||||
)+
|
||||
[A-Za-z]{2,6}$ # Restrict top-level domain to length {2,6}. Google seems
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
def ISEMAIL(value):
|
||||
u"""
|
||||
Returns whether a value is a valid email address.
|
||||
|
||||
Note that checking email validity is not an exact science. The technical standard considers many
|
||||
email addresses valid that are not used in practice, and would not be considered valid by most
|
||||
users. Instead, we follow Google Sheets implementation, with some differences, noted below.
|
||||
|
||||
>>> ISEMAIL("Abc.123@example.com")
|
||||
True
|
||||
>>> ISEMAIL("Bob_O-Reilly+tag@example.com")
|
||||
True
|
||||
>>> ISEMAIL("John Doe")
|
||||
False
|
||||
>>> ISEMAIL("john@aol...com")
|
||||
False
|
||||
|
||||
More tests:
|
||||
>>> ISEMAIL("Abc@example.com") # True, True
|
||||
True
|
||||
>>> ISEMAIL("Abc.123@example.com") # True, True
|
||||
True
|
||||
>>> ISEMAIL("foo@bar.com") # True, True
|
||||
True
|
||||
>>> ISEMAIL("asdf@com.zt") # True, True
|
||||
True
|
||||
>>> ISEMAIL("Bob_O-Reilly+tag@example.com") # True, True
|
||||
True
|
||||
>>> ISEMAIL("john@server.department.company.com") # True, True
|
||||
True
|
||||
>>> ISEMAIL("asdf@mail.ru") # True, True
|
||||
True
|
||||
>>> ISEMAIL("fabio@foo.qwer.COM") # True, True
|
||||
True
|
||||
>>> ISEMAIL("user+mailbox/department=shipping@example.com") # False, True
|
||||
True
|
||||
>>> ISEMAIL(u"user+mailbox/department=shipping@example.com") # False, True
|
||||
True
|
||||
>>> ISEMAIL("customer/department=shipping@example.com") # False, True
|
||||
True
|
||||
>>> ISEMAIL("Bob_O'Reilly+tag@example.com") # False, True
|
||||
True
|
||||
>>> ISEMAIL(u"фыва@mail.ru") # False, True
|
||||
True
|
||||
>>> ISEMAIL("my@baddash.-.com") # True, False
|
||||
False
|
||||
>>> ISEMAIL("my@baddash.-a.com") # True, False
|
||||
False
|
||||
>>> ISEMAIL("my@baddash.b-.com") # True, False
|
||||
False
|
||||
>>> ISEMAIL("john@-.com") # True, False
|
||||
False
|
||||
>>> ISEMAIL("fabio@disapproved.solutions") # False, False
|
||||
False
|
||||
>>> ISEMAIL("!def!xyz%abc@example.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("!#$%&'*+-/=?^_`.{|}~@example.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL(u"伊昭傑@郵件.商務") # False, False
|
||||
False
|
||||
>>> ISEMAIL(u"राम@मोहन.ईन्फो") # False, Fale
|
||||
False
|
||||
>>> ISEMAIL(u"юзер@екзампл.ком") # False, False
|
||||
False
|
||||
>>> ISEMAIL(u"θσερ@εχαμπλε.ψομ") # False, False
|
||||
False
|
||||
>>> ISEMAIL(u"葉士豪@臺網中心.tw") # False, False
|
||||
False
|
||||
>>> ISEMAIL(u"jeff@臺網中心.tw") # False, False
|
||||
False
|
||||
>>> ISEMAIL(u"葉士豪@臺網中心.台灣") # False, False
|
||||
False
|
||||
>>> ISEMAIL(u"jeff葉@臺網中心.tw") # False, False
|
||||
False
|
||||
>>> ISEMAIL("my.name@domain.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("my.name@domain.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("my@.leadingdot.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("my@..leadingfwdot.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("my@..twodots.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("my@twodots..com") # False, False
|
||||
False
|
||||
>>> ISEMAIL(".leadingdot@domain.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("..twodots@domain.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("twodots..here@domain.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("me@⒈wouldbeinvalid.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("Foo Bar <a+2asdf@qwer.bar.com>") # False, False
|
||||
False
|
||||
>>> ISEMAIL("Abc\\@def@example.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("foo@bar@google.com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("john@aol...com") # False, False
|
||||
False
|
||||
>>> ISEMAIL("x@ทีเอชนิค.ไทย") # False, False
|
||||
False
|
||||
>>> ISEMAIL("asdf@mail") # False, False
|
||||
False
|
||||
>>> ISEMAIL("example@良好Mail.中国") # False, False
|
||||
False
|
||||
"""
|
||||
return bool(_email_regexp.match(value))
|
||||
|
||||
|
||||
_url_regexp = re.compile(
|
||||
r"""^
|
||||
((ftp|http|https|gopher|mailto|news|telnet|aim)://)?
|
||||
(\w+@)? # Allow 'user@' part, esp. useful for mailto: URLs.
|
||||
([A-Za-z0-9] # Each part of hostname must start with alphanumeric
|
||||
([A-Za-z0-9-]*[A-Za-z0-9])?\. # May have dashes inside, but end in alphanumeric
|
||||
)+
|
||||
[A-Za-z]{2,6} # Restrict top-level domain to length {2,6}. Google seems
|
||||
# to use a whitelist for TLDs longer than 2 characters.
|
||||
([/?][-\w!#$%&'()*+,./:;=?@~]*)?$ # Notably, this excludes <, >, and ".
|
||||
""", re.VERBOSE)
|
||||
|
||||
|
||||
def ISURL(value):
|
||||
"""
|
||||
Checks whether a value is a valid URL. It does not need to be fully qualified, or to include
|
||||
"http://" and "www". It does not follow a standard, but attempts to work similarly to ISURL in
|
||||
Google Sheets, and to return True for text that is likely a URL.
|
||||
|
||||
Valid protocols include ftp, http, https, gopher, mailto, news, telnet, and aim.
|
||||
|
||||
>>> ISURL("http://www.getgrist.com")
|
||||
True
|
||||
>>> ISURL("https://foo.com/test_(wikipedia)#cite-1")
|
||||
True
|
||||
>>> ISURL("mailto://user@example.com")
|
||||
True
|
||||
>>> ISURL("http:///a")
|
||||
False
|
||||
|
||||
More tests:
|
||||
>>> ISURL("http://www.google.com")
|
||||
True
|
||||
>>> ISURL("www.google.com/")
|
||||
True
|
||||
>>> ISURL("google.com")
|
||||
True
|
||||
>>> ISURL("http://a.b-c.de")
|
||||
True
|
||||
>>> ISURL("a.b-c.de")
|
||||
True
|
||||
>>> ISURL("http://j.mp/---")
|
||||
True
|
||||
>>> ISURL("ftp://foo.bar/baz")
|
||||
True
|
||||
>>> ISURL("https://foo.com/blah_(wikipedia)#cite-1")
|
||||
True
|
||||
>>> ISURL("mailto://user@google.com")
|
||||
True
|
||||
>>> ISURL("http://user@www.google.com")
|
||||
True
|
||||
>>> ISURL("http://foo.com/!#$%25&'()*+,-./=?@_~")
|
||||
True
|
||||
>>> ISURL("http://../")
|
||||
False
|
||||
>>> ISURL("http://??/")
|
||||
False
|
||||
>>> ISURL("a.-b.cd")
|
||||
False
|
||||
>>> ISURL("http://foo.bar?q=Spaces should be encoded ")
|
||||
False
|
||||
>>> ISURL("//")
|
||||
False
|
||||
>>> ISURL("///a")
|
||||
False
|
||||
>>> ISURL("http:///a")
|
||||
False
|
||||
>>> ISURL("bar://www.google.com")
|
||||
False
|
||||
>>> ISURL("http:// shouldfail.com")
|
||||
False
|
||||
>>> ISURL("ftps://foo.bar/")
|
||||
False
|
||||
>>> ISURL("http://-error-.invalid/")
|
||||
False
|
||||
>>> ISURL("http://0.0.0.0")
|
||||
False
|
||||
>>> ISURL("http://.www.foo.bar/")
|
||||
False
|
||||
>>> ISURL("http://.www.foo.bar./")
|
||||
False
|
||||
>>> ISURL("example.com/file[/].html")
|
||||
False
|
||||
>>> ISURL("http://example.com/file[/].html")
|
||||
False
|
||||
>>> ISURL("http://mw1.google.com/kml-samples/gp/seattle/gigapxl/$[level]/r$[y]_c$[x].jpg")
|
||||
False
|
||||
>>> ISURL("http://foo.com/>")
|
||||
False
|
||||
"""
|
||||
value = value.strip()
|
||||
if ' ' in value: # Disallow spaces inside value.
|
||||
return False
|
||||
return bool(_url_regexp.match(value))
|
||||
|
||||
|
||||
def N(value):
|
||||
"""
|
||||
Returns the value converted to a number. True/False are converted to 1/0. A date is converted to
|
||||
Excel-style serial number of the date. Anything else is converted to 0.
|
||||
|
||||
>>> N(7)
|
||||
7
|
||||
>>> N(7.1)
|
||||
7.1
|
||||
>>> N("Even")
|
||||
0
|
||||
>>> N("7")
|
||||
0
|
||||
>>> N(True)
|
||||
1
|
||||
>>> N(datetime.datetime(2011, 4, 17))
|
||||
40650.0
|
||||
"""
|
||||
if ISNUMBER(value):
|
||||
return value
|
||||
if isinstance(value, datetime.date):
|
||||
return date.DATE_TO_XL(value)
|
||||
return 0
|
||||
|
||||
|
||||
def NA():
|
||||
"""
|
||||
Returns the "value not available" error, `#N/A`.
|
||||
|
||||
>>> math.isnan(NA())
|
||||
True
|
||||
"""
|
||||
return float('nan')
|
||||
|
||||
|
||||
def TYPE(value):
|
||||
"""
|
||||
Returns a number associated with the type of data passed into the function. This is not
|
||||
implemented in Grist. Use `isinstance(value, type)` or `type(value)`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def CELL(info_type, reference):
|
||||
"""
|
||||
Returns the requested information about the specified cell. This is not implemented in Grist
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
# Unique sentinel value to represent that a lazy value evaluates with an exception.
|
||||
_error_sentinel = object()
|
||||
|
||||
def lazy_value_or_error(value):
|
||||
"""
|
||||
Evaluates a value like lazy_value(), but returns _error_sentinel on exception.
|
||||
"""
|
||||
try:
|
||||
return value() if callable(value) else value
|
||||
except Exception:
|
||||
return _error_sentinel
|
||||
|
||||
def is_error(value):
|
||||
"""
|
||||
Checks whether a value is an invalid value or _error_sentinel.
|
||||
"""
|
||||
return ((value is _error_sentinel)
|
||||
or isinstance(value, AltText)
|
||||
or (isinstance(value, float) and math.isnan(value)))
|
||||
165
sandbox/grist/functions/logical.py
Normal file
165
sandbox/grist/functions/logical.py
Normal file
@@ -0,0 +1,165 @@
|
||||
from info import lazy_value_or_error, is_error
|
||||
from usertypes import AltText # pylint: disable=unused-import,import-error
|
||||
|
||||
|
||||
def AND(logical_expression, *logical_expressions):
|
||||
"""
|
||||
Returns True if all of the arguments are logically true, and False if any are false.
|
||||
Same as `all([value1, value2, ...])`.
|
||||
|
||||
>>> AND(1)
|
||||
True
|
||||
>>> AND(0)
|
||||
False
|
||||
>>> AND(1, 1)
|
||||
True
|
||||
>>> AND(1,2,3,4)
|
||||
True
|
||||
>>> AND(1,2,3,4,0)
|
||||
False
|
||||
"""
|
||||
return all((logical_expression,) + logical_expressions)
|
||||
|
||||
|
||||
def FALSE():
|
||||
"""
|
||||
Returns the logical value `False`. You may also use the value `False` directly. This
|
||||
function is provided primarily for compatibility with other spreadsheet programs.
|
||||
|
||||
>>> FALSE()
|
||||
False
|
||||
"""
|
||||
return False
|
||||
|
||||
|
||||
def IF(logical_expression, value_if_true, value_if_false):
|
||||
"""
|
||||
Returns one value if a logical expression is `True` and another if it is `False`.
|
||||
|
||||
The equivalent Python expression is:
|
||||
```
|
||||
value_if_true if logical_expression else value_if_false
|
||||
```
|
||||
|
||||
Since Grist supports multi-line formulas, you may also use Python blocks such as:
|
||||
```
|
||||
if logical_expression:
|
||||
return value_if_true
|
||||
else:
|
||||
return value_if_false
|
||||
```
|
||||
|
||||
NOTE: Grist follows Excel model by only evaluating one of the value expressions, by
|
||||
automatically wrapping the expressions to use lazy evaluation. This allows `IF(False, 1/0, 1)`
|
||||
to evaluate to `1` rather than raise an exception.
|
||||
|
||||
>>> IF(12, "Yes", "No")
|
||||
'Yes'
|
||||
>>> IF(None, "Yes", "No")
|
||||
'No'
|
||||
>>> IF(True, 0.85, 0.0)
|
||||
0.85
|
||||
>>> IF(False, 0.85, 0.0)
|
||||
0.0
|
||||
|
||||
More tests:
|
||||
>>> IF(True, lambda: (1/0), lambda: (17))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ZeroDivisionError: integer division or modulo by zero
|
||||
>>> IF(False, lambda: (1/0), lambda: (17))
|
||||
17
|
||||
"""
|
||||
return lazy_value(value_if_true) if logical_expression else lazy_value(value_if_false)
|
||||
|
||||
|
||||
def IFERROR(value, value_if_error=""):
|
||||
"""
|
||||
Returns the first argument if it is not an error value, otherwise returns the second argument if
|
||||
present, or a blank if the second argument is absent.
|
||||
|
||||
NOTE: Grist handles values that raise an exception by wrapping them to use lazy evaluation.
|
||||
|
||||
>>> IFERROR(float('nan'), "**NAN**")
|
||||
'**NAN**'
|
||||
>>> IFERROR(17.17, "**NAN**")
|
||||
17.17
|
||||
>>> IFERROR("Text")
|
||||
'Text'
|
||||
>>> IFERROR(AltText("hello"))
|
||||
''
|
||||
|
||||
More tests:
|
||||
>>> IFERROR(lambda: (1/0.1), "X")
|
||||
10.0
|
||||
>>> IFERROR(lambda: (1/0.0), "X")
|
||||
'X'
|
||||
>>> IFERROR(lambda: AltText("A"), "err")
|
||||
'err'
|
||||
>>> IFERROR(lambda: None, "err")
|
||||
|
||||
>>> IFERROR(lambda: foo.bar, 123)
|
||||
123
|
||||
>>> IFERROR(lambda: "test".bar(), 123)
|
||||
123
|
||||
>>> IFERROR(lambda: "test".bar())
|
||||
''
|
||||
>>> IFERROR(lambda: "test".upper(), 123)
|
||||
'TEST'
|
||||
"""
|
||||
value = lazy_value_or_error(value)
|
||||
return value if not is_error(value) else value_if_error
|
||||
|
||||
|
||||
def NOT(logical_expression):
|
||||
"""
|
||||
Returns the opposite of a logical value: `NOT(True)` returns `False`; `NOT(False)` returns
|
||||
`True`. Same as `not logical_expression`.
|
||||
|
||||
>>> NOT(123)
|
||||
False
|
||||
>>> NOT(0)
|
||||
True
|
||||
"""
|
||||
return not logical_expression
|
||||
|
||||
|
||||
def OR(logical_expression, *logical_expressions):
|
||||
"""
|
||||
Returns True if any of the arguments is logically true, and false if all of the
|
||||
arguments are false.
|
||||
Same as `any([value1, value2, ...])`.
|
||||
|
||||
>>> OR(1)
|
||||
True
|
||||
>>> OR(0)
|
||||
False
|
||||
>>> OR(1, 1)
|
||||
True
|
||||
>>> OR(0, 1)
|
||||
True
|
||||
>>> OR(0, 0)
|
||||
False
|
||||
>>> OR(0,False,0.0,"",None)
|
||||
False
|
||||
>>> OR(0,None,3,0)
|
||||
True
|
||||
"""
|
||||
return any((logical_expression,) + logical_expressions)
|
||||
|
||||
|
||||
def TRUE():
|
||||
"""
|
||||
Returns the logical value `True`. You may also use the value `True` directly. This
|
||||
function is provided primarily for compatibility with other spreadsheet programs.
|
||||
|
||||
>>> TRUE()
|
||||
True
|
||||
"""
|
||||
return True
|
||||
|
||||
def lazy_value(value):
|
||||
"""
|
||||
Evaluates a lazy value by calling it when it's a callable, or returns it unchanged otherwise.
|
||||
"""
|
||||
return value() if callable(value) else value
|
||||
80
sandbox/grist/functions/lookup.py
Normal file
80
sandbox/grist/functions/lookup.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# pylint: disable=redefined-builtin, line-too-long
|
||||
|
||||
def ADDRESS(row, column, absolute_relative_mode, use_a1_notation, sheet):
|
||||
"""Returns a cell reference as a string."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def CHOOSE(index, choice1, choice2):
|
||||
"""Returns an element from a list of choices based on index."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def COLUMN(cell_reference=None):
|
||||
"""Returns the column number of a specified cell, with `A=1`."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def COLUMNS(range):
|
||||
"""Returns the number of columns in a specified array or range."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def GETPIVOTDATA(value_name, any_pivot_table_cell, original_column_1, pivot_item_1=None, *args):
|
||||
"""Extracts an aggregated value from a pivot table that corresponds to the specified row and column headings."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def HLOOKUP(search_key, range, index, is_sorted):
|
||||
"""Horizontal lookup. Searches across the first row of a range for a key and returns the value of a specified cell in the column found."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def HYPERLINK(url, link_label):
|
||||
"""Creates a hyperlink inside a cell."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def INDEX(reference, row, column):
|
||||
"""Returns the content of a cell, specified by row and column offset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def INDIRECT(cell_reference_as_string):
|
||||
"""Returns a cell reference specified by a string."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def LOOKUP(search_key, search_range_or_search_result_array, result_range=None):
|
||||
"""Looks through a row or column for a key and returns the value of the cell in a result range located in the same position as the search row or column."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def MATCH(search_key, range, search_type):
|
||||
"""Returns the relative position of an item in a range that matches a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def OFFSET(cell_reference, offset_rows, offset_columns, height, width):
|
||||
"""Returns a range reference shifted a specified number of rows and columns from a starting cell reference."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def ROW(cell_reference):
|
||||
"""Returns the row number of a specified cell."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def ROWS(range):
|
||||
"""Returns the number of rows in a specified array or range."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def VLOOKUP(table, **field_value_pairs):
|
||||
"""
|
||||
Vertical lookup. Searches the given table for a record matching the given `field=value`
|
||||
arguments. If multiple records match, returns one of them. If none match, returns the special
|
||||
empty record.
|
||||
|
||||
The returned object is a record whose fields are available using `.field` syntax. For example,
|
||||
`VLOOKUP(Employees, EmployeeID=$EmpID).Salary`.
|
||||
|
||||
Note that `VLOOKUP` isn't commonly needed in Grist, since [Reference columns](col-refs) are the
|
||||
best way to link data between tables, and allow simple efficient usage such as `$Person.Age`.
|
||||
|
||||
`VLOOKUP` is exactly quivalent to `table.lookupOne(**field_value_pairs)`. See
|
||||
[lookupOne](#lookupone).
|
||||
|
||||
For example:
|
||||
```
|
||||
VLOOKUP(People, First_Name="Lewis", Last_Name="Carroll")
|
||||
VLOOKUP(People, First_Name="Lewis", Last_Name="Carroll").Age
|
||||
```
|
||||
"""
|
||||
return table.lookupOne(**field_value_pairs)
|
||||
830
sandbox/grist/functions/math.py
Normal file
830
sandbox/grist/functions/math.py
Normal file
@@ -0,0 +1,830 @@
|
||||
# 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)
|
||||
329
sandbox/grist/functions/schedule.py
Normal file
329
sandbox/grist/functions/schedule.py
Normal file
@@ -0,0 +1,329 @@
|
||||
from datetime import datetime, timedelta
|
||||
import re
|
||||
from date import DATEADD, NOW, DTIME
|
||||
from moment_parse import MONTH_NAMES, DAY_NAMES
|
||||
|
||||
# Limit exports to schedule, so that upper-case constants like MONTH_NAMES, DAY_NAMES don't end up
|
||||
# exposed as if Excel-style functions (or break docs generation).
|
||||
__all__ = ['SCHEDULE']
|
||||
|
||||
def SCHEDULE(schedule, start=None, count=10, end=None):
|
||||
"""
|
||||
Returns the list of `datetime` objects generated according to the `schedule` string. Starts at
|
||||
`start`, which defaults to NOW(). Generates at most `count` results (10 by default). If `end` is
|
||||
given, stops there.
|
||||
|
||||
The schedule has the format "INTERVAL: SLOTS, ...". For example:
|
||||
|
||||
annual: Jan-15, Apr-15, Jul-15 -- Three times a year on given dates at midnight.
|
||||
annual: 1/15, 4/15, 7/15 -- Same as above.
|
||||
monthly: /1 2pm, /15 2pm -- The 1st and the 15th of each month, at 2pm.
|
||||
3-months: /10, +1m /20 -- Every 3 months on the 10th of month 1, 20th of month 2.
|
||||
weekly: Mo 9am, Tu 9am, Fr 2pm -- Three times a week at specified times.
|
||||
2-weeks: Mo, +1w Tu -- Every 2 weeks on Monday of week 1, Tuesday of week 2.
|
||||
daily: 07:30, 21:00 -- Twice a day at specified times.
|
||||
2-day: 12am, 4pm, +1d 8am -- Three times every two days, evenly spaced.
|
||||
hourly: :15, :45 -- 15 minutes before and after each hour.
|
||||
4-hour: :00, 1:20, 2:40 -- Three times every 4 hours, evenly spaced.
|
||||
10-minute: +0s -- Every 10 minutes on the minute.
|
||||
|
||||
INTERVAL must be either of the form `N-unit` where `N` is a number and `unit` is one of `year`,
|
||||
`month`, `week`, `day`, `hour`; or one of the aliases: `annual`, `monthly`, `weekly`, `daily`,
|
||||
`hourly`, which mean `1-year`, `1-month`, etc.
|
||||
|
||||
SLOTS support the following units:
|
||||
|
||||
`Jan-15` or `1/15` -- Month and day of the month; available when INTERVAL is year-based.
|
||||
`/15` -- Day of the month, available when INTERVAL is month-based.
|
||||
`Mon`, `Mo`, `Friday` -- Day of the week (or abbreviation), when INTERVAL is week-based.
|
||||
10am, 1:30pm, 15:45 -- Time of day, available for day-based or longer intervals.
|
||||
:45, :00 -- Minutes of the hour, available when INTERVAL is hour-based.
|
||||
+1d, +15d -- How many days to add to start of INTERVAL.
|
||||
+1w -- How many weeks to add to start of INTERVAL.
|
||||
+1m -- How many months to add to start of INTERVAL.
|
||||
|
||||
The SLOTS are always relative to the INTERVAL rather than to `start`. Week-based intervals start
|
||||
on Sunday. E.g. `weekly: +1d, +4d` is the same as `weekly: Mon, Thu`, and generates times on
|
||||
Mondays and Thursdays regardless of `start`.
|
||||
|
||||
The first generated time is determined by the *unit* of the INTERVAL without regard to the
|
||||
multiple. E.g. both "2-week: Mon" and "3-week: Mon" start on the first Monday after `start`, and
|
||||
then generate either every second or every third Monday after that. Similarly, `24-hour: :00`
|
||||
starts with the first top-of-the-hour after `start` (not with midnight), and then repeats every
|
||||
24 hours. To start with the midnight after `start`, use `daily: 0:00`.
|
||||
|
||||
For interval units of a day or longer, if time-of-day is not specified, it defaults to midnight.
|
||||
|
||||
The time zone of `start` determines the time zone of the generated times.
|
||||
|
||||
>>> def show(dates): return [d.strftime("%Y-%m-%d %H:%M") for d in dates]
|
||||
>>> start = datetime(2018, 9, 4, 14, 0); # 2pm on Tue, Sep 4 2018.
|
||||
|
||||
>>> show(SCHEDULE('annual: Jan-15, Apr-15, Jul-15, Oct-15', start=start, count=4))
|
||||
['2018-10-15 00:00', '2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00']
|
||||
|
||||
>>> show(SCHEDULE('annual: 1/15, 4/15, 7/15', start=start, count=4))
|
||||
['2019-01-15 00:00', '2019-04-15 00:00', '2019-07-15 00:00', '2020-01-15 00:00']
|
||||
|
||||
>>> show(SCHEDULE('monthly: /1 2pm, /15 5pm', start=start, count=4))
|
||||
['2018-09-15 17:00', '2018-10-01 14:00', '2018-10-15 17:00', '2018-11-01 14:00']
|
||||
|
||||
>>> show(SCHEDULE('3-months: /10, +1m /20', start=start, count=4))
|
||||
['2018-09-10 00:00', '2018-10-20 00:00', '2018-12-10 00:00', '2019-01-20 00:00']
|
||||
|
||||
>>> show(SCHEDULE('weekly: Mo 9am, Tu 9am, Fr 2pm', start=start, count=4))
|
||||
['2018-09-07 14:00', '2018-09-10 09:00', '2018-09-11 09:00', '2018-09-14 14:00']
|
||||
|
||||
>>> show(SCHEDULE('2-weeks: Mo, +1w Tu', start=start, count=4))
|
||||
['2018-09-11 00:00', '2018-09-17 00:00', '2018-09-25 00:00', '2018-10-01 00:00']
|
||||
|
||||
>>> show(SCHEDULE('daily: 07:30, 21:00', start=start, count=4))
|
||||
['2018-09-04 21:00', '2018-09-05 07:30', '2018-09-05 21:00', '2018-09-06 07:30']
|
||||
|
||||
>>> show(SCHEDULE('2-day: 12am, 4pm, +1d 8am', start=start, count=4))
|
||||
['2018-09-04 16:00', '2018-09-05 08:00', '2018-09-06 00:00', '2018-09-06 16:00']
|
||||
|
||||
>>> show(SCHEDULE('hourly: :15, :45', start=start, count=4))
|
||||
['2018-09-04 14:15', '2018-09-04 14:45', '2018-09-04 15:15', '2018-09-04 15:45']
|
||||
|
||||
>>> show(SCHEDULE('4-hour: :00, +1H :20, +2H :40', start=start, count=4))
|
||||
['2018-09-04 14:00', '2018-09-04 15:20', '2018-09-04 16:40', '2018-09-04 18:00']
|
||||
"""
|
||||
return Schedule(schedule).series(start or NOW(), end, count=count)
|
||||
|
||||
class Delta(object):
|
||||
"""
|
||||
Similar to timedelta, keeps intervals by unit. Specifically, this is needed for months
|
||||
and years, since those can't be represented exactly with a timedelta.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._timedelta = timedelta(0)
|
||||
self._months = 0
|
||||
|
||||
def add_interval(self, number, unit):
|
||||
if unit == 'months':
|
||||
self._months += number
|
||||
elif unit == 'years':
|
||||
self._months += number * 12
|
||||
else:
|
||||
self._timedelta += timedelta(**{unit: number})
|
||||
return self
|
||||
|
||||
def add_to(self, dtime):
|
||||
return datetime.combine(DATEADD(dtime, months=self._months), dtime.timetz()) + self._timedelta
|
||||
|
||||
|
||||
class Schedule(object):
|
||||
"""
|
||||
Schedule parses a schedule spec into an interval and slots in the constructor. Then the series()
|
||||
method applies it to any start/end dates.
|
||||
"""
|
||||
def __init__(self, spec_string):
|
||||
parts = spec_string.split(":", 1)
|
||||
if len(parts) != 2:
|
||||
raise ValueError("schedule must have the form INTERVAL: SLOTS, ...")
|
||||
|
||||
count, unit = _parse_interval(parts[0].strip())
|
||||
self._interval_unit = unit
|
||||
self._interval = Delta().add_interval(count, unit)
|
||||
self._slots = [_parse_slot(t, self._interval_unit) for t in parts[1].split(",")]
|
||||
|
||||
def series(self, start_dtime, end_dtime, count=10):
|
||||
# Start with a preceding unit boundary, then check the slots within that unit and start with
|
||||
# the first one that's at start_dtime or later.
|
||||
start_dtime = DTIME(start_dtime)
|
||||
end_dtime = end_dtime and DTIME(end_dtime)
|
||||
dtime = _round_down_to_unit(start_dtime, self._interval_unit)
|
||||
while True:
|
||||
for slot in self._slots:
|
||||
if count <= 0:
|
||||
return
|
||||
out = slot.add_to(dtime)
|
||||
if out < start_dtime:
|
||||
continue
|
||||
if end_dtime is not None and out > end_dtime:
|
||||
return
|
||||
yield out
|
||||
count -= 1
|
||||
dtime = self._interval.add_to(dtime)
|
||||
|
||||
def _fail(message):
|
||||
raise ValueError(message)
|
||||
|
||||
def _round_down_to_unit(dtime, unit):
|
||||
"""
|
||||
Rounds datetime down to the given unit. Weeks are rounded to start of Sunday.
|
||||
"""
|
||||
tz = dtime.tzinfo
|
||||
return ( datetime(dtime.year, 1, 1, tzinfo=tz) if unit == 'years'
|
||||
else datetime(dtime.year, dtime.month, 1, tzinfo=tz) if unit == 'months'
|
||||
else (dtime - timedelta(days=dtime.isoweekday() % 7))
|
||||
.replace(hour=0, minute=0, second=0, microsecond=0) if unit == 'weeks'
|
||||
else dtime.replace(hour=0, minute=0, second=0, microsecond=0) if unit == 'days'
|
||||
else dtime.replace(minute=0, second=0, microsecond=0) if unit == 'hours'
|
||||
else dtime.replace(second=0, microsecond=0) if unit == 'minutes'
|
||||
else dtime.replace(microsecond=0) if unit == 'seconds'
|
||||
else _fail("Invalid unit %s" % unit)
|
||||
)
|
||||
|
||||
_UNITS = ('years', 'months', 'weeks', 'days', 'hours', 'minutes', 'seconds')
|
||||
_VALID_UNITS = set(_UNITS)
|
||||
_SINGULAR_UNITS = dict(zip(('year', 'month', 'week', 'day', 'hour', 'minute', 'second'), _UNITS))
|
||||
_SHORT_UNITS = dict(zip(('y', 'm', 'w', 'd', 'H', 'M', 'S'), _UNITS))
|
||||
|
||||
_INTERVAL_ALIASES = {
|
||||
'annual': (1, 'years'),
|
||||
'monthly': (1, 'months'),
|
||||
'weekly': (1, 'weeks'),
|
||||
'daily': (1, 'days'),
|
||||
'hourly': (1, 'hours'),
|
||||
}
|
||||
|
||||
_INTERVAL_RE = re.compile(r'^(?P<num>\d+)[-\s]+(?P<unit>[a-z]+)$', re.I)
|
||||
|
||||
# Maps weekday names, including 2- and 3-letter abbreviations, to numbers 0 through 6.
|
||||
WEEKDAY_OFFSETS = {}
|
||||
for (i, name) in enumerate(DAY_NAMES):
|
||||
WEEKDAY_OFFSETS[name] = i
|
||||
WEEKDAY_OFFSETS[name[:3]] = i
|
||||
WEEKDAY_OFFSETS[name[:2]] = i
|
||||
|
||||
# Maps month names, including 3-letter abbreviations, to numbers 0 through 11.
|
||||
MONTH_OFFSETS = {}
|
||||
for (i, name) in enumerate(MONTH_NAMES):
|
||||
MONTH_OFFSETS[name] = i
|
||||
MONTH_OFFSETS[name[:3]] = i
|
||||
|
||||
|
||||
def _parse_interval(interval_str):
|
||||
"""
|
||||
Given a spec like "daily" or "3-week", returns (N, unit), such as (1, "days") or (3, "weeks").
|
||||
"""
|
||||
interval_str = interval_str.lower()
|
||||
if interval_str in _INTERVAL_ALIASES:
|
||||
return _INTERVAL_ALIASES[interval_str]
|
||||
|
||||
m = _INTERVAL_RE.match(interval_str)
|
||||
if not m:
|
||||
raise ValueError("Not a valid interval '%s'" % interval_str)
|
||||
num = int(m.group("num"))
|
||||
unit = m.group("unit")
|
||||
unit = _SINGULAR_UNITS.get(unit, unit)
|
||||
if unit not in _VALID_UNITS:
|
||||
raise ValueError("Unknown unit '%s' in interval '%s'" % (unit, interval_str))
|
||||
return (num, unit)
|
||||
|
||||
|
||||
def _parse_slot(slot_str, parent_unit):
|
||||
"""
|
||||
Parses a slot in one of several recognized formats. Allowed formats depend on parent_unit, e.g.
|
||||
'Jan-15' is valid when parent_unit is 'years', but not when it is 'hours'. We also disallow
|
||||
using the same unit more than once, which is confusing, e.g. "+1d +2d" or "9:30am +2H".
|
||||
Returns a Delta object.
|
||||
"""
|
||||
parts = slot_str.split()
|
||||
if not parts:
|
||||
raise ValueError("At least one slot must be specified")
|
||||
|
||||
delta = Delta()
|
||||
seen_units = set()
|
||||
allowed_slot_types = _ALLOWED_SLOTS_BY_UNIT.get(parent_unit) or ('delta',)
|
||||
|
||||
# Slot parts go through parts like "Jan-15 16pm", collecting the offsets into a single Delta.
|
||||
for part in parts:
|
||||
m = _SLOT_RE.match(part)
|
||||
if not m:
|
||||
raise ValueError("Invalid slot '%s'" % part)
|
||||
for slot_type in allowed_slot_types:
|
||||
if m.group(slot_type):
|
||||
# If there is a group for one slot type, that's the only group. We find and use the
|
||||
# corresponding parser, then move on to the next slot part.
|
||||
for count, unit in _SLOT_PARSERS[slot_type](m):
|
||||
delta.add_interval(count, unit)
|
||||
if unit in seen_units:
|
||||
raise ValueError("Duplicate unit %s in '%s'" % (unit, slot_str))
|
||||
seen_units.add(unit)
|
||||
break
|
||||
else:
|
||||
# If none of the allowed slot types was found, it must be a disallowed one.
|
||||
raise ValueError("Invalid slot '%s' for unit '%s'" % (part, parent_unit))
|
||||
return delta
|
||||
|
||||
# We parse all slot types using one big regex. The constants below define one part of the regex
|
||||
# for each slot type (e.g. to match "Jan-15" or "5:30am" or "+1d"). Note that all group names
|
||||
# (defined with (?P<NAME>...)) must be distinct.
|
||||
_DATE_RE = r'(?:(?P<month_name>[a-z]+)-|(?P<month_num>\d+)/)(?P<month_day>\d+)'
|
||||
_MDAY_RE = r'/(?P<month_day2>\d+)'
|
||||
_WDAY_RE = r'(?P<weekday>[a-z]+)'
|
||||
_TIME_RE = r'(?P<hours>\d+)(?:\:(?P<minutes>\d{2})(?P<ampm1>am|pm)?|(?P<ampm2>am|pm))'
|
||||
_MINS_RE = r':(?P<minutes2>\d{2})'
|
||||
_DELTA_RE = r'\+(?P<count>\d+)(?P<unit>[a-z]+)'
|
||||
|
||||
# The regex parts are combined and compiled here. Only one group will match, corresponding to one
|
||||
# slot type. Different slot types depend on the unit of the overall interval.
|
||||
_SLOT_RE = re.compile(
|
||||
r'^(?:(?P<date>%s)|(?P<mday>%s)|(?P<wday>%s)|(?P<time>%s)|(?P<mins>%s)|(?P<delta>%s))$' %
|
||||
(_DATE_RE, _MDAY_RE, _WDAY_RE, _TIME_RE, _MINS_RE, _DELTA_RE), re.IGNORECASE)
|
||||
|
||||
# Slot types that make sense for each unit of overall interval. If not listed (e.g. "minutes")
|
||||
# then only "delta" slot type is allowed.
|
||||
_ALLOWED_SLOTS_BY_UNIT = {
|
||||
'years': ('date', 'time', 'delta'),
|
||||
'months': ('mday', 'time', 'delta'),
|
||||
'weeks': ('wday', 'time', 'delta'),
|
||||
'days': ('time', 'delta'),
|
||||
'hours': ('mins', 'delta'),
|
||||
}
|
||||
|
||||
# The helper methods below parse one slot type each, given a regex match that matched that slot
|
||||
# type. These are combined and used via the _SLOT_PARSERS dict below.
|
||||
def _parse_slot_date(m):
|
||||
mday = int(m.group("month_day"))
|
||||
month_name = m.group("month_name")
|
||||
month_num = m.group("month_num")
|
||||
if month_name:
|
||||
name = month_name.lower()
|
||||
if name not in MONTH_OFFSETS:
|
||||
raise ValueError("Unknown month '%s'" % month_name)
|
||||
mnum = MONTH_OFFSETS[name]
|
||||
else:
|
||||
mnum = int(month_num) - 1
|
||||
return [(mnum, 'months'), (mday - 1, 'days')]
|
||||
|
||||
def _parse_slot_mday(m):
|
||||
mday = int(m.group("month_day2"))
|
||||
return [(mday - 1, 'days')]
|
||||
|
||||
def _parse_slot_wday(m):
|
||||
wday = m.group("weekday").lower()
|
||||
if wday not in WEEKDAY_OFFSETS:
|
||||
raise ValueError("Unknown day of the week '%s'" % wday)
|
||||
return [(WEEKDAY_OFFSETS[wday], "days")]
|
||||
|
||||
def _parse_slot_time(m):
|
||||
hours = int(m.group("hours"))
|
||||
minutes = int(m.group("minutes") or 0)
|
||||
ampm = m.group("ampm1") or m.group("ampm2")
|
||||
if ampm:
|
||||
hours = (hours % 12) + (12 if ampm.lower() == "pm" else 0)
|
||||
return [(hours, 'hours'), (minutes, 'minutes')]
|
||||
|
||||
def _parse_slot_mins(m):
|
||||
minutes = int(m.group("minutes2"))
|
||||
return [(minutes, 'minutes')]
|
||||
|
||||
def _parse_slot_delta(m):
|
||||
count = int(m.group("count"))
|
||||
unit = m.group("unit")
|
||||
if unit not in _SHORT_UNITS:
|
||||
raise ValueError("Unknown unit '%s' in interval '%s'" % (unit, m.group()))
|
||||
return [(count, _SHORT_UNITS[unit])]
|
||||
|
||||
_SLOT_PARSERS = {
|
||||
'date': _parse_slot_date,
|
||||
'mday': _parse_slot_mday,
|
||||
'wday': _parse_slot_wday,
|
||||
'time': _parse_slot_time,
|
||||
'mins': _parse_slot_mins,
|
||||
'delta': _parse_slot_delta,
|
||||
}
|
||||
615
sandbox/grist/functions/stats.py
Normal file
615
sandbox/grist/functions/stats.py
Normal file
@@ -0,0 +1,615 @@
|
||||
# pylint: disable=redefined-builtin, line-too-long, unused-argument
|
||||
|
||||
from math import _chain, _chain_numeric, _chain_numeric_a
|
||||
from info import ISNUMBER, ISLOGICAL
|
||||
from date import DATE # pylint: disable=unused-import
|
||||
|
||||
|
||||
def _average(iterable):
|
||||
total, count = 0.0, 0
|
||||
for value in iterable:
|
||||
total += value
|
||||
count += 1
|
||||
return total / count
|
||||
|
||||
def _default_if_empty(iterable, default):
|
||||
"""
|
||||
Yields all values from iterable, except when it is empty, yields just the single default value.
|
||||
"""
|
||||
empty = True
|
||||
for value in iterable:
|
||||
empty = False
|
||||
yield value
|
||||
if empty:
|
||||
yield default
|
||||
|
||||
|
||||
def AVEDEV(value1, value2):
|
||||
"""Calculates the average of the magnitudes of deviations of data from a dataset's mean."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def AVERAGE(value, *more_values):
|
||||
"""
|
||||
Returns the numerical average value in a dataset, ignoring non-numerical values.
|
||||
|
||||
Each argument may be a value or an array. Values that are not numbers, including logical
|
||||
and blank values, and text representations of numbers, are ignored.
|
||||
|
||||
>>> AVERAGE([2, -1.0, 11])
|
||||
4.0
|
||||
>>> AVERAGE([2, -1, 11, "Hello"])
|
||||
4.0
|
||||
>>> AVERAGE([2, -1, "Hello", DATE(2015,1,1)], True, [False, "123", "", 11])
|
||||
4.0
|
||||
>>> AVERAGE(False, True)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ZeroDivisionError: float division by zero
|
||||
"""
|
||||
return _average(_chain_numeric(value, *more_values))
|
||||
|
||||
|
||||
def AVERAGEA(value, *more_values):
|
||||
"""
|
||||
Returns the numerical average value in a dataset, counting non-numerical values as 0.
|
||||
|
||||
Each argument may be a value of an array. Values that are not numbers, including dates and text
|
||||
representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and
|
||||
False as 0.
|
||||
|
||||
>>> AVERAGEA([2, -1.0, 11])
|
||||
4.0
|
||||
>>> AVERAGEA([2, -1, 11, "Hello"])
|
||||
3.0
|
||||
>>> AVERAGEA([2, -1, "Hello", DATE(2015,1,1)], True, [False, "123", "", 11.5])
|
||||
1.5
|
||||
>>> AVERAGEA(False, True)
|
||||
0.5
|
||||
"""
|
||||
return _average(_chain_numeric_a(value, *more_values))
|
||||
|
||||
# Note that Google Sheets offers a similar function, called AVERAGE.WEIGHTED
|
||||
# (https://support.google.com/docs/answer/9084098?hl=en)
|
||||
def AVERAGE_WEIGHTED(pairs):
|
||||
"""
|
||||
Given a list of (value, weight) pairs, finds the average of the values weighted by the
|
||||
corresponding weights. Ignores any pairs with a non-numerical value or weight.
|
||||
|
||||
If you have two lists, of values and weights, use the Python built-in zip() function to create a
|
||||
list of pairs.
|
||||
|
||||
>>> AVERAGE_WEIGHTED(((95, .25), (90, .1), ("X", .5), (85, .15), (88, .2), (82, .3), (70, None)))
|
||||
87.7
|
||||
>>> AVERAGE_WEIGHTED(zip([95, 90, "X", 85, 88, 82, 70], [25, 10, 50, 15, 20, 30, None]))
|
||||
87.7
|
||||
>>> AVERAGE_WEIGHTED(zip([95, 90, False, 85, 88, 82, 70], [.25, .1, .5, .15, .2, .3, True]))
|
||||
87.7
|
||||
"""
|
||||
sum_value, sum_weight = 0.0, 0.0
|
||||
for value, weight in pairs:
|
||||
# The type-checking here is the same as used by _chain_numeric.
|
||||
if ISNUMBER(value) and not ISLOGICAL(value) and ISNUMBER(weight) and not ISLOGICAL(weight):
|
||||
sum_value += value * weight
|
||||
sum_weight += weight
|
||||
return sum_value / sum_weight
|
||||
|
||||
|
||||
def AVERAGEIF(criteria_range, criterion, average_range=None):
|
||||
"""Returns the average of a range depending on criteria."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def AVERAGEIFS(average_range, criteria_range1, criterion1, *args):
|
||||
"""Returns the average of a range depending on multiple criteria."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def BINOMDIST(num_successes, num_trials, prob_success, cumulative):
|
||||
"""
|
||||
Calculates the probability of drawing a certain number of successes (or a maximum number of
|
||||
successes) in a certain number of tries given a population of a certain size containing a
|
||||
certain number of successes, with replacement of draws.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def CONFIDENCE(alpha, standard_deviation, pop_size):
|
||||
"""Calculates the width of half the confidence interval for a normal distribution."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def CORREL(data_y, data_x):
|
||||
"""Calculates r, the Pearson product-moment correlation coefficient of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def COUNT(value, *more_values):
|
||||
"""
|
||||
Returns the count of numerical values in a dataset, ignoring non-numerical values.
|
||||
|
||||
Each argument may be a value or an array. Values that are not numbers, including logical
|
||||
and blank values, and text representations of numbers, are ignored.
|
||||
|
||||
>>> COUNT([2, -1.0, 11])
|
||||
3
|
||||
>>> COUNT([2, -1, 11, "Hello"])
|
||||
3
|
||||
>>> COUNT([2, -1, "Hello", DATE(2015,1,1)], True, [False, "123", "", 11.5])
|
||||
3
|
||||
>>> COUNT(False, True)
|
||||
0
|
||||
"""
|
||||
return sum(1 for v in _chain_numeric(value, *more_values))
|
||||
|
||||
|
||||
def COUNTA(value, *more_values):
|
||||
"""
|
||||
Returns the count of all values in a dataset, including non-numerical values.
|
||||
|
||||
Each argument may be a value or an array.
|
||||
|
||||
>>> COUNTA([2, -1.0, 11])
|
||||
3
|
||||
>>> COUNTA([2, -1, 11, "Hello"])
|
||||
4
|
||||
>>> COUNTA([2, -1, "Hello", DATE(2015,1,1)], True, [False, "123", "", 11.5])
|
||||
9
|
||||
>>> COUNTA(False, True)
|
||||
2
|
||||
"""
|
||||
return sum(1 for v in _chain(value, *more_values))
|
||||
|
||||
|
||||
def COVAR(data_y, data_x):
|
||||
"""Calculates the covariance of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def CRITBINOM(num_trials, prob_success, target_prob):
|
||||
"""Calculates the smallest value for which the cumulative binomial distribution is greater than or equal to a specified criteria."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def DEVSQ(value1, value2):
|
||||
"""Calculates the sum of squares of deviations based on a sample."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def EXPONDIST(x, lambda_, cumulative):
|
||||
"""Returns the value of the exponential distribution function with a specified lambda at a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def F_DIST(x, degrees_freedom1, degrees_freedom2, cumulative):
|
||||
"""
|
||||
Calculates the left-tailed F probability distribution (degree of diversity) for two data sets
|
||||
with given input x. Alternately called Fisher-Snedecor distribution or Snedecor's F
|
||||
distribution.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def F_DIST_RT(x, degrees_freedom1, degrees_freedom2):
|
||||
"""
|
||||
Calculates the right-tailed F probability distribution (degree of diversity) for two data sets
|
||||
with given input x. Alternately called Fisher-Snedecor distribution or Snedecor's F
|
||||
distribution.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def FDIST(x, degrees_freedom1, degrees_freedom2):
|
||||
"""
|
||||
Calculates the right-tailed F probability distribution (degree of diversity) for two data sets
|
||||
with given input x. Alternately called Fisher-Snedecor distribution or Snedecor's F
|
||||
distribution.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def FISHER(value):
|
||||
"""Returns the Fisher transformation of a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def FISHERINV(value):
|
||||
"""Returns the inverse Fisher transformation of a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def FORECAST(x, data_y, data_x):
|
||||
"""Calculates the expected y-value for a specified x based on a linear regression of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def GEOMEAN(value1, value2):
|
||||
"""Calculates the geometric mean of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def HARMEAN(value1, value2):
|
||||
"""Calculates the harmonic mean of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def HYPGEOMDIST(num_successes, num_draws, successes_in_pop, pop_size):
|
||||
"""Calculates the probability of drawing a certain number of successes in a certain number of tries given a population of a certain size containing a certain number of successes, without replacement of draws."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def INTERCEPT(data_y, data_x):
|
||||
"""Calculates the y-value at which the line resulting from linear regression of a dataset will intersect the y-axis (x=0)."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def KURT(value1, value2):
|
||||
"""Calculates the kurtosis of a dataset, which describes the shape, and in particular the "peakedness" of that dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def LARGE(data, n):
|
||||
"""Returns the nth largest element from a data set, where n is user-defined."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def LOGINV(x, mean, standard_deviation):
|
||||
"""Returns the value of the inverse log-normal cumulative distribution with given mean and standard deviation at a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def LOGNORMDIST(x, mean, standard_deviation):
|
||||
"""Returns the value of the log-normal cumulative distribution with given mean and standard deviation at a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
def MAX(value, *more_values):
|
||||
"""
|
||||
Returns the maximum value in a dataset, ignoring non-numerical values.
|
||||
|
||||
Each argument may be a value or an array. Values that are not numbers, including logical
|
||||
and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments
|
||||
contain no numbers.
|
||||
|
||||
>>> MAX([2, -1.5, 11.5])
|
||||
11.5
|
||||
>>> MAX([2, -1.5, "Hello", DATE(2015, 1, 1)], True, [False, "123", "", 11.5])
|
||||
11.5
|
||||
>>> MAX(True, -123)
|
||||
-123
|
||||
>>> MAX("123", -123)
|
||||
-123
|
||||
>>> MAX("Hello", "123", DATE(2015, 1, 1))
|
||||
0
|
||||
"""
|
||||
return max(_default_if_empty(_chain_numeric(value, *more_values), 0))
|
||||
|
||||
|
||||
def MAXA(value, *more_values):
|
||||
"""
|
||||
Returns the maximum numeric value in a dataset.
|
||||
|
||||
Each argument may be a value of an array. Values that are not numbers, including dates and text
|
||||
representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and
|
||||
False as 0. Returns 0 if the arguments contain no numbers.
|
||||
|
||||
>>> MAXA([2, -1.5, 11.5])
|
||||
11.5
|
||||
>>> MAXA([2, -1.5, "Hello", DATE(2015, 1, 1)], True, [False, "123", "", 11.5])
|
||||
11.5
|
||||
>>> MAXA(True, -123)
|
||||
1
|
||||
>>> MAXA("123", -123)
|
||||
0
|
||||
>>> MAXA("Hello", "123", DATE(2015, 1, 1))
|
||||
0
|
||||
"""
|
||||
return max(_default_if_empty(_chain_numeric_a(value, *more_values), 0))
|
||||
|
||||
|
||||
def MEDIAN(value, *more_values):
|
||||
"""
|
||||
Returns the median value in a numeric dataset, ignoring non-numerical values.
|
||||
|
||||
Each argument may be a value or an array. Values that are not numbers, including logical
|
||||
and blank values, and text representations of numbers, are ignored.
|
||||
|
||||
Produces an error if the arguments contain no numbers.
|
||||
|
||||
The median is the middle number when all values are sorted. So half of the values in the dataset
|
||||
are less than the median, and half of the values are greater. If there is an even number of
|
||||
values in the dataset, returns the average of the two numbers in the middle.
|
||||
|
||||
>>> MEDIAN(1, 2, 3, 4, 5)
|
||||
3
|
||||
>>> MEDIAN(3, 5, 1, 4, 2)
|
||||
3
|
||||
>>> MEDIAN(xrange(10))
|
||||
4.5
|
||||
>>> MEDIAN("Hello", "123", DATE(2015, 1, 1), 12.3)
|
||||
12.3
|
||||
>>> MEDIAN("Hello", "123", DATE(2015, 1, 1))
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: MEDIAN requires at least one number
|
||||
"""
|
||||
values = sorted(_chain_numeric(value, *more_values))
|
||||
if not values:
|
||||
raise ValueError("MEDIAN requires at least one number")
|
||||
count = len(values)
|
||||
if count % 2 == 0:
|
||||
return (values[count / 2 - 1] + values[count / 2]) / 2.0
|
||||
else:
|
||||
return values[(count - 1) / 2]
|
||||
|
||||
|
||||
def MIN(value, *more_values):
|
||||
"""
|
||||
Returns the minimum value in a dataset, ignoring non-numerical values.
|
||||
|
||||
Each argument may be a value or an array. Values that are not numbers, including logical
|
||||
and blank values, and text representations of numbers, are ignored. Returns 0 if the arguments
|
||||
contain no numbers.
|
||||
|
||||
>>> MIN([2, -1.5, 11.5])
|
||||
-1.5
|
||||
>>> MIN([2, -1.5, "Hello", DATE(2015, 1, 1)], True, [False, "123", "", 11.5])
|
||||
-1.5
|
||||
>>> MIN(True, 123)
|
||||
123
|
||||
>>> MIN("-123", 123)
|
||||
123
|
||||
>>> MIN("Hello", "123", DATE(2015, 1, 1))
|
||||
0
|
||||
"""
|
||||
return min(_default_if_empty(_chain_numeric(value, *more_values), 0))
|
||||
|
||||
def MINA(value, *more_values):
|
||||
"""
|
||||
Returns the minimum numeric value in a dataset.
|
||||
|
||||
Each argument may be a value of an array. Values that are not numbers, including dates and text
|
||||
representations of numbers, are counted as 0 (zero). Logical value of True is counted as 1, and
|
||||
False as 0. Returns 0 if the arguments contain no numbers.
|
||||
|
||||
>>> MINA([2, -1.5, 11.5])
|
||||
-1.5
|
||||
>>> MINA([2, -1.5, "Hello", DATE(2015, 1, 1)], True, [False, "123", "", 11.5])
|
||||
-1.5
|
||||
>>> MINA(True, 123)
|
||||
1
|
||||
>>> MINA("-123", 123)
|
||||
0
|
||||
>>> MINA("Hello", "123", DATE(2015, 1, 1))
|
||||
0
|
||||
"""
|
||||
return min(_default_if_empty(_chain_numeric_a(value, *more_values), 0))
|
||||
|
||||
|
||||
def MODE(value1, value2):
|
||||
"""Returns the most commonly occurring value in a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def NEGBINOMDIST(num_failures, num_successes, prob_success):
|
||||
"""Calculates the probability of drawing a certain number of failures before a certain number of successes given a probability of success in independent trials."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def NORMDIST(x, mean, standard_deviation, cumulative):
|
||||
"""
|
||||
Returns the value of the normal distribution function (or normal cumulative distribution
|
||||
function) for a specified value, mean, and standard deviation.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def NORMINV(x, mean, standard_deviation):
|
||||
"""Returns the value of the inverse normal distribution function for a specified value, mean, and standard deviation."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def NORMSDIST(x):
|
||||
"""Returns the value of the standard normal cumulative distribution function for a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def NORMSINV(x):
|
||||
"""Returns the value of the inverse standard normal distribution function for a specified value."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def PEARSON(data_y, data_x):
|
||||
"""Calculates r, the Pearson product-moment correlation coefficient of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def PERCENTILE(data, percentile):
|
||||
"""Returns the value at a given percentile of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def PERCENTRANK(data, value, significant_digits=None):
|
||||
"""Returns the percentage rank (percentile) of a specified value in a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def PERCENTRANK_EXC(data, value, significant_digits=None):
|
||||
"""Returns the percentage rank (percentile) from 0 to 1 exclusive of a specified value in a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def PERCENTRANK_INC(data, value, significant_digits=None):
|
||||
"""Returns the percentage rank (percentile) from 0 to 1 inclusive of a specified value in a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def PERMUT(n, k):
|
||||
"""Returns the number of ways to choose some number of objects from a pool of a given size of objects, considering order."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def POISSON(x, mean, cumulative):
|
||||
"""
|
||||
Returns the value of the Poisson distribution function (or Poisson cumulative distribution
|
||||
function) for a specified value and mean.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def PROB(data, probabilities, low_limit, high_limit=None):
|
||||
"""Given a set of values and corresponding probabilities, calculates the probability that a value chosen at random falls between two limits."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def QUARTILE(data, quartile_number):
|
||||
"""Returns a value nearest to a specified quartile of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def RANK(value, data, is_ascending=None):
|
||||
"""Returns the rank of a specified value in a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def RANK_AVG(value, data, is_ascending=None):
|
||||
"""Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the average rank of the entries will be returned."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def RANK_EQ(value, data, is_ascending=None):
|
||||
"""Returns the rank of a specified value in a dataset. If there is more than one entry of the same value in the dataset, the top rank of the entries will be returned."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def RSQ(data_y, data_x):
|
||||
"""Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def SKEW(value1, value2):
|
||||
"""Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def SLOPE(data_y, data_x):
|
||||
"""Calculates the slope of the line resulting from linear regression of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def SMALL(data, n):
|
||||
"""Returns the nth smallest element from a data set, where n is user-defined."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def STANDARDIZE(value, mean, standard_deviation):
|
||||
"""Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution."""
|
||||
raise NotImplementedError()
|
||||
|
||||
# This should make us all cry a little. Because the sandbox does not do Python3 (which has
|
||||
# statistics package), and because it does not do numpy (because it's native and hasn't been built
|
||||
# for it), we have to implement simple stats functions by hand.
|
||||
# TODO: switch to use the statistics package instead, once we upgrade to Python3.
|
||||
#
|
||||
# The following implementation of stdev is taken from https://stackoverflow.com/a/27758326/328565
|
||||
def _mean(data):
|
||||
return sum(data) / float(len(data))
|
||||
|
||||
def _ss(data):
|
||||
"""Return sum of square deviations of sequence data."""
|
||||
c = _mean(data)
|
||||
return sum((x-c)**2 for x in data)
|
||||
|
||||
def _stddev(data, ddof=0):
|
||||
"""Calculates the population standard deviation
|
||||
by default; specify ddof=1 to compute the sample
|
||||
standard deviation."""
|
||||
n = len(data)
|
||||
ss = _ss(data)
|
||||
pvar = ss/(n-ddof)
|
||||
return pvar**0.5
|
||||
|
||||
# The examples in the doctests below come from https://support.google.com/docs/answer/3094054 and
|
||||
# related articles, which helps ensure correctness and compatibility.
|
||||
def STDEV(value, *more_values):
|
||||
"""
|
||||
Calculates the standard deviation based on a sample, ignoring non-numerical values.
|
||||
|
||||
>>> STDEV([2, 5, 8, 13, 10])
|
||||
4.277849927241488
|
||||
>>> STDEV([2, 5, 8, 13, 10, True, False, "Test"])
|
||||
4.277849927241488
|
||||
>>> STDEV([2, 5, 8, 13, 10], 3, 12, 15)
|
||||
4.810702354423639
|
||||
>>> STDEV([2, 5, 8, 13, 10], [3, 12, 15])
|
||||
4.810702354423639
|
||||
>>> STDEV([5])
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ZeroDivisionError: float division by zero
|
||||
"""
|
||||
return _stddev(list(_chain_numeric(value, *more_values)), 1)
|
||||
|
||||
def STDEVA(value, *more_values):
|
||||
"""
|
||||
Calculates the standard deviation based on a sample, setting text to the value `0`.
|
||||
|
||||
>>> STDEVA([2, 5, 8, 13, 10])
|
||||
4.277849927241488
|
||||
>>> STDEVA([2, 5, 8, 13, 10, True, False, "Test"])
|
||||
4.969550137731641
|
||||
>>> STDEVA([2, 5, 8, 13, 10], 1, 0, 0)
|
||||
4.969550137731641
|
||||
>>> STDEVA([2, 5, 8, 13, 10], [1, 0, 0])
|
||||
4.969550137731641
|
||||
>>> STDEVA([5])
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ZeroDivisionError: float division by zero
|
||||
"""
|
||||
return _stddev(list(_chain_numeric_a(value, *more_values)), 1)
|
||||
|
||||
def STDEVP(value, *more_values):
|
||||
"""
|
||||
Calculates the standard deviation based on an entire population, ignoring non-numerical values.
|
||||
|
||||
>>> STDEVP([2, 5, 8, 13, 10])
|
||||
3.8262252939417984
|
||||
>>> STDEVP([2, 5, 8, 13, 10, True, False, "Test"])
|
||||
3.8262252939417984
|
||||
>>> STDEVP([2, 5, 8, 13, 10], 3, 12, 15)
|
||||
4.5
|
||||
>>> STDEVP([2, 5, 8, 13, 10], [3, 12, 15])
|
||||
4.5
|
||||
>>> STDEVP([5])
|
||||
0.0
|
||||
"""
|
||||
return _stddev(list(_chain_numeric(value, *more_values)), 0)
|
||||
|
||||
def STDEVPA(value, *more_values):
|
||||
"""
|
||||
Calculates the standard deviation based on an entire population, setting text to the value `0`.
|
||||
|
||||
>>> STDEVPA([2, 5, 8, 13, 10])
|
||||
3.8262252939417984
|
||||
>>> STDEVPA([2, 5, 8, 13, 10, True, False, "Test"])
|
||||
4.648588495446763
|
||||
>>> STDEVPA([2, 5, 8, 13, 10], 1, 0, 0)
|
||||
4.648588495446763
|
||||
>>> STDEVPA([2, 5, 8, 13, 10], [1, 0, 0])
|
||||
4.648588495446763
|
||||
>>> STDEVPA([5])
|
||||
0.0
|
||||
"""
|
||||
return _stddev(list(_chain_numeric_a(value, *more_values)), 0)
|
||||
|
||||
def STEYX(data_y, data_x):
|
||||
"""Calculates the standard error of the predicted y-value for each x in the regression of a dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def T_INV(probability, degrees_freedom):
|
||||
"""Calculates the negative inverse of the one-tailed TDIST function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def T_INV_2T(probability, degrees_freedom):
|
||||
"""Calculates the inverse of the two-tailed TDIST function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def TDIST(x, degrees_freedom, tails):
|
||||
"""Calculates the probability for Student's t-distribution with a given input (x)."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def TINV(probability, degrees_freedom):
|
||||
"""Calculates the inverse of the two-tailed TDIST function."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def TRIMMEAN(data, exclude_proportion):
|
||||
"""Calculates the mean of a dataset excluding some proportion of data from the high and low ends of the dataset."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def TTEST(range1, range2, tails, type):
|
||||
"""Returns the probability associated with t-test. Determines whether two samples are likely to have come from the same two underlying populations that have the same mean."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def VAR(value1, value2):
|
||||
"""Calculates the variance based on a sample."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def VARA(value1, value2):
|
||||
"""Calculates an estimate of variance based on a sample, setting text to the value `0`."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def VARP(value1, value2):
|
||||
"""Calculates the variance based on an entire population."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def VARPA(value1, value2):
|
||||
"""Calculates the variance based on an entire population, setting text to the value `0`."""
|
||||
raise NotImplementedError()
|
||||
|
||||
def WEIBULL(x, shape, scale, cumulative):
|
||||
"""
|
||||
Returns the value of the Weibull distribution function (or Weibull cumulative distribution
|
||||
function) for a specified shape and scale.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def ZTEST(data, value, standard_deviation):
|
||||
"""Returns the two-tailed P-value of a Z-test with standard distribution."""
|
||||
raise NotImplementedError()
|
||||
270
sandbox/grist/functions/test_schedule.py
Normal file
270
sandbox/grist/functions/test_schedule.py
Normal file
@@ -0,0 +1,270 @@
|
||||
from datetime import date, datetime, timedelta
|
||||
import os
|
||||
import timeit
|
||||
import unittest
|
||||
|
||||
import moment
|
||||
import schedule
|
||||
from functions.date import DTIME
|
||||
from functions import date as _date
|
||||
|
||||
DT = DTIME
|
||||
|
||||
TICK = timedelta.resolution
|
||||
_orig_global_tz_getter = None
|
||||
|
||||
class TestSchedule(unittest.TestCase):
|
||||
def assertDate(self, date_or_dtime, expected_str):
|
||||
"""Formats date_or_dtime and compares the formatted value."""
|
||||
return self.assertEqual(date_or_dtime.strftime("%Y-%m-%d %H:%M:%S"), expected_str)
|
||||
|
||||
def assertDateIso(self, date_or_dtime, expected_str):
|
||||
"""Formats date_or_dtime and compares the formatted value."""
|
||||
return self.assertEqual(date_or_dtime.isoformat(' '), expected_str)
|
||||
|
||||
def assertDelta(self, delta, months=0, **timedelta_args):
|
||||
"""Asserts that the given delta corresponds to the given number of various units."""
|
||||
self.assertEqual(delta._months, months)
|
||||
self.assertEqual(delta._timedelta, timedelta(**timedelta_args))
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
global _orig_global_tz_getter # pylint: disable=global-statement
|
||||
_orig_global_tz_getter = _date._get_global_tz
|
||||
_date._get_global_tz = lambda: moment.tzinfo('America/New_York')
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
_date._get_global_tz = _orig_global_tz_getter
|
||||
|
||||
|
||||
def test_round_down_to_unit(self):
|
||||
RDU = schedule._round_down_to_unit
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11"), "years"), "2018-01-01 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-01-01 00:00:00"), "years"), "2018-01-01 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-01-01 00:00:00") - TICK, "years"), "2017-01-01 00:00:00")
|
||||
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11"), "months"), "2018-09-01 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-01 00:00:00"), "months"), "2018-09-01 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-01 00:00:00") - TICK, "months"), "2018-08-01 00:00:00")
|
||||
|
||||
# Note that 9/4 was a Tuesday, so start of the week (Sunday) is 9/2
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11"), "weeks"), "2018-09-02 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-02 00:00:00"), "weeks"), "2018-09-02 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-02 00:00:00") - TICK, "weeks"), "2018-08-26 00:00:00")
|
||||
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11"), "days"), "2018-09-04 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-04 00:00:00"), "days"), "2018-09-04 00:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-04 00:00:00") - TICK, "days"), "2018-09-03 00:00:00")
|
||||
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11"), "hours"), "2018-09-04 14:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-04 14:00:00"), "hours"), "2018-09-04 14:00:00")
|
||||
self.assertDate(RDU(DT("2018-09-04 14:00:00") - TICK, "hours"), "2018-09-04 13:00:00")
|
||||
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11"), "minutes"), "2018-09-04 14:38:00")
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:00"), "minutes"), "2018-09-04 14:38:00")
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:00") - TICK, "minutes"), "2018-09-04 14:37:00")
|
||||
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11"), "seconds"), "2018-09-04 14:38:11")
|
||||
self.assertDate(RDU(DT("2018-09-04 14:38:11") - TICK, "seconds"), "2018-09-04 14:38:10")
|
||||
|
||||
with self.assertRaisesRegexp(ValueError, r"Invalid unit inches"):
|
||||
RDU(DT("2018-09-04 14:38:11"), "inches")
|
||||
|
||||
def test_round_down_to_unit_tz(self):
|
||||
RDU = schedule._round_down_to_unit
|
||||
dt = datetime(2018, 1, 1, 0, 0, 0, tzinfo=moment.tzinfo("America/New_York"))
|
||||
self.assertDateIso(RDU(dt, "years"), "2018-01-01 00:00:00-05:00")
|
||||
self.assertDateIso(RDU(dt - TICK, "years"), "2017-01-01 00:00:00-05:00")
|
||||
|
||||
self.assertDateIso(RDU(dt, "months"), "2018-01-01 00:00:00-05:00")
|
||||
self.assertDateIso(RDU(dt - TICK, "months"), "2017-12-01 00:00:00-05:00")
|
||||
|
||||
# 2018-01-01 is a Monday
|
||||
self.assertDateIso(RDU(dt, "weeks"), "2017-12-31 00:00:00-05:00")
|
||||
self.assertDateIso(RDU(dt - timedelta(days=1) - TICK, "weeks"), "2017-12-24 00:00:00-05:00")
|
||||
|
||||
self.assertDateIso(RDU(dt, "days"), "2018-01-01 00:00:00-05:00")
|
||||
self.assertDateIso(RDU(dt - TICK, "days"), "2017-12-31 00:00:00-05:00")
|
||||
|
||||
self.assertDateIso(RDU(dt, "hours"), "2018-01-01 00:00:00-05:00")
|
||||
self.assertDateIso(RDU(dt - TICK, "hours"), "2017-12-31 23:00:00-05:00")
|
||||
|
||||
def test_parse_interval(self):
|
||||
self.assertEqual(schedule._parse_interval("annual"), (1, "years"))
|
||||
self.assertEqual(schedule._parse_interval("daily"), (1, "days"))
|
||||
self.assertEqual(schedule._parse_interval("1-year"), (1, "years"))
|
||||
self.assertEqual(schedule._parse_interval("1 year"), (1, "years"))
|
||||
self.assertEqual(schedule._parse_interval("1 Years"), (1, "years"))
|
||||
self.assertEqual(schedule._parse_interval("25-months"), (25, "months"))
|
||||
self.assertEqual(schedule._parse_interval("3-day"), (3, "days"))
|
||||
self.assertEqual(schedule._parse_interval("2-hour"), (2, "hours"))
|
||||
with self.assertRaisesRegexp(ValueError, "Not a valid interval"):
|
||||
schedule._parse_interval("1Year")
|
||||
with self.assertRaisesRegexp(ValueError, "Not a valid interval"):
|
||||
schedule._parse_interval("1y")
|
||||
with self.assertRaisesRegexp(ValueError, "Unknown unit"):
|
||||
schedule._parse_interval("1-daily")
|
||||
|
||||
def test_parse_slot(self):
|
||||
self.assertDelta(schedule._parse_slot('Jan-15', 'years'), months=0, days=14)
|
||||
self.assertDelta(schedule._parse_slot('1/15', 'years'), months=0, days=14)
|
||||
self.assertDelta(schedule._parse_slot('march-1', 'years'), months=2, days=0)
|
||||
self.assertDelta(schedule._parse_slot('03/09', 'years'), months=2, days=8)
|
||||
|
||||
self.assertDelta(schedule._parse_slot('/15', 'months'), days=14)
|
||||
self.assertDelta(schedule._parse_slot('/1', 'months'), days=0)
|
||||
|
||||
self.assertDelta(schedule._parse_slot('Mon', 'weeks'), days=1)
|
||||
self.assertDelta(schedule._parse_slot('tu', 'weeks'), days=2)
|
||||
self.assertDelta(schedule._parse_slot('Friday', 'weeks'), days=5)
|
||||
|
||||
self.assertDelta(schedule._parse_slot('10am', 'days'), hours=10)
|
||||
self.assertDelta(schedule._parse_slot('1:30pm', 'days'), hours=13, minutes=30)
|
||||
self.assertDelta(schedule._parse_slot('15:45', 'days'), hours=15, minutes=45)
|
||||
self.assertDelta(schedule._parse_slot('Apr-1 9am', 'years'), months=3, days=0, hours=9)
|
||||
self.assertDelta(schedule._parse_slot('/3 12:30', 'months'), days=2, hours=12, minutes=30)
|
||||
self.assertDelta(schedule._parse_slot('Sat 6:15pm', 'weeks'), days=6, hours=18, minutes=15)
|
||||
|
||||
self.assertDelta(schedule._parse_slot(':45', 'hours'), minutes=45)
|
||||
self.assertDelta(schedule._parse_slot(':00', 'hours'), minutes=00)
|
||||
|
||||
self.assertDelta(schedule._parse_slot('+1d', 'days'), days=1)
|
||||
self.assertDelta(schedule._parse_slot('+15d', 'months'), days=15)
|
||||
self.assertDelta(schedule._parse_slot('+3w', 'weeks'), weeks=3)
|
||||
self.assertDelta(schedule._parse_slot('+2m', 'years'), months=2)
|
||||
self.assertDelta(schedule._parse_slot('+1y', 'years'), months=12)
|
||||
|
||||
# Test a few combinations.
|
||||
self.assertDelta(schedule._parse_slot('+1y 4/5 3:45pm +30S', 'years'),
|
||||
months=15, days=4, hours=15, minutes=45, seconds=30)
|
||||
self.assertDelta(schedule._parse_slot('+2w Wed +6H +20M +40S', 'weeks'),
|
||||
weeks=2, days=3, hours=6, minutes=20, seconds=40)
|
||||
self.assertDelta(schedule._parse_slot('+2m /20 11pm', 'months'), months=2, days=19, hours=23)
|
||||
self.assertDelta(schedule._parse_slot('+2M +30S', 'minutes'), minutes=2, seconds=30)
|
||||
|
||||
def test_parse_slot_errors(self):
|
||||
# Test failures with duplicate units
|
||||
with self.assertRaisesRegexp(ValueError, 'Duplicate unit'):
|
||||
schedule._parse_slot('+1d +2d', 'weeks')
|
||||
with self.assertRaisesRegexp(ValueError, 'Duplicate unit'):
|
||||
schedule._parse_slot('9:30am +2H', 'days')
|
||||
with self.assertRaisesRegexp(ValueError, 'Duplicate unit'):
|
||||
schedule._parse_slot('/15 +1d', 'months')
|
||||
with self.assertRaisesRegexp(ValueError, 'Duplicate unit'):
|
||||
schedule._parse_slot('Feb-1 12:30pm +20M', 'years')
|
||||
|
||||
# Test failures with improper slot types
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot.*for unit'):
|
||||
schedule._parse_slot('Feb-1', 'weeks')
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot.*for unit'):
|
||||
schedule._parse_slot('Monday', 'months')
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot.*for unit'):
|
||||
schedule._parse_slot('4/15', 'hours')
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot.*for unit'):
|
||||
schedule._parse_slot('/1', 'years')
|
||||
|
||||
# Test failures with outright invalid slot syntax.
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot'):
|
||||
schedule._parse_slot('Feb:1', 'weeks')
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot'):
|
||||
schedule._parse_slot('/1d', 'months')
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot'):
|
||||
schedule._parse_slot('10', 'hours')
|
||||
with self.assertRaisesRegexp(ValueError, 'Invalid slot'):
|
||||
schedule._parse_slot('H1', 'years')
|
||||
|
||||
# Test failures with unknown values
|
||||
with self.assertRaisesRegexp(ValueError, 'Unknown month'):
|
||||
schedule._parse_slot('februarium-1', 'years')
|
||||
with self.assertRaisesRegexp(ValueError, 'Unknown day of the week'):
|
||||
schedule._parse_slot('snu', 'weeks')
|
||||
with self.assertRaisesRegexp(ValueError, 'Unknown unit'):
|
||||
schedule._parse_slot('+1t', 'hours')
|
||||
|
||||
def test_schedule(self):
|
||||
# A few more examples. The ones in doctest strings are those that help documentation; the rest
|
||||
# are in this file to keep the size of the main file more manageable.
|
||||
|
||||
# Note that the start of 2018-01-01 is a Monday
|
||||
self.assertEqual(list(schedule.SCHEDULE(
|
||||
"1-week: +1d 9:30am, +4d 3:30pm", start=datetime(2018,1,1), end=datetime(2018,1,31))),
|
||||
[
|
||||
DT("2018-01-01 09:30:00"), DT("2018-01-04 15:30:00"),
|
||||
DT("2018-01-08 09:30:00"), DT("2018-01-11 15:30:00"),
|
||||
DT("2018-01-15 09:30:00"), DT("2018-01-18 15:30:00"),
|
||||
DT("2018-01-22 09:30:00"), DT("2018-01-25 15:30:00"),
|
||||
DT("2018-01-29 09:30:00"),
|
||||
])
|
||||
|
||||
self.assertEqual(list(schedule.SCHEDULE(
|
||||
"3-month: +0d 12pm", start=datetime(2018,1,1), end=datetime(2018,6,30))),
|
||||
[DT('2018-01-01 12:00:00'), DT('2018-04-01 12:00:00')])
|
||||
|
||||
# Ensure we can use date() object for start/end too.
|
||||
self.assertEqual(list(schedule.SCHEDULE(
|
||||
"3-month: +0d 12pm", start=date(2018,1,1), end=date(2018,6,30))),
|
||||
[DT('2018-01-01 12:00:00'), DT('2018-04-01 12:00:00')])
|
||||
|
||||
# We can even use strings.
|
||||
self.assertEqual(list(schedule.SCHEDULE(
|
||||
"3-month: +0d 12pm", start="2018-01-01", end="2018-06-30")),
|
||||
[DT('2018-01-01 12:00:00'), DT('2018-04-01 12:00:00')])
|
||||
|
||||
def test_timezone(self):
|
||||
# Verify that the time zone of `start` determines the time zone of generated times.
|
||||
tz_ny = moment.tzinfo("America/New_York")
|
||||
self.assertEqual([d.isoformat(' ') for d in schedule.SCHEDULE(
|
||||
"daily: 9am", count=4, start=datetime(2018, 2, 14, tzinfo=tz_ny))],
|
||||
[ '2018-02-14 09:00:00-05:00', '2018-02-15 09:00:00-05:00',
|
||||
'2018-02-16 09:00:00-05:00', '2018-02-17 09:00:00-05:00' ])
|
||||
|
||||
tz_la = moment.tzinfo("America/Los_Angeles")
|
||||
self.assertEqual([d.isoformat(' ') for d in schedule.SCHEDULE(
|
||||
"daily: 9am, 4:30pm", count=4, start=datetime(2018, 2, 14, 9, 0, tzinfo=tz_la))],
|
||||
[ '2018-02-14 09:00:00-08:00', '2018-02-14 16:30:00-08:00',
|
||||
'2018-02-15 09:00:00-08:00', '2018-02-15 16:30:00-08:00' ])
|
||||
|
||||
tz_utc = moment.tzinfo("UTC")
|
||||
self.assertEqual([d.isoformat(' ') for d in schedule.SCHEDULE(
|
||||
"daily: 9am, 4:30pm", count=4, start=datetime(2018, 2, 14, 17, 0, tzinfo=tz_utc))],
|
||||
[ '2018-02-15 09:00:00+00:00', '2018-02-15 16:30:00+00:00',
|
||||
'2018-02-16 09:00:00+00:00', '2018-02-16 16:30:00+00:00' ])
|
||||
|
||||
# This is not really a test but just a way to see some timing information about Schedule
|
||||
# implementation. Run with env PY_TIMING_TESTS=1 in the environment, and the console output will
|
||||
# include the measured times.
|
||||
@unittest.skipUnless(os.getenv("PY_TIMING_TESTS") == "1", "Set PY_TIMING_TESTS=1 for timing")
|
||||
def test_timing(self):
|
||||
N = 1000
|
||||
sched = "weekly: Mo 10:30am, We 10:30am"
|
||||
setup = """
|
||||
from functions import schedule
|
||||
from datetime import datetime
|
||||
"""
|
||||
setup = "from functions import test_schedule as t"
|
||||
|
||||
expected_result = [
|
||||
datetime(2018, 9, 24, 10, 30), datetime(2018, 9, 26, 22, 30),
|
||||
datetime(2018, 10, 1, 10, 30), datetime(2018, 10, 3, 22, 30),
|
||||
]
|
||||
self.assertEqual(timing_schedule_full(), expected_result)
|
||||
t = min(timeit.repeat(stmt="t.timing_schedule_full()", setup=setup, number=N, repeat=3))
|
||||
print "\n*** SCHEDULE call with 4 points: %.2f us" % (t * 1000000 / N)
|
||||
|
||||
t = min(timeit.repeat(stmt="t.timing_schedule_init()", setup=setup, number=N, repeat=3))
|
||||
print "*** Schedule constructor: %.2f us" % (t * 1000000 / N)
|
||||
|
||||
self.assertEqual(timing_schedule_series(), expected_result)
|
||||
t = min(timeit.repeat(stmt="t.timing_schedule_series()", setup=setup, number=N, repeat=3))
|
||||
print "*** Schedule series with 4 points: %.2f us" % (t * 1000000 / N)
|
||||
|
||||
def timing_schedule_full():
|
||||
return list(schedule.SCHEDULE("weekly: Mo 10:30am, We 10:30pm",
|
||||
start=datetime(2018, 9, 23), count=4))
|
||||
|
||||
def timing_schedule_init():
|
||||
return schedule.Schedule("weekly: Mo 10:30am, We 10:30pm")
|
||||
|
||||
def timing_schedule_series(sched=schedule.Schedule("weekly: Mo 10:30am, We 10:30pm")):
|
||||
return list(sched.series(datetime(2018, 9, 23), None, count=4))
|
||||
590
sandbox/grist/functions/text.py
Normal file
590
sandbox/grist/functions/text.py
Normal file
@@ -0,0 +1,590 @@
|
||||
# -*- coding: UTF-8 -*-
|
||||
|
||||
import datetime
|
||||
import dateutil.parser
|
||||
import numbers
|
||||
import re
|
||||
|
||||
from usertypes import AltText # pylint: disable=import-error
|
||||
|
||||
def CHAR(table_number):
|
||||
"""
|
||||
Convert a number into a character according to the current Unicode table.
|
||||
Same as `unichr(number)`.
|
||||
|
||||
>>> CHAR(65)
|
||||
u'A'
|
||||
>>> CHAR(33)
|
||||
u'!'
|
||||
"""
|
||||
return unichr(table_number)
|
||||
|
||||
|
||||
# See http://stackoverflow.com/a/93029/328565
|
||||
_control_chars = ''.join(map(unichr, range(0,32) + range(127,160)))
|
||||
_control_char_re = re.compile('[%s]' % re.escape(_control_chars))
|
||||
|
||||
def CLEAN(text):
|
||||
"""
|
||||
Returns the text with the non-printable characters removed.
|
||||
|
||||
This removes both characters with values 0 through 31, and other Unicode characters in the
|
||||
"control characters" category.
|
||||
|
||||
>>> CLEAN(CHAR(9) + "Monthly report" + CHAR(10))
|
||||
u'Monthly report'
|
||||
"""
|
||||
return _control_char_re.sub('', text)
|
||||
|
||||
|
||||
def CODE(string):
|
||||
"""
|
||||
Returns the numeric Unicode map value of the first character in the string provided.
|
||||
Same as `ord(string[0])`.
|
||||
|
||||
>>> CODE("A")
|
||||
65
|
||||
>>> CODE("!")
|
||||
33
|
||||
>>> CODE("!A")
|
||||
33
|
||||
"""
|
||||
return ord(string[0])
|
||||
|
||||
|
||||
def CONCATENATE(string, *more_strings):
|
||||
"""
|
||||
Joins together any number of text strings into one string. Also available under the name
|
||||
`CONCAT`. Same as the Python expression `"".join(array_of_strings)`.
|
||||
|
||||
>>> CONCATENATE("Stream population for ", "trout", " ", "species", " is ", 32, "/mile.")
|
||||
u'Stream population for trout species is 32/mile.'
|
||||
>>> CONCATENATE("In ", 4, " days it is ", datetime.date(2016,1,1))
|
||||
u'In 4 days it is 2016-01-01'
|
||||
>>> CONCATENATE("abc")
|
||||
u'abc'
|
||||
>>> CONCAT(0, "abc")
|
||||
u'0abc'
|
||||
"""
|
||||
return u''.join(unicode(val) for val in (string,) + more_strings)
|
||||
|
||||
|
||||
CONCAT = CONCATENATE
|
||||
|
||||
|
||||
def DOLLAR(number, decimals=2):
|
||||
"""
|
||||
Formats a number into a formatted dollar amount, with decimals rounded to the specified place (.
|
||||
If decimals value is omitted, it defaults to 2.
|
||||
|
||||
>>> DOLLAR(1234.567)
|
||||
'$1,234.57'
|
||||
>>> DOLLAR(1234.567, -2)
|
||||
'$1,200'
|
||||
>>> DOLLAR(-1234.567, -2)
|
||||
'($1,200)'
|
||||
>>> DOLLAR(-0.123, 4)
|
||||
'($0.1230)'
|
||||
>>> DOLLAR(99.888)
|
||||
'$99.89'
|
||||
>>> DOLLAR(0)
|
||||
'$0.00'
|
||||
>>> DOLLAR(10, 0)
|
||||
'$10'
|
||||
"""
|
||||
formatted = "${:,.{}f}".format(round(abs(number), decimals), max(0, decimals))
|
||||
return formatted if number >= 0 else "(" + formatted + ")"
|
||||
|
||||
|
||||
def EXACT(string1, string2):
|
||||
"""
|
||||
Tests whether two strings are identical. Same as `string2 == string2`.
|
||||
|
||||
>>> EXACT("word", "word")
|
||||
True
|
||||
>>> EXACT("Word", "word")
|
||||
False
|
||||
>>> EXACT("w ord", "word")
|
||||
False
|
||||
"""
|
||||
return string1 == string2
|
||||
|
||||
|
||||
def FIND(find_text, within_text, start_num=1):
|
||||
"""
|
||||
Returns the position at which a string is first found within text.
|
||||
|
||||
Find is case-sensitive. The returned position is 1 if within_text starts with find_text.
|
||||
Start_num specifies the character at which to start the search, defaulting to 1 (the first
|
||||
character of within_text).
|
||||
|
||||
If find_text is not found, or start_num is invalid, raises ValueError.
|
||||
|
||||
>>> FIND("M", "Miriam McGovern")
|
||||
1
|
||||
>>> FIND("m", "Miriam McGovern")
|
||||
6
|
||||
>>> FIND("M", "Miriam McGovern", 3)
|
||||
8
|
||||
>>> FIND(" #", "Hello world # Test")
|
||||
12
|
||||
>>> FIND("gle", "Google", 1)
|
||||
4
|
||||
>>> FIND("GLE", "Google", 1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: substring not found
|
||||
>>> FIND("page", "homepage")
|
||||
5
|
||||
>>> FIND("page", "homepage", 6)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: substring not found
|
||||
"""
|
||||
return within_text.index(find_text, start_num - 1) + 1
|
||||
|
||||
|
||||
def FIXED(number, decimals=2, no_commas=False):
|
||||
"""
|
||||
Formats a number with a fixed number of decimal places (2 by default), and commas.
|
||||
If no_commas is True, then omits the commas.
|
||||
|
||||
>>> FIXED(1234.567, 1)
|
||||
'1,234.6'
|
||||
>>> FIXED(1234.567, -1)
|
||||
'1,230'
|
||||
>>> FIXED(-1234.567, -1, True)
|
||||
'-1230'
|
||||
>>> FIXED(44.332)
|
||||
'44.33'
|
||||
>>> FIXED(3521.478, 2, False)
|
||||
'3,521.48'
|
||||
>>> FIXED(-3521.478, 1, True)
|
||||
'-3521.5'
|
||||
>>> FIXED(3521.478, 0, True)
|
||||
'3521'
|
||||
>>> FIXED(3521.478, -2, True)
|
||||
'3500'
|
||||
"""
|
||||
comma_flag = '' if no_commas else ','
|
||||
return "{:{}.{}f}".format(round(number, decimals), comma_flag, max(0, decimals))
|
||||
|
||||
|
||||
def LEFT(string, num_chars=1):
|
||||
"""
|
||||
Returns a substring of length num_chars from the beginning of the given string. If num_chars is
|
||||
omitted, it is assumed to be 1. Same as `string[:num_chars]`.
|
||||
|
||||
>>> LEFT("Sale Price", 4)
|
||||
'Sale'
|
||||
>>> LEFT('Swededn')
|
||||
'S'
|
||||
>>> LEFT('Text', -1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: num_chars invalid
|
||||
"""
|
||||
if num_chars < 0:
|
||||
raise ValueError("num_chars invalid")
|
||||
return string[:num_chars]
|
||||
|
||||
|
||||
def LEN(text):
|
||||
"""
|
||||
Returns the number of characters in a text string. Same as `len(text)`.
|
||||
|
||||
>>> LEN("Phoenix, AZ")
|
||||
11
|
||||
>>> LEN("")
|
||||
0
|
||||
>>> LEN(" One ")
|
||||
11
|
||||
"""
|
||||
return len(text)
|
||||
|
||||
|
||||
def LOWER(text):
|
||||
"""
|
||||
Converts a specified string to lowercase. Same as `text.lower()`.
|
||||
|
||||
>>> LOWER("E. E. Cummings")
|
||||
'e. e. cummings'
|
||||
>>> LOWER("Apt. 2B")
|
||||
'apt. 2b'
|
||||
"""
|
||||
return text.lower()
|
||||
|
||||
|
||||
def MID(text, start_num, num_chars):
|
||||
"""
|
||||
Returns a segment of a string, starting at start_num. The first character in text has
|
||||
start_num 1.
|
||||
|
||||
>>> MID("Fluid Flow", 1, 5)
|
||||
'Fluid'
|
||||
>>> MID("Fluid Flow", 7, 20)
|
||||
'Flow'
|
||||
>>> MID("Fluid Flow", 20, 5)
|
||||
''
|
||||
>>> MID("Fluid Flow", 0, 5)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: start_num invalid
|
||||
"""
|
||||
if start_num < 1:
|
||||
raise ValueError("start_num invalid")
|
||||
return text[start_num - 1 : start_num - 1 + num_chars]
|
||||
|
||||
|
||||
def PROPER(text):
|
||||
"""
|
||||
Capitalizes each word in a specified string. It converts the first letter of each word to
|
||||
uppercase, and all other letters to lowercase. Same as `text.title()`.
|
||||
|
||||
>>> PROPER('this is a TITLE')
|
||||
'This Is A Title'
|
||||
>>> PROPER('2-way street')
|
||||
'2-Way Street'
|
||||
>>> PROPER('76BudGet')
|
||||
'76Budget'
|
||||
"""
|
||||
return text.title()
|
||||
|
||||
|
||||
def REGEXEXTRACT(text, regular_expression):
|
||||
"""
|
||||
Extracts the first part of text that matches regular_expression.
|
||||
|
||||
>>> REGEXEXTRACT("Google Doc 101", "[0-9]+")
|
||||
'101'
|
||||
>>> REGEXEXTRACT("The price today is $826.25", "[0-9]*\\.[0-9]+[0-9]+")
|
||||
'826.25'
|
||||
|
||||
If there is a parenthesized expression, it is returned instead of the whole match.
|
||||
>>> REGEXEXTRACT("(Content) between brackets", "\\(([A-Za-z]+)\\)")
|
||||
'Content'
|
||||
>>> REGEXEXTRACT("Foo", "Bar")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: REGEXEXTRACT text does not match
|
||||
"""
|
||||
m = re.search(regular_expression, text)
|
||||
if not m:
|
||||
raise ValueError("REGEXEXTRACT text does not match")
|
||||
return m.group(1) if m.lastindex else m.group(0)
|
||||
|
||||
|
||||
def REGEXMATCH(text, regular_expression):
|
||||
"""
|
||||
Returns whether a piece of text matches a regular expression.
|
||||
|
||||
>>> REGEXMATCH("Google Doc 101", "[0-9]+")
|
||||
True
|
||||
>>> REGEXMATCH("Google Doc", "[0-9]+")
|
||||
False
|
||||
>>> REGEXMATCH("The price today is $826.25", "[0-9]*\\.[0-9]+[0-9]+")
|
||||
True
|
||||
>>> REGEXMATCH("(Content) between brackets", "\\(([A-Za-z]+)\\)")
|
||||
True
|
||||
>>> REGEXMATCH("Foo", "Bar")
|
||||
False
|
||||
"""
|
||||
return bool(re.search(regular_expression, text))
|
||||
|
||||
|
||||
def REGEXREPLACE(text, regular_expression, replacement):
|
||||
"""
|
||||
Replaces all parts of text matching the given regular expression with replacement text.
|
||||
|
||||
>>> REGEXREPLACE("Google Doc 101", "[0-9]+", "777")
|
||||
'Google Doc 777'
|
||||
>>> REGEXREPLACE("Google Doc", "[0-9]+", "777")
|
||||
'Google Doc'
|
||||
>>> REGEXREPLACE("The price is $826.25", "[0-9]*\\.[0-9]+[0-9]+", "315.75")
|
||||
'The price is $315.75'
|
||||
>>> REGEXREPLACE("(Content) between brackets", "\\(([A-Za-z]+)\\)", "Word")
|
||||
'Word between brackets'
|
||||
>>> REGEXREPLACE("Foo", "Bar", "Baz")
|
||||
'Foo'
|
||||
"""
|
||||
return re.sub(regular_expression, replacement, text)
|
||||
|
||||
|
||||
def REPLACE(old_text, start_num, num_chars, new_text):
|
||||
"""
|
||||
Replaces part of a text string with a different text string. Start_num is counted from 1.
|
||||
|
||||
>>> REPLACE("abcdefghijk", 6, 5, "*")
|
||||
'abcde*k'
|
||||
>>> REPLACE("2009", 3, 2, "10")
|
||||
'2010'
|
||||
>>> REPLACE('123456', 1, 3, '@')
|
||||
'@456'
|
||||
>>> REPLACE('foo', 1, 0, 'bar')
|
||||
'barfoo'
|
||||
>>> REPLACE('foo', 0, 1, 'bar')
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: start_num invalid
|
||||
"""
|
||||
if start_num < 1:
|
||||
raise ValueError("start_num invalid")
|
||||
return old_text[:start_num - 1] + new_text + old_text[start_num - 1 + num_chars:]
|
||||
|
||||
|
||||
def REPT(text, number_times):
|
||||
"""
|
||||
Returns specified text repeated a number of times. Same as `text * number_times`.
|
||||
|
||||
The result of the REPT function cannot be longer than 32767 characters, or it raises a
|
||||
ValueError.
|
||||
|
||||
>>> REPT("*-", 3)
|
||||
'*-*-*-'
|
||||
>>> REPT('-', 10)
|
||||
'----------'
|
||||
>>> REPT('-', 0)
|
||||
''
|
||||
>>> len(REPT('---', 10000))
|
||||
30000
|
||||
>>> REPT('---', 11000)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: number_times invalid
|
||||
>>> REPT('-', -1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: number_times invalid
|
||||
"""
|
||||
if number_times < 0 or len(text) * number_times > 32767:
|
||||
raise ValueError("number_times invalid")
|
||||
return text * int(number_times)
|
||||
|
||||
|
||||
def RIGHT(string, num_chars=1):
|
||||
"""
|
||||
Returns a substring of length num_chars from the end of a specified string. If num_chars is
|
||||
omitted, it is assumed to be 1. Same as `string[-num_chars:]`.
|
||||
|
||||
>>> RIGHT("Sale Price", 5)
|
||||
'Price'
|
||||
>>> RIGHT('Stock Number')
|
||||
'r'
|
||||
>>> RIGHT('Text', 100)
|
||||
'Text'
|
||||
>>> RIGHT('Text', -1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: num_chars invalid
|
||||
"""
|
||||
if num_chars < 0:
|
||||
raise ValueError("num_chars invalid")
|
||||
return string[-num_chars:]
|
||||
|
||||
|
||||
def SEARCH(find_text, within_text, start_num=1):
|
||||
"""
|
||||
Returns the position at which a string is first found within text, ignoring case.
|
||||
|
||||
Find is case-sensitive. The returned position is 1 if within_text starts with find_text.
|
||||
Start_num specifies the character at which to start the search, defaulting to 1 (the first
|
||||
character of within_text).
|
||||
|
||||
If find_text is not found, or start_num is invalid, raises ValueError.
|
||||
>>> SEARCH("e", "Statements", 6)
|
||||
7
|
||||
>>> SEARCH("margin", "Profit Margin")
|
||||
8
|
||||
>>> SEARCH(" ", "Profit Margin")
|
||||
7
|
||||
>>> SEARCH('"', 'The "boss" is here.')
|
||||
5
|
||||
>>> SEARCH("gle", "Google")
|
||||
4
|
||||
>>> SEARCH("GLE", "Google")
|
||||
4
|
||||
"""
|
||||
# .lower() isn't always correct for unicode. See http://stackoverflow.com/a/29247821/328565
|
||||
return within_text.lower().index(find_text.lower(), start_num - 1) + 1
|
||||
|
||||
|
||||
def SUBSTITUTE(text, old_text, new_text, instance_num=None):
|
||||
u"""
|
||||
Replaces existing text with new text in a string. It is useful when you know the substring of
|
||||
text to replace. Use REPLACE when you know the position of text to replace.
|
||||
|
||||
If instance_num is given, it specifies which occurrence of old_text to replace. If omitted, all
|
||||
occurrences are replaced.
|
||||
|
||||
Same as `text.replace(old_text, new_text)` when instance_num is omitted.
|
||||
|
||||
>>> SUBSTITUTE("Sales Data", "Sales", "Cost")
|
||||
'Cost Data'
|
||||
>>> SUBSTITUTE("Quarter 1, 2008", "1", "2", 1)
|
||||
'Quarter 2, 2008'
|
||||
>>> SUBSTITUTE("Quarter 1, 2011", "1", "2", 3)
|
||||
'Quarter 1, 2012'
|
||||
|
||||
More tests:
|
||||
>>> SUBSTITUTE("Hello world", "", "-")
|
||||
'Hello world'
|
||||
>>> SUBSTITUTE("Hello world", " ", "-")
|
||||
'Hello-world'
|
||||
>>> SUBSTITUTE("Hello world", " ", 12.1)
|
||||
'Hello12.1world'
|
||||
>>> SUBSTITUTE(u"Hello world", u" ", 12.1)
|
||||
u'Hello12.1world'
|
||||
>>> SUBSTITUTE("Hello world", "world", "")
|
||||
'Hello '
|
||||
>>> SUBSTITUTE("Hello", "world", "")
|
||||
'Hello'
|
||||
|
||||
Overlapping matches are all counted when looking for instance_num.
|
||||
>>> SUBSTITUTE('abababab', 'abab', 'xxxx')
|
||||
'xxxxxxxx'
|
||||
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 1)
|
||||
'xxxxabab'
|
||||
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 2)
|
||||
'abxxxxab'
|
||||
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 3)
|
||||
'ababxxxx'
|
||||
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 4)
|
||||
'abababab'
|
||||
>>> SUBSTITUTE('abababab', 'abab', 'xxxx', 0)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: instance_num invalid
|
||||
"""
|
||||
if not old_text:
|
||||
return text
|
||||
|
||||
if not isinstance(new_text, basestring):
|
||||
new_text = str(new_text)
|
||||
|
||||
if instance_num is None:
|
||||
return text.replace(old_text, new_text)
|
||||
|
||||
if instance_num <= 0:
|
||||
raise ValueError("instance_num invalid")
|
||||
|
||||
# No trivial way to replace nth occurrence.
|
||||
i = -1
|
||||
for c in xrange(instance_num):
|
||||
i = text.find(old_text, i + 1)
|
||||
if i < 0:
|
||||
return text
|
||||
return text[:i] + new_text + text[i + len(old_text):]
|
||||
|
||||
|
||||
def T(value):
|
||||
"""
|
||||
Returns value if value is text, or the empty string when value is not text.
|
||||
|
||||
>>> T('Text')
|
||||
'Text'
|
||||
>>> T(826)
|
||||
''
|
||||
>>> T('826')
|
||||
'826'
|
||||
>>> T(False)
|
||||
''
|
||||
>>> T('100 points')
|
||||
'100 points'
|
||||
>>> T(AltText('Text'))
|
||||
'Text'
|
||||
>>> T(float('nan'))
|
||||
''
|
||||
"""
|
||||
return (value if isinstance(value, basestring) else
|
||||
str(value) if isinstance(value, AltText) else "")
|
||||
|
||||
|
||||
def TEXT(number, format_type):
|
||||
"""
|
||||
Converts a number into text according to a specified format. It is not yet implemented in
|
||||
Grist.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
_trim_re = re.compile(r' +')
|
||||
|
||||
def TRIM(text):
|
||||
"""
|
||||
Removes all spaces from text except for single spaces between words. Note that TRIM does not
|
||||
remove other whitespace such as tab or newline characters.
|
||||
|
||||
>>> TRIM(" First Quarter\\n Earnings ")
|
||||
'First Quarter\\n Earnings'
|
||||
>>> TRIM("")
|
||||
''
|
||||
"""
|
||||
return _trim_re.sub(' ', text.strip())
|
||||
|
||||
|
||||
def UPPER(text):
|
||||
"""
|
||||
Converts a specified string to uppercase. Same as `text.lower()`.
|
||||
|
||||
>>> UPPER("e. e. cummings")
|
||||
'E. E. CUMMINGS'
|
||||
>>> UPPER("Apt. 2B")
|
||||
'APT. 2B'
|
||||
"""
|
||||
return text.upper()
|
||||
|
||||
|
||||
def VALUE(text):
|
||||
"""
|
||||
Converts a string in accepted date, time or number formats into a number or date.
|
||||
|
||||
>>> VALUE("$1,000")
|
||||
1000
|
||||
>>> VALUE("16:48:00") - VALUE("12:00:00")
|
||||
datetime.timedelta(0, 17280)
|
||||
>>> VALUE("01/01/2012")
|
||||
datetime.datetime(2012, 1, 1, 0, 0)
|
||||
>>> VALUE("")
|
||||
0
|
||||
>>> VALUE(0)
|
||||
0
|
||||
>>> VALUE("826")
|
||||
826
|
||||
>>> VALUE("-826.123123123")
|
||||
-826.123123123
|
||||
>>> VALUE(float('nan'))
|
||||
nan
|
||||
>>> VALUE("Invalid")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: text cannot be parsed to a number
|
||||
>>> VALUE("13/13/13")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
ValueError: text cannot be parsed to a number
|
||||
"""
|
||||
# This is not particularly robust, but makes an attempt to handle a number of cases: numbers,
|
||||
# including optional comma separators, dates/times, leading dollar-sign.
|
||||
if isinstance(text, (numbers.Number, datetime.date)):
|
||||
return text
|
||||
text = text.strip().lstrip('$')
|
||||
nocommas = text.replace(',', '')
|
||||
if nocommas == "":
|
||||
return 0
|
||||
|
||||
try:
|
||||
return int(nocommas)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return float(nocommas)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return dateutil.parser.parse(text)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
raise ValueError('text cannot be parsed to a number')
|
||||
Reference in New Issue
Block a user