mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
521 lines
14 KiB
Python
521 lines
14 KiB
Python
|
# -*- 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)))
|