diff --git a/Sources/Rego/Builtins/Numbers.swift b/Sources/Rego/Builtins/Numbers.swift new file mode 100644 index 0000000..629df74 --- /dev/null +++ b/Sources/Rego/Builtins/Numbers.swift @@ -0,0 +1,72 @@ +import AST +import Foundation + +extension BuiltinFuncs { + static func numbersRange(ctx: BuiltinContext, args: [AST.RegoValue]) async throws -> AST.RegoValue { + guard args.count == 2 else { + throw BuiltinError.argumentCountMismatch(got: args.count, want: 2) + } + + return try generateSequence(args: args, withStep: false) + } + + static func numbersRangeStep(ctx: BuiltinContext, args: [AST.RegoValue]) async throws -> AST.RegoValue { + guard args.count == 3 else { + throw BuiltinError.argumentCountMismatch(got: args.count, want: 3) + } + + return try generateSequence(args: args, withStep: true) + } + + private static func generateSequence(args: [AST.RegoValue], withStep: Bool) throws -> RegoValue { + guard case .number(_) = args[0] else { + throw BuiltinError.argumentTypeMismatch(arg: "a", got: args[0].typeName, want: "number") + } + + guard case .number(_) = args[1] else { + throw BuiltinError.argumentTypeMismatch(arg: "b", got: args[1].typeName, want: "number") + } + + // NOTE that we are okay with this argument being a float with integer value + // numbers.range_step(1.0, 3.0, 1.0) works just fine + guard let intA = args[0].integerValue else { + throw BuiltinError.evalError(msg: "operand 1 must be integer number but got floating-point number") + } + + // NOTE that we are okay with this argument being a float with integer value + // numbers.range_step(1.0, 3.0, 1.0) works just fine + guard let intB = args[1].integerValue else { + throw BuiltinError.evalError(msg: "operand 2 must be integer number but got floating-point number") + } + + var step: Int64 = 1 + if withStep { + guard case .number(_) = args[2] else { + throw BuiltinError.argumentTypeMismatch(arg: "step", got: args[2].typeName, want: "number") + } + // NOTE that we are okay with this argument being a float with integer value + // numbers.range_step(1.0, 3.0, 1.0) works just fine + guard let stepValue = args[2].integerValue else { + throw BuiltinError.evalError(msg: "step must be integer number but got floating-point number") + } + + guard stepValue > 0 else { + throw BuiltinError.evalError(msg: "step must be a positive integer") + } + + step = stepValue + } + if intB > intA { + return .array( + stride(from: intA, to: intB + 1, by: Int64.Stride(step)) + .map({ NSNumber(value: $0).toNumberRegoValue(asInt: true) })) + + } + + return .array( + stride(from: intA, to: intB - 1, by: -Int64.Stride(step)) + .map({ NSNumber(value: $0).toNumberRegoValue(asInt: true) })) + + } + +} diff --git a/Sources/Rego/Builtins/Registry.swift b/Sources/Rego/Builtins/Registry.swift index 90cea50..dd7d4ae 100644 --- a/Sources/Rego/Builtins/Registry.swift +++ b/Sources/Rego/Builtins/Registry.swift @@ -97,6 +97,10 @@ public struct BuiltinRegistry: Sendable { "hex.encode": BuiltinFuncs.hexEncode, "hex.decode": BuiltinFuncs.hexDecode, + // Numbers + "numbers.range": BuiltinFuncs.numbersRange, + "numbers.range_step": BuiltinFuncs.numbersRangeStep, + // Objects "object.get": BuiltinFuncs.objectGet, "object.keys": BuiltinFuncs.objectKeys, diff --git a/Tests/RegoTests/BuiltinTests/NumbersTests.swift b/Tests/RegoTests/BuiltinTests/NumbersTests.swift new file mode 100644 index 0000000..881dc97 --- /dev/null +++ b/Tests/RegoTests/BuiltinTests/NumbersTests.swift @@ -0,0 +1,160 @@ +import AST +import Foundation +import Testing + +@testable import Rego + +extension BuiltinTests { + @Suite("BuiltinTests - Numbers", .tags(.builtins)) + struct NumbersTests {} +} + +extension BuiltinTests.NumbersTests { + static let rangeTests: [BuiltinTests.TestCase] = [ + BuiltinTests.TestCase( + description: "when a = b", + name: "numbers.range", + args: [1, 1], + expected: .success([1]) + ), + BuiltinTests.TestCase( + description: "when a < b", + name: "numbers.range", + args: [1, 3], + expected: .success([1, 2, 3]) + ), + BuiltinTests.TestCase( + description: "when a > b", + name: "numbers.range", + args: [3, 1], + expected: .success([3, 2, 1]) + ), + BuiltinTests.TestCase( + description: "with a is not an integer", + name: "numbers.range", + args: [1.2, 3], + expected: .failure( + BuiltinError.evalError( + msg: "operand 1 must be integer number but got floating-point number")) + ), + BuiltinTests.TestCase( + description: "when b is not an integer", + name: "numbers.range", + args: [1, 3.14], + expected: .failure( + BuiltinError.evalError( + msg: "operand 2 must be integer number but got floating-point number")) + ), + BuiltinTests.TestCase( + description: "when a and b are floats with integer value", + name: "numbers.range", + args: [1.0000, 3.0000], + expected: .success([1, 2, 3]) + ), + ] + + static let rangeStepTests: [BuiltinTests.TestCase] = [ + BuiltinTests.TestCase( + description: "when a = b", + name: "numbers.range_step", + args: [1, 1, 5], + expected: .success([1]) + ), + BuiltinTests.TestCase( + description: "when a < b", + name: "numbers.range_step", + args: [1, 10, 3], + expected: .success([1, 4, 7, 10]) + ), + BuiltinTests.TestCase( + description: "when a < b with large step", + name: "numbers.range_step", + args: [1, 10, 200], + expected: .success([1]) + ), + BuiltinTests.TestCase( + description: "when a > b", + name: "numbers.range_step", + args: [4, -4, 2], + expected: .success([4, 2, 0, -2, -4]) + ), + BuiltinTests.TestCase( + description: "when a > b with large step", + name: "numbers.range_step", + args: [2, 0, 100], + expected: .success([2]) + ), + BuiltinTests.TestCase( + description: "with a is not an integer", + name: "numbers.range_step", + args: [1.2, 3, 1], + expected: .failure( + BuiltinError.evalError( + msg: "operand 1 must be integer number but got floating-point number")) + ), + BuiltinTests.TestCase( + description: "when b is not an integer", + name: "numbers.range_step", + args: [1, 3.14, 1], + expected: .failure( + BuiltinError.evalError( + msg: "operand 2 must be integer number but got floating-point number")) + ), + BuiltinTests.TestCase( + description: "when step < 0", + name: "numbers.range_step", + args: [3, 1, -1], + expected: .failure( + BuiltinError.evalError( + msg: "step must be a positive integer")) + ), + BuiltinTests.TestCase( + description: "when step is not an integer", + name: "numbers.range_step", + args: [1, 3, 1.5], + expected: .failure( + BuiltinError.evalError( + msg: "step must be integer number but got floating-point number")) + ), + BuiltinTests.TestCase( + description: "when a, b and step are floats with integer value", + name: "numbers.range_step", + args: [1.0000, 10.000, 3.00], + expected: .success([1, 4, 7, 10]) + ), + ] + + static var allTests: [BuiltinTests.TestCase] { + [ + BuiltinTests.generateFailureTests( + builtinName: "numbers.range", sampleArgs: [1, 1], argIndex: 0, argName: "a", + allowedArgTypes: ["number"], + generateNumberOfArgsTest: true), + BuiltinTests.generateFailureTests( + builtinName: "numbers.range", sampleArgs: [1, 1], argIndex: 1, argName: "b", + allowedArgTypes: ["number"], + generateNumberOfArgsTest: false), + rangeTests, + + BuiltinTests.generateFailureTests( + builtinName: "numbers.range_step", sampleArgs: [1, 1, 1], argIndex: 0, argName: "a", + allowedArgTypes: ["number"], + generateNumberOfArgsTest: true), + BuiltinTests.generateFailureTests( + builtinName: "numbers.range_step", sampleArgs: [1, 1, 1], argIndex: 1, argName: "b", + allowedArgTypes: ["number"], + generateNumberOfArgsTest: false), + BuiltinTests.generateFailureTests( + builtinName: "numbers.range_step", sampleArgs: [1, 1, 1], argIndex: 2, argName: "step", + allowedArgTypes: ["number"], + generateNumberOfArgsTest: false), + rangeStepTests, + ].flatMap { $0 } + } + + @Test(arguments: allTests) + func testBuiltins(tc: BuiltinTests.TestCase) async throws { + try await BuiltinTests.testBuiltin(tc: tc) + } + +}