diff --git a/docs/docs/reference/functions.md b/docs/docs/reference/functions.md index dc69d542..a9c19ae7 100644 --- a/docs/docs/reference/functions.md +++ b/docs/docs/reference/functions.md @@ -503,6 +503,32 @@ Result: 6 ``` +### `range` + +| Arity | Parameter 1 Type | Parameter 2 Type (optional) | Parameter 3 Type (optional) | Return Type | +| --- | ---| --- | --- | --- | +| 1 - 3 | `number` | `number` | `number` | `array` | + +Takes arguments `start`, `stop`, and `step`. Returns a list of integers starting at `start`, ending at `stop` (non-inclusive), using steps of size `steps` + +Below is a table describing how the variables `start`, `stop`, and `step` are constructed at different arities. This corresponds to Python's `range()` method. + +| Variable | Arity=1 | Arity=2 | Arity=3 | +| --- | --- | --- | --- | +| `stop` | First Arg | Second Arg | Second Arg | +| `start` | `0` | First Arg | First Arg | +| `step` | `1` | `1` | Third Arg | + +Numbers provided to range must all be integers. Step must be greater than zero. + +#### Example + +`range 5` yields `[0, 1, 2, 3, 4]` + +`range 3 8` yields `[3, 4, 5, 6, 7]` + +`range 3 8 2` yields `[3, 5, 7]` + ### `regex` | Arity | Parameter 1 Type | Parameter 2 Type (optional) | Return Type | diff --git a/js/src/builtins/index.ts b/js/src/builtins/index.ts index a2dabcc6..69eafec3 100644 --- a/js/src/builtins/index.ts +++ b/js/src/builtins/index.ts @@ -27,6 +27,7 @@ import not from "./not"; import notequal from "./notequal"; import or from "./or"; import plus from "./plus"; +import range from "./range"; import reduce from "./reduce"; import regex from "./regex"; import replace from "./replace"; @@ -92,7 +93,7 @@ const divide = numericBinaryOperator((a, b) => { if (b === 0) { throw new RuntimeError("Division by zero"); } - return (a / b); + return a / b; }); export default { @@ -117,6 +118,7 @@ export default { map, mapkeys, mapvalues, + range, reduce, regex, replace, diff --git a/js/src/builtins/range.ts b/js/src/builtins/range.ts new file mode 100644 index 00000000..b69d2f6b --- /dev/null +++ b/js/src/builtins/range.ts @@ -0,0 +1,36 @@ +import { pushRuntimeValueToStack } from "../stackManip"; +import { BuiltinFunction, RuntimeValue } from "../types"; +import { arity, validateType } from "../util"; + +const validateIsInteger = (value: RuntimeValue) => { + const res = validateType("number", value); + if (!Number.isInteger(value)) { + throw new Error("Arguments to range must be integers"); + } + return res; +}; + +const range: BuiltinFunction = arity([1, 2, 3], (args, stack, exec) => { + let start = 0; + let end; + if (args.length > 1) { + start = validateIsInteger(exec(args[0], stack)); + end = validateIsInteger(exec(args[1], stack)); + } else { + end = validateIsInteger(exec(args[0], stack)); + } + let step = 1; + if (args.length > 2) { + step = validateIsInteger(exec(args[2], stack)); + } + const target = []; + if (step === 0) { + throw new Error("Range: Step size cannot be 0"); + } + for (let i = start; i < end; i += step) { + target.push(i); + } + return target; +}); + +export default range; diff --git a/py/mistql/builtins.py b/py/mistql/builtins.py index befc4ebe..5e32ae43 100644 --- a/py/mistql/builtins.py +++ b/py/mistql/builtins.py @@ -6,7 +6,7 @@ from mistql.exceptions import (MistQLRuntimeError, MistQLTypeError, OpenAnIssueIfYouGetThisError) from mistql.expression import BaseExpression, RefExpression -from mistql.runtime_value import RuntimeValue, RuntimeValueType, assert_type +from mistql.runtime_value import RuntimeValue, RuntimeValueType, assert_type, assert_int from mistql.stack import Stack, add_runtime_value_to_stack Args = List[BaseExpression] @@ -484,6 +484,26 @@ def match_operator(arguments: Args, stack: Stack, exec: Exec) -> RuntimeValue: return match(arguments[::-1], stack, exec) +@builtin("range", 1, 3) +def _range(arguments: Args, stack: Stack, exec: Exec) -> RuntimeValue: + start = 0 + step = 1 + if len(arguments) == 1: + stop = int(assert_int(exec(arguments[0], stack)).value) + elif len(arguments) == 2: + start = int(assert_int(exec(arguments[0], stack)).value) + stop = int(assert_int(exec(arguments[1], stack)).value) + elif len(arguments) == 3: + start = int(assert_int(exec(arguments[0], stack)).value) + stop = int(assert_int(exec(arguments[1], stack)).value) + step = int(assert_int(exec(arguments[2], stack)).value) + else: + raise OpenAnIssueIfYouGetThisError( + "Unexpectedly reaching end of function in range call." + ) + return RuntimeValue.of(list(range(start, stop, step))) + + @builtin("replace", 3) def replace(arguments: Args, stack: Stack, exec: Exec) -> RuntimeValue: pattern = exec(arguments[0], stack) diff --git a/py/mistql/runtime_value.py b/py/mistql/runtime_value.py index b53688d1..3d8f89c3 100644 --- a/py/mistql/runtime_value.py +++ b/py/mistql/runtime_value.py @@ -343,3 +343,9 @@ def assert_type( if value.type != expected_type: raise MistQLTypeError(f"Expected {expected_type}, got {value.type}") return value + +def assert_int(value: RuntimeValue): + value = assert_type(value, RuntimeValueType.Number) + if value.value != int(value.value): + raise MistQLTypeError(f"Expected integer, got {value.value}") + return value diff --git a/shared/testdata.json b/shared/testdata.json index de291994..e82d5893 100644 --- a/shared/testdata.json +++ b/shared/testdata.json @@ -3978,6 +3978,86 @@ } ] }, + { + "describe": "#range", + "cases": [ + { + "it": "returns an array of numbers", + "assertions": [ + { + "query": "range 3", + "data": null, + "expected": [0, 1, 2] + } + ] + }, + { + "it": "returns an array of numbers from a start", + "assertions": [ + { + "query": "range 3 6", + "data": null, + "expected": [3, 4, 5] + } + ] + }, + { + "it": "returns an array of numbers from a start and step", + "assertions": [ + { + "query": "range 3 6 2", + "data": null, + "expected": [3, 5] + }, + { + "query": "range 3 7 2", + "data": null, + "expected": [3, 5] + }, + { + "query": "range 3 8 2", + "data": null, + "expected": [3, 5, 7] + } + ] + }, + { + "it": "fails if any of the arguments are not integers", + "assertions": [ + { + "query": "range 3 6 \"a\"", + "data": null, + "throws": true + }, + { + "query": "range 3 6 2.5", + "data": null, + "throws": true + }, + { + "query": "range 3.5 6 2", + "data": null, + "throws": true + }, + { + "query": "range 3 6.5 2", + "data": null, + "throws": true + } + ] + }, + { + "it": "fails if the step is 0", + "assertions": [ + { + "query": "range 3 6 0", + "data": null, + "throws": true + } + ] + } + ] + }, { "describe": "#apply", "cases": [