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..3ce3d331 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 = assert_int(exec(arguments[0], stack)) + elif len(arguments) == 2: + start = assert_int(exec(arguments[0], stack)) + stop = assert_int(exec(arguments[1], stack)) + elif len(arguments) == 3: + start = assert_int(exec(arguments[0], stack)) + stop = assert_int(exec(arguments[1], stack)) + step = assert_int(exec(arguments[2], stack)) + 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..1ca8d5d2 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): + 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": [