Skip to content

Commit 8c0d3bc

Browse files
authored
Add more advanced formulas (baserow#4297)
* Add replace() * Add length() * Add contains() * Add reverse() * Add join() * Add split() * Add is_empty() * Add strip() * Add sum() * Add avg() * Add at() * Add changelog * Small fixes * Add tests for ensure_array() * Improve error handling for RuntimeLength * RuntimeContains should return None/null for the default case * Improve array/object/int check for RuntimeIsEmpty * Fix logic to only work on strings for RuntimeStrip * Lint fix * RuntimeSum backend validation shouldn't allow non-numbers. * RuntimeAvg backend validation shouldn't allow non-numbers. * Handle ValueError in literal array string * Lint fix backend * Fix frontend tests * Add more test cases * Refactor backend tests * Remove allowLiteralArray/allow_literal_array * Add new RuntimeToArray function and refactor existing functions that use Any type to avoid using ensurer. Update examples with usage of to_array. * Refactor frontend tests * Add RuntimeToArray in the backend + tests * Refactor backend functions/tests to avoid using ensurer * Improve frontend validation and tests * Fix examples * simplify * Remove local debugging code * Allow > to compare any type * Allow < to compare any type * Allow >= to compare any type * Allow <= to compare any type * lint fix * is_empty should raise an error for unsupported types. * Change arg type to text for strip * split() should return a list with the same arg if the string has no spaces and no delimited is provided. * Update avg() helper to allow a strict param * Fix toArray description * is_empty() should return false by default, and handle supported args correctly. * Refactor at() to bubble up error * Let join() bubble up errors * Allow reverse() to bubble up errors * Allow contains() to bubble up errors * Allow length() to bubble up errors * Update examples for comparison operators to include date examples
1 parent c1088ce commit 8c0d3bc

File tree

13 files changed

+2109
-17
lines changed

13 files changed

+2109
-17
lines changed

backend/src/baserow/core/apps.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,11 @@ def ready(self):
3838
from baserow.core.formula.runtime_formula_types import (
3939
RuntimeAdd,
4040
RuntimeAnd,
41+
RuntimeAt,
42+
RuntimeAvg,
4143
RuntimeCapitalize,
4244
RuntimeConcat,
45+
RuntimeContains,
4346
RuntimeDateTimeFormat,
4447
RuntimeDay,
4548
RuntimeDivide,
@@ -51,8 +54,11 @@ def ready(self):
5154
RuntimeGreaterThanOrEqual,
5255
RuntimeHour,
5356
RuntimeIf,
57+
RuntimeIsEmpty,
5458
RuntimeIsEven,
5559
RuntimeIsOdd,
60+
RuntimeJoin,
61+
RuntimeLength,
5662
RuntimeLessThan,
5763
RuntimeLessThanOrEqual,
5864
RuntimeLower,
@@ -66,8 +72,14 @@ def ready(self):
6672
RuntimeRandomBool,
6773
RuntimeRandomFloat,
6874
RuntimeRandomInt,
75+
RuntimeReplace,
76+
RuntimeReverse,
6977
RuntimeRound,
7078
RuntimeSecond,
79+
RuntimeSplit,
80+
RuntimeStrip,
81+
RuntimeSum,
82+
RuntimeToArray,
7183
RuntimeToday,
7284
RuntimeUpper,
7385
RuntimeYear,
@@ -108,6 +120,18 @@ def ready(self):
108120
formula_runtime_function_registry.register(RuntimeIf())
109121
formula_runtime_function_registry.register(RuntimeAnd())
110122
formula_runtime_function_registry.register(RuntimeOr())
123+
formula_runtime_function_registry.register(RuntimeReplace())
124+
formula_runtime_function_registry.register(RuntimeLength())
125+
formula_runtime_function_registry.register(RuntimeContains())
126+
formula_runtime_function_registry.register(RuntimeReverse())
127+
formula_runtime_function_registry.register(RuntimeJoin())
128+
formula_runtime_function_registry.register(RuntimeSplit())
129+
formula_runtime_function_registry.register(RuntimeIsEmpty())
130+
formula_runtime_function_registry.register(RuntimeStrip())
131+
formula_runtime_function_registry.register(RuntimeSum())
132+
formula_runtime_function_registry.register(RuntimeAvg())
133+
formula_runtime_function_registry.register(RuntimeAt())
134+
formula_runtime_function_registry.register(RuntimeToArray())
111135

112136
from baserow.core.permission_manager import (
113137
AllowIfTemplatePermissionManagerType,

backend/src/baserow/core/formula/argument_types.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytz
66

77
from baserow.core.formula.validator import (
8+
ensure_array,
89
ensure_boolean,
910
ensure_datetime,
1011
ensure_numeric,
@@ -112,3 +113,25 @@ def test(self, value):
112113

113114
def parse(self, value):
114115
return value
116+
117+
118+
class ArrayOfNumbersBaserowRuntimeFormulaArgumentType(
119+
BaserowRuntimeFormulaArgumentType
120+
):
121+
def test(self, value):
122+
try:
123+
value = ensure_array(value)
124+
except ValidationError:
125+
return False
126+
127+
for item in value:
128+
try:
129+
ensure_numeric(item)
130+
except ValidationError:
131+
return False
132+
133+
return True
134+
135+
def parse(self, value):
136+
value = ensure_array(value)
137+
return [ensure_numeric(item) for item in value]

backend/src/baserow/core/formula/parser/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,7 @@ def __init__(self, operatorText):
5757

5858
class BaserowFormulaSyntaxError(BaserowFormulaException):
5959
pass
60+
61+
62+
class BaserowFormulaExecuteError(BaserowFormulaException):
63+
pass

backend/src/baserow/core/formula/runtime_formula_types.py

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from baserow.core.formula.argument_types import (
99
AnyBaserowRuntimeFormulaArgumentType,
10+
ArrayOfNumbersBaserowRuntimeFormulaArgumentType,
1011
BooleanBaserowRuntimeFormulaArgumentType,
1112
DateTimeBaserowRuntimeFormulaArgumentType,
1213
DictBaserowRuntimeFormulaArgumentType,
@@ -17,7 +18,7 @@
1718
from baserow.core.formula.registries import RuntimeFormulaFunction
1819
from baserow.core.formula.types import FormulaArg, FormulaArgs, FormulaContext
1920
from baserow.core.formula.utils.date import convert_date_format_moment_to_python
20-
from baserow.core.formula.validator import ensure_string
21+
from baserow.core.formula.validator import ensure_array, ensure_string
2122

2223

2324
class RuntimeConcat(RuntimeFormulaFunction):
@@ -418,3 +419,162 @@ class RuntimeOr(RuntimeFormulaFunction):
418419

419420
def execute(self, context: FormulaContext, args: FormulaArgs):
420421
return args[0] or args[1]
422+
423+
424+
class RuntimeReplace(RuntimeFormulaFunction):
425+
type = "replace"
426+
427+
args = [
428+
TextBaserowRuntimeFormulaArgumentType(),
429+
TextBaserowRuntimeFormulaArgumentType(),
430+
TextBaserowRuntimeFormulaArgumentType(),
431+
]
432+
433+
def execute(self, context: FormulaContext, args: FormulaArgs):
434+
return args[0].replace(args[1], args[2])
435+
436+
437+
class RuntimeLength(RuntimeFormulaFunction):
438+
type = "length"
439+
440+
args = [
441+
AnyBaserowRuntimeFormulaArgumentType(),
442+
]
443+
444+
def execute(self, context: FormulaContext, args: FormulaArgs):
445+
return len(args[0])
446+
447+
448+
class RuntimeContains(RuntimeFormulaFunction):
449+
type = "contains"
450+
451+
args = [
452+
AnyBaserowRuntimeFormulaArgumentType(),
453+
AnyBaserowRuntimeFormulaArgumentType(),
454+
]
455+
456+
def execute(self, context: FormulaContext, args: FormulaArgs):
457+
return args[1] in args[0]
458+
459+
460+
class RuntimeReverse(RuntimeFormulaFunction):
461+
type = "reverse"
462+
463+
args = [
464+
AnyBaserowRuntimeFormulaArgumentType(),
465+
]
466+
467+
def execute(self, context: FormulaContext, args: FormulaArgs):
468+
value = args[0]
469+
470+
if isinstance(value, list):
471+
return list(reversed(value))
472+
473+
if isinstance(value, str):
474+
return "".join(list(reversed(value)))
475+
476+
raise TypeError(f"Cannot reverse {value}")
477+
478+
479+
class RuntimeJoin(RuntimeFormulaFunction):
480+
type = "join"
481+
482+
args = [
483+
AnyBaserowRuntimeFormulaArgumentType(),
484+
TextBaserowRuntimeFormulaArgumentType(optional=True),
485+
]
486+
487+
def execute(self, context: FormulaContext, args: FormulaArgs):
488+
value = args[0]
489+
separator = args[1] if len(args) == 2 else ","
490+
return separator.join(value)
491+
492+
493+
class RuntimeSplit(RuntimeFormulaFunction):
494+
type = "split"
495+
496+
args = [
497+
TextBaserowRuntimeFormulaArgumentType(),
498+
TextBaserowRuntimeFormulaArgumentType(optional=True),
499+
]
500+
501+
def execute(self, context: FormulaContext, args: FormulaArgs):
502+
separator = args[1] if len(args) == 2 else None
503+
return args[0].split(separator)
504+
505+
506+
class RuntimeIsEmpty(RuntimeFormulaFunction):
507+
type = "is_empty"
508+
509+
args = [
510+
AnyBaserowRuntimeFormulaArgumentType(),
511+
]
512+
513+
def execute(self, context: FormulaContext, args: FormulaArgs):
514+
value = args[0]
515+
516+
if value is None:
517+
return True
518+
519+
if isinstance(value, (list, str, dict)):
520+
if isinstance(value, str):
521+
value = value.strip()
522+
return len(value) == 0
523+
524+
return False
525+
526+
527+
class RuntimeStrip(RuntimeFormulaFunction):
528+
type = "strip"
529+
530+
args = [
531+
TextBaserowRuntimeFormulaArgumentType(),
532+
]
533+
534+
def execute(self, context: FormulaContext, args: FormulaArgs):
535+
return args[0].strip()
536+
537+
538+
class RuntimeSum(RuntimeFormulaFunction):
539+
type = "sum"
540+
541+
args = [
542+
ArrayOfNumbersBaserowRuntimeFormulaArgumentType(),
543+
]
544+
545+
def execute(self, context: FormulaContext, args: FormulaArgs):
546+
return sum(args[0])
547+
548+
549+
class RuntimeAvg(RuntimeFormulaFunction):
550+
type = "avg"
551+
552+
args = [
553+
ArrayOfNumbersBaserowRuntimeFormulaArgumentType(),
554+
]
555+
556+
def execute(self, context: FormulaContext, args: FormulaArgs):
557+
return sum(args[0]) / len(args[0])
558+
559+
560+
class RuntimeAt(RuntimeFormulaFunction):
561+
type = "at"
562+
563+
args = [
564+
AnyBaserowRuntimeFormulaArgumentType(),
565+
NumberBaserowRuntimeFormulaArgumentType(cast_to_int=True),
566+
]
567+
568+
def execute(self, context: FormulaContext, args: FormulaArgs):
569+
value = args[0]
570+
index = args[1]
571+
return value[index]
572+
573+
574+
class RuntimeToArray(RuntimeFormulaFunction):
575+
type = "to_array"
576+
577+
args = [TextBaserowRuntimeFormulaArgumentType()]
578+
579+
def execute(self, context: FormulaContext, args: FormulaArgs):
580+
return ensure_array(args[0])

0 commit comments

Comments
 (0)