(core) Improve suggestions for formula autocomplete

Summary:
- Make suggestions less case-sensitive (not entirely case-insensitive, but
  allow top-level suggestions to match in all-lowercase)
- Add function signatures to suggestions for Grist functions.
- Excel-like functions that are present but not implemented are no longer
  offered as suggestions.

Test Plan:
Added a test case on python side, and a browser test case for how suggestions
are rendered and inserted.

Reviewers: paulfitz

Reviewed By: paulfitz

Differential Revision: https://phab.getgrist.com/D2608
This commit is contained in:
Dmitry S 2020-09-11 14:18:03 -04:00
parent 2fbd3f1706
commit 003029bf8a
9 changed files with 259 additions and 16 deletions

View File

@ -0,0 +1,87 @@
"""
Helper class for handling formula autocomplete.
It's intended to use with rlcompleter.Completer. It allows finding global names using
lowercase searches, and adds function usage information to some results.
"""
import __builtin__
import inspect
from collections import namedtuple
# funcname is the function name, e.g. "MAX"
# argspec is the signature, e.g. "(arg, *more_args)"
# isgrist is a boolean for whether this function should be in Grist documentation.
Completion = namedtuple('Completion', ['funcname', 'argspec', 'isgrist'])
def is_grist_func(func):
try:
return inspect.getmodule(func).__name__.startswith('functions.')
except Exception, e:
return e
class AutocompleteContext(object):
def __init__(self, usercode_context):
# rlcompleter is case-sensitive. This is hard to work around while maintaining attribute
# lookups. As a middle ground, we only introduce lowercase versions of all global names.
self._context = {
key: value for key, value in usercode_context.iteritems()
# Don't propose unimplemented functions in autocomplete
if not (value and callable(value) and getattr(value, 'unimplemented', None))
}
# Prepare detailed Completion objects for functions where we can supply more info.
# TODO It would be nice to include builtin functions too, but getargspec doesn't work there.
self._functions = {}
for key, value in self._context.iteritems():
if value and callable(value):
argspec = inspect.formatargspec(*inspect.getargspec(value))
self._functions[key] = Completion(key, argspec, is_grist_func(value))
# Add in the important UserTable methods, with custom friendlier descriptions.
self._functions['.lookupOne'] = Completion('.lookupOne', '(colName=<value>, ...)', True)
self._functions['.lookupRecords'] = Completion('.lookupRecords', '(colName=<value>, ...)', True)
# Remember the original name for each lowercase one.
self._lowercase = {}
for key in self._context:
lower = key.lower()
if lower == key:
continue
if lower not in self._context and lower not in __builtin__.__dict__:
self._lowercase[lower] = key
else:
# This is still good enough to find a match for, and translate back to the original.
# It allows rlcompleter to match e.g. 'max' against 'max', 'Max', and 'MAX' (using keys
# 'max', 'max*', and 'max**', respectively).
lower += '*'
if lower in self._lowercase:
lower += '*'
self._lowercase[lower] = key
# Add the lowercase names to the context, and to the detailed completions in _functions.
for lower, key in self._lowercase.iteritems():
self._context[lower] = self._context[key]
if key in self._functions:
self._functions[lower] = self._functions[key]
def get_context(self):
return self._context
def process_result(self, result):
# Callables are returned by rlcompleter with a trailing "(".
if result.endswith('('):
funcname = result[0:-1]
dot = funcname.rfind(".")
key = funcname[dot:] if dot >= 0 else funcname
completion = self._functions.get(key)
# Return the detailed completion if we have it, or the result string otherwise.
if completion:
# For methods (eg ".lookupOne"), use the original result as funcname (eg "Foo.lookupOne").
if dot >= 0:
completion = completion._replace(funcname=funcname)
return tuple(completion)
return result
# Return translation from lowercase if there is one, or the result string otherwise.
return self._lowercase.get(result, result)

View File

@ -1,3 +1,4 @@
# pylint:disable=too-many-lines
""" """
The data engine ties the code generated from the schema with the document data, and with The data engine ties the code generated from the schema with the document data, and with
dependency tracking. dependency tracking.
@ -5,16 +6,17 @@ dependency tracking.
import contextlib import contextlib
import itertools import itertools
import re import re
import rlcompleter
import sys import sys
import time
import traceback import traceback
from collections import namedtuple, OrderedDict, Hashable from collections import namedtuple, OrderedDict, Hashable
from sortedcontainers import SortedSet from sortedcontainers import SortedSet
import time
import rlcompleter
import acl import acl
import actions import actions
import action_obj import action_obj
from autocomplete_context import AutocompleteContext
from codebuilder import DOLLAR_REGEX from codebuilder import DOLLAR_REGEX
import depend import depend
import docactions import docactions
@ -232,6 +234,9 @@ class Engine(object):
# current cell. # current cell.
self._cell_required_error = None self._cell_required_error = None
# Initial empty context for autocompletions; we update it when we generate the usercode module.
self._autocomplete_context = AutocompleteContext({})
def load_empty(self): def load_empty(self):
""" """
Initialize an empty document, e.g. a newly-created one. Initialize an empty document, e.g. a newly-created one.
@ -1016,6 +1021,9 @@ class Engine(object):
self._repl.locals.update(self.gencode.usercode.__dict__) self._repl.locals.update(self.gencode.usercode.__dict__)
self.gencode.usercode.__dict__.update(self._repl.locals) self.gencode.usercode.__dict__.update(self._repl.locals)
# Update the context used for autocompletions.
self._autocomplete_context = AutocompleteContext(self.gencode.usercode.__dict__)
# TODO: Whenever schema changes, we need to adjust the ACL resources to remove or rename # TODO: Whenever schema changes, we need to adjust the ACL resources to remove or rename
# tableIds and colIds. # tableIds and colIds.
@ -1205,8 +1213,9 @@ class Engine(object):
if txt == '$': if txt == '$':
tweaked_txt = 'rec.' tweaked_txt = 'rec.'
table = self.tables[table_id] table = self.tables[table_id]
context = {'rec': table.sample_record}
context.update(self.gencode.usercode.__dict__) context = self._autocomplete_context.get_context()
context['rec'] = table.sample_record
completer = rlcompleter.Completer(context) completer = rlcompleter.Completer(context)
results = [] results = []
@ -1219,11 +1228,12 @@ class Engine(object):
break break
if skipped_completions.search(result): if skipped_completions.search(result):
continue continue
results.append(result) results.append(self._autocomplete_context.process_result(result))
# If we changed the prefix (expanding the $ symbol) we now need to change it back. # If we changed the prefix (expanding the $ symbol) we now need to change it back.
if tweaked_txt != txt: if tweaked_txt != txt:
results = [txt + result[len(tweaked_txt):] for result in results] results = [txt + result[len(tweaked_txt):] for result in results]
results.sort() results.sort(key=lambda r: r[0] if type(r) == tuple else r)
return results return results
def _get_undo_checkpoint(self): def _get_undo_checkpoint(self):

View File

@ -9,9 +9,11 @@ import re
import column import column
from functions import date # pylint: disable=import-error from functions import date # pylint: disable=import-error
from functions.unimplemented import unimplemented
from usertypes import AltText # pylint: disable=import-error from usertypes import AltText # pylint: disable=import-error
from records import Record, RecordSet from records import Record, RecordSet
@unimplemented
def ISBLANK(value): def ISBLANK(value):
""" """
Returns whether a value refers to an empty cell. It isn't implemented in Grist. To check for an Returns whether a value refers to an empty cell. It isn't implemented in Grist. To check for an
@ -486,6 +488,7 @@ def NA():
return float('nan') return float('nan')
@unimplemented
def TYPE(value): def TYPE(value):
""" """
Returns a number associated with the type of data passed into the function. This is not Returns a number associated with the type of data passed into the function. This is not
@ -493,6 +496,7 @@ def TYPE(value):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def CELL(info_type, reference): def CELL(info_type, reference):
""" """
Returns the requested information about the specified cell. This is not implemented in Grist Returns the requested information about the specified cell. This is not implemented in Grist

View File

@ -1,57 +1,72 @@
# pylint: disable=redefined-builtin, line-too-long # pylint: disable=redefined-builtin, line-too-long
from unimplemented import unimplemented
@unimplemented
def ADDRESS(row, column, absolute_relative_mode, use_a1_notation, sheet): def ADDRESS(row, column, absolute_relative_mode, use_a1_notation, sheet):
"""Returns a cell reference as a string.""" """Returns a cell reference as a string."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def CHOOSE(index, choice1, choice2): def CHOOSE(index, choice1, choice2):
"""Returns an element from a list of choices based on index.""" """Returns an element from a list of choices based on index."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def COLUMN(cell_reference=None): def COLUMN(cell_reference=None):
"""Returns the column number of a specified cell, with `A=1`.""" """Returns the column number of a specified cell, with `A=1`."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def COLUMNS(range): def COLUMNS(range):
"""Returns the number of columns in a specified array or range.""" """Returns the number of columns in a specified array or range."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def GETPIVOTDATA(value_name, any_pivot_table_cell, original_column_1, pivot_item_1=None, *args): 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.""" """Extracts an aggregated value from a pivot table that corresponds to the specified row and column headings."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def HLOOKUP(search_key, range, index, is_sorted): 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.""" """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() raise NotImplementedError()
@unimplemented
def HYPERLINK(url, link_label): def HYPERLINK(url, link_label):
"""Creates a hyperlink inside a cell.""" """Creates a hyperlink inside a cell."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def INDEX(reference, row, column): def INDEX(reference, row, column):
"""Returns the content of a cell, specified by row and column offset.""" """Returns the content of a cell, specified by row and column offset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def INDIRECT(cell_reference_as_string): def INDIRECT(cell_reference_as_string):
"""Returns a cell reference specified by a string.""" """Returns a cell reference specified by a string."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def LOOKUP(search_key, search_range_or_search_result_array, result_range=None): 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.""" """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() raise NotImplementedError()
@unimplemented
def MATCH(search_key, range, search_type): def MATCH(search_key, range, search_type):
"""Returns the relative position of an item in a range that matches a specified value.""" """Returns the relative position of an item in a range that matches a specified value."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def OFFSET(cell_reference, offset_rows, offset_columns, height, width): 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.""" """Returns a range reference shifted a specified number of rows and columns from a starting cell reference."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def ROW(cell_reference): def ROW(cell_reference):
"""Returns the row number of a specified cell.""" """Returns the row number of a specified cell."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def ROWS(range): def ROWS(range):
"""Returns the number of rows in a specified array or range.""" """Returns the number of rows in a specified array or range."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -7,6 +7,7 @@ import operator
import random import random
from functions.info import ISNUMBER, ISLOGICAL from functions.info import ISNUMBER, ISLOGICAL
from functions.unimplemented import unimplemented
import roman import roman
# Iterates through elements of iterable arguments, or through individual args when not iterable. # Iterates through elements of iterable arguments, or through individual args when not iterable.
@ -727,6 +728,7 @@ def SQRTPI(value):
""" """
return _math.sqrt(_math.pi * value) return _math.sqrt(_math.pi * value)
@unimplemented
def SUBTOTAL(function_code, range1, range2): def SUBTOTAL(function_code, range1, range2):
""" """
Returns a subtotal for a vertical range of cells using a specified aggregation function. Returns a subtotal for a vertical range of cells using a specified aggregation function.
@ -755,12 +757,14 @@ def SUM(value1, *more_values):
return sum(_chain_numeric_a(value1, *more_values)) return sum(_chain_numeric_a(value1, *more_values))
@unimplemented
def SUMIF(records, criterion, sum_range): def SUMIF(records, criterion, sum_range):
""" """
Returns a conditional sum across a range. Returns a conditional sum across a range.
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def SUMIFS(sum_range, criteria_range1, criterion1, *args): def SUMIFS(sum_range, criteria_range1, criterion1, *args):
""" """
Returns the sum of a range depending on multiple criteria. Returns the sum of a range depending on multiple criteria.
@ -782,6 +786,7 @@ def SUMPRODUCT(array1, *more_arrays):
""" """
return sum(reduce(operator.mul, values) for values in itertools.izip(array1, *more_arrays)) return sum(reduce(operator.mul, values) for values in itertools.izip(array1, *more_arrays))
@unimplemented
def SUMSQ(value1, value2): def SUMSQ(value1, value2):
""" """
Returns the sum of the squares of a series of numbers and/or cells. Returns the sum of the squares of a series of numbers and/or cells.

View File

@ -3,7 +3,7 @@
from math import _chain, _chain_numeric, _chain_numeric_a from math import _chain, _chain_numeric, _chain_numeric_a
from info import ISNUMBER, ISLOGICAL from info import ISNUMBER, ISLOGICAL
from date import DATE # pylint: disable=unused-import from date import DATE # pylint: disable=unused-import
from unimplemented import unimplemented
def _average(iterable): def _average(iterable):
total, count = 0.0, 0 total, count = 0.0, 0
@ -24,6 +24,7 @@ def _default_if_empty(iterable, default):
yield default yield default
@unimplemented
def AVEDEV(value1, value2): def AVEDEV(value1, value2):
"""Calculates the average of the magnitudes of deviations of data from a dataset's mean.""" """Calculates the average of the magnitudes of deviations of data from a dataset's mean."""
raise NotImplementedError() raise NotImplementedError()
@ -95,14 +96,17 @@ def AVERAGE_WEIGHTED(pairs):
return sum_value / sum_weight return sum_value / sum_weight
@unimplemented
def AVERAGEIF(criteria_range, criterion, average_range=None): def AVERAGEIF(criteria_range, criterion, average_range=None):
"""Returns the average of a range depending on criteria.""" """Returns the average of a range depending on criteria."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def AVERAGEIFS(average_range, criteria_range1, criterion1, *args): def AVERAGEIFS(average_range, criteria_range1, criterion1, *args):
"""Returns the average of a range depending on multiple criteria.""" """Returns the average of a range depending on multiple criteria."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def BINOMDIST(num_successes, num_trials, prob_success, cumulative): def BINOMDIST(num_successes, num_trials, prob_success, cumulative):
""" """
Calculates the probability of drawing a certain number of successes (or a maximum number of Calculates the probability of drawing a certain number of successes (or a maximum number of
@ -111,10 +115,12 @@ def BINOMDIST(num_successes, num_trials, prob_success, cumulative):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def CONFIDENCE(alpha, standard_deviation, pop_size): def CONFIDENCE(alpha, standard_deviation, pop_size):
"""Calculates the width of half the confidence interval for a normal distribution.""" """Calculates the width of half the confidence interval for a normal distribution."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def CORREL(data_y, data_x): def CORREL(data_y, data_x):
"""Calculates r, the Pearson product-moment correlation coefficient of a dataset.""" """Calculates r, the Pearson product-moment correlation coefficient of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@ -156,22 +162,27 @@ def COUNTA(value, *more_values):
return sum(1 for v in _chain(value, *more_values)) return sum(1 for v in _chain(value, *more_values))
@unimplemented
def COVAR(data_y, data_x): def COVAR(data_y, data_x):
"""Calculates the covariance of a dataset.""" """Calculates the covariance of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def CRITBINOM(num_trials, prob_success, target_prob): 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.""" """Calculates the smallest value for which the cumulative binomial distribution is greater than or equal to a specified criteria."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def DEVSQ(value1, value2): def DEVSQ(value1, value2):
"""Calculates the sum of squares of deviations based on a sample.""" """Calculates the sum of squares of deviations based on a sample."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def EXPONDIST(x, lambda_, cumulative): def EXPONDIST(x, lambda_, cumulative):
"""Returns the value of the exponential distribution function with a specified lambda at a specified value.""" """Returns the value of the exponential distribution function with a specified lambda at a specified value."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def F_DIST(x, degrees_freedom1, degrees_freedom2, cumulative): def F_DIST(x, degrees_freedom1, degrees_freedom2, cumulative):
""" """
Calculates the left-tailed F probability distribution (degree of diversity) for two data sets Calculates the left-tailed F probability distribution (degree of diversity) for two data sets
@ -180,6 +191,7 @@ def F_DIST(x, degrees_freedom1, degrees_freedom2, cumulative):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def F_DIST_RT(x, degrees_freedom1, degrees_freedom2): def F_DIST_RT(x, degrees_freedom1, degrees_freedom2):
""" """
Calculates the right-tailed F probability distribution (degree of diversity) for two data sets Calculates the right-tailed F probability distribution (degree of diversity) for two data sets
@ -188,6 +200,7 @@ def F_DIST_RT(x, degrees_freedom1, degrees_freedom2):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def FDIST(x, degrees_freedom1, degrees_freedom2): def FDIST(x, degrees_freedom1, degrees_freedom2):
""" """
Calculates the right-tailed F probability distribution (degree of diversity) for two data sets Calculates the right-tailed F probability distribution (degree of diversity) for two data sets
@ -196,46 +209,57 @@ def FDIST(x, degrees_freedom1, degrees_freedom2):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def FISHER(value): def FISHER(value):
"""Returns the Fisher transformation of a specified value.""" """Returns the Fisher transformation of a specified value."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def FISHERINV(value): def FISHERINV(value):
"""Returns the inverse Fisher transformation of a specified value.""" """Returns the inverse Fisher transformation of a specified value."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def FORECAST(x, data_y, data_x): def FORECAST(x, data_y, data_x):
"""Calculates the expected y-value for a specified x based on a linear regression of a dataset.""" """Calculates the expected y-value for a specified x based on a linear regression of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def GEOMEAN(value1, value2): def GEOMEAN(value1, value2):
"""Calculates the geometric mean of a dataset.""" """Calculates the geometric mean of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def HARMEAN(value1, value2): def HARMEAN(value1, value2):
"""Calculates the harmonic mean of a dataset.""" """Calculates the harmonic mean of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def HYPGEOMDIST(num_successes, num_draws, successes_in_pop, pop_size): 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.""" """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() raise NotImplementedError()
@unimplemented
def INTERCEPT(data_y, data_x): 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).""" """Calculates the y-value at which the line resulting from linear regression of a dataset will intersect the y-axis (x=0)."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def KURT(value1, value2): def KURT(value1, value2):
"""Calculates the kurtosis of a dataset, which describes the shape, and in particular the "peakedness" of that dataset.""" """Calculates the kurtosis of a dataset, which describes the shape, and in particular the "peakedness" of that dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def LARGE(data, n): def LARGE(data, n):
"""Returns the nth largest element from a data set, where n is user-defined.""" """Returns the nth largest element from a data set, where n is user-defined."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def LOGINV(x, mean, standard_deviation): 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.""" """Returns the value of the inverse log-normal cumulative distribution with given mean and standard deviation at a specified value."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def LOGNORMDIST(x, mean, standard_deviation): 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.""" """Returns the value of the log-normal cumulative distribution with given mean and standard deviation at a specified value."""
raise NotImplementedError() raise NotImplementedError()
@ -364,14 +388,17 @@ def MINA(value, *more_values):
return min(_default_if_empty(_chain_numeric_a(value, *more_values), 0)) return min(_default_if_empty(_chain_numeric_a(value, *more_values), 0))
@unimplemented
def MODE(value1, value2): def MODE(value1, value2):
"""Returns the most commonly occurring value in a dataset.""" """Returns the most commonly occurring value in a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def NEGBINOMDIST(num_failures, num_successes, prob_success): 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.""" """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() raise NotImplementedError()
@unimplemented
def NORMDIST(x, mean, standard_deviation, cumulative): def NORMDIST(x, mean, standard_deviation, cumulative):
""" """
Returns the value of the normal distribution function (or normal cumulative distribution Returns the value of the normal distribution function (or normal cumulative distribution
@ -379,42 +406,52 @@ def NORMDIST(x, mean, standard_deviation, cumulative):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def NORMINV(x, mean, standard_deviation): def NORMINV(x, mean, standard_deviation):
"""Returns the value of the inverse normal distribution function for a specified value, mean, and standard deviation.""" """Returns the value of the inverse normal distribution function for a specified value, mean, and standard deviation."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def NORMSDIST(x): def NORMSDIST(x):
"""Returns the value of the standard normal cumulative distribution function for a specified value.""" """Returns the value of the standard normal cumulative distribution function for a specified value."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def NORMSINV(x): def NORMSINV(x):
"""Returns the value of the inverse standard normal distribution function for a specified value.""" """Returns the value of the inverse standard normal distribution function for a specified value."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def PEARSON(data_y, data_x): def PEARSON(data_y, data_x):
"""Calculates r, the Pearson product-moment correlation coefficient of a dataset.""" """Calculates r, the Pearson product-moment correlation coefficient of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def PERCENTILE(data, percentile): def PERCENTILE(data, percentile):
"""Returns the value at a given percentile of a dataset.""" """Returns the value at a given percentile of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def PERCENTRANK(data, value, significant_digits=None): def PERCENTRANK(data, value, significant_digits=None):
"""Returns the percentage rank (percentile) of a specified value in a dataset.""" """Returns the percentage rank (percentile) of a specified value in a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def PERCENTRANK_EXC(data, value, significant_digits=None): 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.""" """Returns the percentage rank (percentile) from 0 to 1 exclusive of a specified value in a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def PERCENTRANK_INC(data, value, significant_digits=None): 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.""" """Returns the percentage rank (percentile) from 0 to 1 inclusive of a specified value in a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def PERMUT(n, k): 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.""" """Returns the number of ways to choose some number of objects from a pool of a given size of objects, considering order."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def POISSON(x, mean, cumulative): def POISSON(x, mean, cumulative):
""" """
Returns the value of the Poisson distribution function (or Poisson cumulative distribution Returns the value of the Poisson distribution function (or Poisson cumulative distribution
@ -422,42 +459,52 @@ def POISSON(x, mean, cumulative):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def PROB(data, probabilities, low_limit, high_limit=None): 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.""" """Given a set of values and corresponding probabilities, calculates the probability that a value chosen at random falls between two limits."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def QUARTILE(data, quartile_number): def QUARTILE(data, quartile_number):
"""Returns a value nearest to a specified quartile of a dataset.""" """Returns a value nearest to a specified quartile of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def RANK(value, data, is_ascending=None): def RANK(value, data, is_ascending=None):
"""Returns the rank of a specified value in a dataset.""" """Returns the rank of a specified value in a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def RANK_AVG(value, data, is_ascending=None): 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.""" """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() raise NotImplementedError()
@unimplemented
def RANK_EQ(value, data, is_ascending=None): 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.""" """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() raise NotImplementedError()
@unimplemented
def RSQ(data_y, data_x): def RSQ(data_y, data_x):
"""Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset.""" """Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def SKEW(value1, value2): def SKEW(value1, value2):
"""Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean.""" """Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def SLOPE(data_y, data_x): def SLOPE(data_y, data_x):
"""Calculates the slope of the line resulting from linear regression of a dataset.""" """Calculates the slope of the line resulting from linear regression of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def SMALL(data, n): def SMALL(data, n):
"""Returns the nth smallest element from a data set, where n is user-defined.""" """Returns the nth smallest element from a data set, where n is user-defined."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def STANDARDIZE(value, mean, standard_deviation): def STANDARDIZE(value, mean, standard_deviation):
"""Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution.""" """Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution."""
raise NotImplementedError() raise NotImplementedError()
@ -559,50 +606,62 @@ def STDEVPA(value, *more_values):
""" """
return _stddev(list(_chain_numeric_a(value, *more_values)), 0) return _stddev(list(_chain_numeric_a(value, *more_values)), 0)
@unimplemented
def STEYX(data_y, data_x): def STEYX(data_y, data_x):
"""Calculates the standard error of the predicted y-value for each x in the regression of a dataset.""" """Calculates the standard error of the predicted y-value for each x in the regression of a dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def T_INV(probability, degrees_freedom): def T_INV(probability, degrees_freedom):
"""Calculates the negative inverse of the one-tailed TDIST function.""" """Calculates the negative inverse of the one-tailed TDIST function."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def T_INV_2T(probability, degrees_freedom): def T_INV_2T(probability, degrees_freedom):
"""Calculates the inverse of the two-tailed TDIST function.""" """Calculates the inverse of the two-tailed TDIST function."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def TDIST(x, degrees_freedom, tails): def TDIST(x, degrees_freedom, tails):
"""Calculates the probability for Student's t-distribution with a given input (x).""" """Calculates the probability for Student's t-distribution with a given input (x)."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def TINV(probability, degrees_freedom): def TINV(probability, degrees_freedom):
"""Calculates the inverse of the two-tailed TDIST function.""" """Calculates the inverse of the two-tailed TDIST function."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def TRIMMEAN(data, exclude_proportion): 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.""" """Calculates the mean of a dataset excluding some proportion of data from the high and low ends of the dataset."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def TTEST(range1, range2, tails, type): 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.""" """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() raise NotImplementedError()
@unimplemented
def VAR(value1, value2): def VAR(value1, value2):
"""Calculates the variance based on a sample.""" """Calculates the variance based on a sample."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def VARA(value1, value2): def VARA(value1, value2):
"""Calculates an estimate of variance based on a sample, setting text to the value `0`.""" """Calculates an estimate of variance based on a sample, setting text to the value `0`."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def VARP(value1, value2): def VARP(value1, value2):
"""Calculates the variance based on an entire population.""" """Calculates the variance based on an entire population."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def VARPA(value1, value2): def VARPA(value1, value2):
"""Calculates the variance based on an entire population, setting text to the value `0`.""" """Calculates the variance based on an entire population, setting text to the value `0`."""
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def WEIBULL(x, shape, scale, cumulative): def WEIBULL(x, shape, scale, cumulative):
""" """
Returns the value of the Weibull distribution function (or Weibull cumulative distribution Returns the value of the Weibull distribution function (or Weibull cumulative distribution
@ -610,6 +669,7 @@ def WEIBULL(x, shape, scale, cumulative):
""" """
raise NotImplementedError() raise NotImplementedError()
@unimplemented
def ZTEST(data, value, standard_deviation): def ZTEST(data, value, standard_deviation):
"""Returns the two-tailed P-value of a Z-test with standard distribution.""" """Returns the two-tailed P-value of a Z-test with standard distribution."""
raise NotImplementedError() raise NotImplementedError()

View File

@ -5,6 +5,7 @@ import dateutil.parser
import numbers import numbers
import re import re
from unimplemented import unimplemented
from usertypes import AltText # pylint: disable=import-error from usertypes import AltText # pylint: disable=import-error
def CHAR(table_number): def CHAR(table_number):
@ -499,6 +500,7 @@ def T(value):
str(value) if isinstance(value, AltText) else "") str(value) if isinstance(value, AltText) else "")
@unimplemented
def TEXT(number, format_type): def TEXT(number, format_type):
""" """
Converts a number into text according to a specified format. It is not yet implemented in Converts a number into text according to a specified format. It is not yet implemented in

View File

@ -0,0 +1,11 @@
"""
Decorator that marks functions as not implemented. It sets func.unimplemented=True.
Usage:
@unimplemented
def func(...):
raise NotImplemented
"""
def unimplemented(func):
func.unimplemented = True
return func

View File

@ -1,5 +1,4 @@
import testsamples import testsamples
import testutil
import test_engine import test_engine
class TestCompletion(test_engine.EngineTestCase): class TestCompletion(test_engine.EngineTestCase):
@ -24,18 +23,63 @@ class TestCompletion(test_engine.EngineTestCase):
def test_function(self): def test_function(self):
self.assertEqual(self.engine.autocomplete("MEDI", "Address"), self.assertEqual(self.engine.autocomplete("MEDI", "Address"),
["MEDIAN("]) [('MEDIAN', '(value, *more_values)', True)])
self.assertEqual(self.engine.autocomplete("ma", "Address"), [
('MAX', '(value, *more_values)', True),
('MAXA', '(value, *more_values)', True),
'map(',
'math',
'max(',
])
def test_member(self): def test_member(self):
self.assertEqual(self.engine.autocomplete("datetime.tz", "Address"), self.assertEqual(self.engine.autocomplete("datetime.tz", "Address"),
["datetime.tzinfo("]) ["datetime.tzinfo("])
def test_case_insensitive(self):
self.assertEqual(self.engine.autocomplete("medi", "Address"),
[('MEDIAN', '(value, *more_values)', True)])
self.assertEqual(self.engine.autocomplete("std", "Address"), [
('STDEV', '(value, *more_values)', True),
('STDEVA', '(value, *more_values)', True),
('STDEVP', '(value, *more_values)', True),
('STDEVPA', '(value, *more_values)', True)
])
self.assertEqual(self.engine.autocomplete("stu", "Address"),
["Students"])
# Add a table name whose lowercase version conflicts with a builtin.
self.apply_user_action(['AddTable', 'Max', []])
self.assertEqual(self.engine.autocomplete("max", "Address"), [
('MAX', '(value, *more_values)', True),
('MAXA', '(value, *more_values)', True),
'Max',
'max(',
])
self.assertEqual(self.engine.autocomplete("MAX", "Address"), [
('MAX', '(value, *more_values)', True),
('MAXA', '(value, *more_values)', True),
])
def test_suggest_globals_and_tables(self): def test_suggest_globals_and_tables(self):
# Should suggest globals and table names. # Should suggest globals and table names.
self.assertEqual(self.engine.autocomplete("ME", "Address"), ['MEDIAN(']) self.assertEqual(self.engine.autocomplete("ME", "Address"),
[('MEDIAN', '(value, *more_values)', True)])
self.assertEqual(self.engine.autocomplete("Ad", "Address"), ['Address']) self.assertEqual(self.engine.autocomplete("Ad", "Address"), ['Address'])
self.assertGreaterEqual(set(self.engine.autocomplete("S", "Address")), self.assertGreaterEqual(set(self.engine.autocomplete("S", "Address")), {
{'Schools', 'Students', 'SUM(', 'STDEV('}) 'Schools',
'Students',
('SUM', '(value1, *more_values)', True),
('STDEV', '(value, *more_values)', True),
})
self.assertGreaterEqual(set(self.engine.autocomplete("s", "Address")), {
'Schools',
'Students',
'sum(',
('SUM', '(value1, *more_values)', True),
('STDEV', '(value, *more_values)', True),
})
self.assertEqual(self.engine.autocomplete("Addr", "Schools"), ['Address']) self.assertEqual(self.engine.autocomplete("Addr", "Schools"), ['Address'])
def test_suggest_columns(self): def test_suggest_columns(self):
@ -56,11 +100,16 @@ class TestCompletion(test_engine.EngineTestCase):
def test_suggest_lookup_methods(self): def test_suggest_lookup_methods(self):
# Should suggest lookup formulas for tables. # Should suggest lookup formulas for tables.
self.assertEqual(self.engine.autocomplete("Address.", "Students"), self.assertEqual(self.engine.autocomplete("Address.", "Students"), [
['Address.all', 'Address.lookupOne(', 'Address.lookupRecords(']) 'Address.all',
('Address.lookupOne', '(colName=<value>, ...)', True),
('Address.lookupRecords', '(colName=<value>, ...)', True),
])
self.assertEqual(self.engine.autocomplete("Address.lookup", "Students"), self.assertEqual(self.engine.autocomplete("Address.lookup", "Students"), [
['Address.lookupOne(', 'Address.lookupRecords(']) ('Address.lookupOne', '(colName=<value>, ...)', True),
('Address.lookupRecords', '(colName=<value>, ...)', True),
])
def test_suggest_column_type_methods(self): def test_suggest_column_type_methods(self):
# Should treat columns as correct types. # Should treat columns as correct types.