mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(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:
		
							parent
							
								
									2fbd3f1706
								
							
						
					
					
						commit
						003029bf8a
					
				
							
								
								
									
										87
									
								
								sandbox/grist/autocomplete_context.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								sandbox/grist/autocomplete_context.py
									
									
									
									
									
										Normal 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)
 | 
			
		||||
@ -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
 | 
			
		||||
dependency tracking.
 | 
			
		||||
@ -5,16 +6,17 @@ dependency tracking.
 | 
			
		||||
import contextlib
 | 
			
		||||
import itertools
 | 
			
		||||
import re
 | 
			
		||||
import rlcompleter
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
import traceback
 | 
			
		||||
from collections import namedtuple, OrderedDict, Hashable
 | 
			
		||||
from sortedcontainers import SortedSet
 | 
			
		||||
 | 
			
		||||
import time
 | 
			
		||||
import rlcompleter
 | 
			
		||||
import acl
 | 
			
		||||
import actions
 | 
			
		||||
import action_obj
 | 
			
		||||
from autocomplete_context import AutocompleteContext
 | 
			
		||||
from codebuilder import DOLLAR_REGEX
 | 
			
		||||
import depend
 | 
			
		||||
import docactions
 | 
			
		||||
@ -232,6 +234,9 @@ class Engine(object):
 | 
			
		||||
    # current cell.
 | 
			
		||||
    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):
 | 
			
		||||
    """
 | 
			
		||||
    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.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
 | 
			
		||||
    # tableIds and colIds.
 | 
			
		||||
 | 
			
		||||
@ -1205,8 +1213,9 @@ class Engine(object):
 | 
			
		||||
    if txt == '$':
 | 
			
		||||
      tweaked_txt = 'rec.'
 | 
			
		||||
    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)
 | 
			
		||||
    results = []
 | 
			
		||||
@ -1219,11 +1228,12 @@ class Engine(object):
 | 
			
		||||
        break
 | 
			
		||||
      if skipped_completions.search(result):
 | 
			
		||||
        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 tweaked_txt != txt:
 | 
			
		||||
      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
 | 
			
		||||
 | 
			
		||||
  def _get_undo_checkpoint(self):
 | 
			
		||||
 | 
			
		||||
@ -9,9 +9,11 @@ import re
 | 
			
		||||
 | 
			
		||||
import column
 | 
			
		||||
from functions import date      # pylint: disable=import-error
 | 
			
		||||
from functions.unimplemented import unimplemented
 | 
			
		||||
from usertypes import AltText   # pylint: disable=import-error
 | 
			
		||||
from records import Record, RecordSet
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def ISBLANK(value):
 | 
			
		||||
  """
 | 
			
		||||
  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')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def TYPE(value):
 | 
			
		||||
  """
 | 
			
		||||
  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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def CELL(info_type, reference):
 | 
			
		||||
  """
 | 
			
		||||
  Returns the requested information about the specified cell. This is not implemented in Grist
 | 
			
		||||
 | 
			
		||||
@ -1,57 +1,72 @@
 | 
			
		||||
# pylint: disable=redefined-builtin, line-too-long
 | 
			
		||||
from unimplemented import unimplemented
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def ADDRESS(row, column, absolute_relative_mode, use_a1_notation, sheet):
 | 
			
		||||
  """Returns a cell reference as a string."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def CHOOSE(index, choice1, choice2):
 | 
			
		||||
  """Returns an element from a list of choices based on index."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def COLUMN(cell_reference=None):
 | 
			
		||||
  """Returns the column number of a specified cell, with `A=1`."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def COLUMNS(range):
 | 
			
		||||
  """Returns the number of columns in a specified array or range."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def HYPERLINK(url, link_label):
 | 
			
		||||
  """Creates a hyperlink inside a cell."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def INDEX(reference, row, column):
 | 
			
		||||
  """Returns the content of a cell, specified by row and column offset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def INDIRECT(cell_reference_as_string):
 | 
			
		||||
  """Returns a cell reference specified by a string."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def MATCH(search_key, range, search_type):
 | 
			
		||||
  """Returns the relative position of an item in a range that matches a specified value."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def ROW(cell_reference):
 | 
			
		||||
  """Returns the row number of a specified cell."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def ROWS(range):
 | 
			
		||||
  """Returns the number of rows in a specified array or range."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@ -7,6 +7,7 @@ import operator
 | 
			
		||||
import random
 | 
			
		||||
 | 
			
		||||
from functions.info import ISNUMBER, ISLOGICAL
 | 
			
		||||
from functions.unimplemented import unimplemented
 | 
			
		||||
import roman
 | 
			
		||||
 | 
			
		||||
# 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)
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def SUBTOTAL(function_code, range1, range2):
 | 
			
		||||
  """
 | 
			
		||||
  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))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def SUMIF(records, criterion, sum_range):
 | 
			
		||||
  """
 | 
			
		||||
  Returns a conditional sum across a range.
 | 
			
		||||
  """
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def SUMIFS(sum_range, criteria_range1, criterion1, *args):
 | 
			
		||||
  """
 | 
			
		||||
  Returns the sum of a range depending on multiple criteria.
 | 
			
		||||
@ -782,6 +786,7 @@ def SUMPRODUCT(array1, *more_arrays):
 | 
			
		||||
  """
 | 
			
		||||
  return sum(reduce(operator.mul, values) for values in itertools.izip(array1, *more_arrays))
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def SUMSQ(value1, value2):
 | 
			
		||||
  """
 | 
			
		||||
  Returns the sum of the squares of a series of numbers and/or cells.
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@
 | 
			
		||||
from math import _chain, _chain_numeric, _chain_numeric_a
 | 
			
		||||
from info import ISNUMBER, ISLOGICAL
 | 
			
		||||
from date import DATE       # pylint: disable=unused-import
 | 
			
		||||
 | 
			
		||||
from unimplemented import unimplemented
 | 
			
		||||
 | 
			
		||||
def _average(iterable):
 | 
			
		||||
  total, count = 0.0, 0
 | 
			
		||||
@ -24,6 +24,7 @@ def _default_if_empty(iterable, default):
 | 
			
		||||
    yield default
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def AVEDEV(value1, value2):
 | 
			
		||||
  """Calculates the average of the magnitudes of deviations of data from a dataset's mean."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
@ -95,14 +96,17 @@ def AVERAGE_WEIGHTED(pairs):
 | 
			
		||||
  return sum_value / sum_weight
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def AVERAGEIF(criteria_range, criterion, average_range=None):
 | 
			
		||||
  """Returns the average of a range depending on criteria."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def AVERAGEIFS(average_range, criteria_range1, criterion1, *args):
 | 
			
		||||
  """Returns the average of a range depending on multiple criteria."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def BINOMDIST(num_successes, num_trials, prob_success, cumulative):
 | 
			
		||||
  """
 | 
			
		||||
  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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def CONFIDENCE(alpha, standard_deviation, pop_size):
 | 
			
		||||
  """Calculates the width of half the confidence interval for a normal distribution."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def CORREL(data_y, data_x):
 | 
			
		||||
  """Calculates r, the Pearson product-moment correlation coefficient of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
@ -156,22 +162,27 @@ def COUNTA(value, *more_values):
 | 
			
		||||
  return sum(1 for v in _chain(value, *more_values))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def COVAR(data_y, data_x):
 | 
			
		||||
  """Calculates the covariance of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def DEVSQ(value1, value2):
 | 
			
		||||
  """Calculates the sum of squares of deviations based on a sample."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def EXPONDIST(x, lambda_, cumulative):
 | 
			
		||||
  """Returns the value of the exponential distribution function with a specified lambda at a specified value."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def F_DIST(x, degrees_freedom1, degrees_freedom2, cumulative):
 | 
			
		||||
  """
 | 
			
		||||
  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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def F_DIST_RT(x, degrees_freedom1, degrees_freedom2):
 | 
			
		||||
  """
 | 
			
		||||
  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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def FDIST(x, degrees_freedom1, degrees_freedom2):
 | 
			
		||||
  """
 | 
			
		||||
  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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def FISHER(value):
 | 
			
		||||
  """Returns the Fisher transformation of a specified value."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def FISHERINV(value):
 | 
			
		||||
  """Returns the inverse Fisher transformation of a specified value."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def GEOMEAN(value1, value2):
 | 
			
		||||
  """Calculates the geometric mean of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def HARMEAN(value1, value2):
 | 
			
		||||
  """Calculates the harmonic mean of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def KURT(value1, value2):
 | 
			
		||||
  """Calculates the kurtosis of a dataset, which describes the shape, and in particular the "peakedness" of that dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def LARGE(data, n):
 | 
			
		||||
  """Returns the nth largest element from a data set, where n is user-defined."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
@ -364,14 +388,17 @@ def MINA(value, *more_values):
 | 
			
		||||
  return min(_default_if_empty(_chain_numeric_a(value, *more_values), 0))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def MODE(value1, value2):
 | 
			
		||||
  """Returns the most commonly occurring value in a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def NORMDIST(x, mean, standard_deviation, cumulative):
 | 
			
		||||
  """
 | 
			
		||||
  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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def NORMSDIST(x):
 | 
			
		||||
  """Returns the value of the standard normal cumulative distribution function for a specified value."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def NORMSINV(x):
 | 
			
		||||
  """Returns the value of the inverse standard normal distribution function for a specified value."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def PEARSON(data_y, data_x):
 | 
			
		||||
  """Calculates r, the Pearson product-moment correlation coefficient of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def PERCENTILE(data, percentile):
 | 
			
		||||
  """Returns the value at a given percentile of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def PERCENTRANK(data, value, significant_digits=None):
 | 
			
		||||
  """Returns the percentage rank (percentile) of a specified value in a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def POISSON(x, mean, cumulative):
 | 
			
		||||
  """
 | 
			
		||||
  Returns the value of the Poisson distribution function (or Poisson cumulative distribution
 | 
			
		||||
@ -422,42 +459,52 @@ def POISSON(x, mean, cumulative):
 | 
			
		||||
  """
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def QUARTILE(data, quartile_number):
 | 
			
		||||
  """Returns a value nearest to a specified quartile of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def RANK(value, data, is_ascending=None):
 | 
			
		||||
  """Returns the rank of a specified value in a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def RSQ(data_y, data_x):
 | 
			
		||||
  """Calculates the square of r, the Pearson product-moment correlation coefficient of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def SKEW(value1, value2):
 | 
			
		||||
  """Calculates the skewness of a dataset, which describes the symmetry of that dataset about the mean."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def SLOPE(data_y, data_x):
 | 
			
		||||
  """Calculates the slope of the line resulting from linear regression of a dataset."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def SMALL(data, n):
 | 
			
		||||
  """Returns the nth smallest element from a data set, where n is user-defined."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def STANDARDIZE(value, mean, standard_deviation):
 | 
			
		||||
  """Calculates the normalized equivalent of a random variable given mean and standard deviation of the distribution."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
@ -559,50 +606,62 @@ def STDEVPA(value, *more_values):
 | 
			
		||||
  """
 | 
			
		||||
  return _stddev(list(_chain_numeric_a(value, *more_values)), 0)
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def T_INV(probability, degrees_freedom):
 | 
			
		||||
  """Calculates the negative inverse of the one-tailed TDIST function."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def T_INV_2T(probability, degrees_freedom):
 | 
			
		||||
  """Calculates the inverse of the two-tailed TDIST function."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def TDIST(x, degrees_freedom, tails):
 | 
			
		||||
  """Calculates the probability for Student's t-distribution with a given input (x)."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def TINV(probability, degrees_freedom):
 | 
			
		||||
  """Calculates the inverse of the two-tailed TDIST function."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
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()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def VAR(value1, value2):
 | 
			
		||||
  """Calculates the variance based on a sample."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def VARA(value1, value2):
 | 
			
		||||
  """Calculates an estimate of variance based on a sample, setting text to the value `0`."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def VARP(value1, value2):
 | 
			
		||||
  """Calculates the variance based on an entire population."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def VARPA(value1, value2):
 | 
			
		||||
  """Calculates the variance based on an entire population, setting text to the value `0`."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def WEIBULL(x, shape, scale, cumulative):
 | 
			
		||||
  """
 | 
			
		||||
  Returns the value of the Weibull distribution function (or Weibull cumulative distribution
 | 
			
		||||
@ -610,6 +669,7 @@ def WEIBULL(x, shape, scale, cumulative):
 | 
			
		||||
  """
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def ZTEST(data, value, standard_deviation):
 | 
			
		||||
  """Returns the two-tailed P-value of a Z-test with standard distribution."""
 | 
			
		||||
  raise NotImplementedError()
 | 
			
		||||
 | 
			
		||||
@ -5,6 +5,7 @@ import dateutil.parser
 | 
			
		||||
import numbers
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
from unimplemented import unimplemented
 | 
			
		||||
from usertypes import AltText   # pylint: disable=import-error
 | 
			
		||||
 | 
			
		||||
def CHAR(table_number):
 | 
			
		||||
@ -499,6 +500,7 @@ def T(value):
 | 
			
		||||
          str(value) if isinstance(value, AltText) else "")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@unimplemented
 | 
			
		||||
def TEXT(number, format_type):
 | 
			
		||||
  """
 | 
			
		||||
  Converts a number into text according to a specified format. It is not yet implemented in
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										11
									
								
								sandbox/grist/functions/unimplemented.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								sandbox/grist/functions/unimplemented.py
									
									
									
									
									
										Normal 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
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
import testsamples
 | 
			
		||||
import testutil
 | 
			
		||||
import test_engine
 | 
			
		||||
 | 
			
		||||
class TestCompletion(test_engine.EngineTestCase):
 | 
			
		||||
@ -24,18 +23,63 @@ class TestCompletion(test_engine.EngineTestCase):
 | 
			
		||||
 | 
			
		||||
  def test_function(self):
 | 
			
		||||
    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):
 | 
			
		||||
    self.assertEqual(self.engine.autocomplete("datetime.tz", "Address"),
 | 
			
		||||
                     ["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):
 | 
			
		||||
    # 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.assertGreaterEqual(set(self.engine.autocomplete("S", "Address")),
 | 
			
		||||
                            {'Schools', 'Students', 'SUM(', 'STDEV('})
 | 
			
		||||
    self.assertGreaterEqual(set(self.engine.autocomplete("S", "Address")), {
 | 
			
		||||
      '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'])
 | 
			
		||||
 | 
			
		||||
  def test_suggest_columns(self):
 | 
			
		||||
@ -56,11 +100,16 @@ class TestCompletion(test_engine.EngineTestCase):
 | 
			
		||||
 | 
			
		||||
  def test_suggest_lookup_methods(self):
 | 
			
		||||
    # Should suggest lookup formulas for tables.
 | 
			
		||||
    self.assertEqual(self.engine.autocomplete("Address.", "Students"),
 | 
			
		||||
        ['Address.all', 'Address.lookupOne(', 'Address.lookupRecords('])
 | 
			
		||||
    self.assertEqual(self.engine.autocomplete("Address.", "Students"), [
 | 
			
		||||
      'Address.all',
 | 
			
		||||
      ('Address.lookupOne', '(colName=<value>, ...)', True),
 | 
			
		||||
      ('Address.lookupRecords', '(colName=<value>, ...)', True),
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
    self.assertEqual(self.engine.autocomplete("Address.lookup", "Students"),
 | 
			
		||||
        ['Address.lookupOne(', 'Address.lookupRecords('])
 | 
			
		||||
    self.assertEqual(self.engine.autocomplete("Address.lookup", "Students"), [
 | 
			
		||||
      ('Address.lookupOne', '(colName=<value>, ...)', True),
 | 
			
		||||
      ('Address.lookupRecords', '(colName=<value>, ...)', True),
 | 
			
		||||
    ])
 | 
			
		||||
 | 
			
		||||
  def test_suggest_column_type_methods(self):
 | 
			
		||||
    # Should treat columns as correct types.
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user