Skip to content

Commit 6da561a

Browse files
committed
builtins: numbers.range and range_step
Implement numbers.range and numbers.range_step builtins maintaining compatibility with Go version. Resolves #6 Signed-off-by: Dmitry Frenkel <[email protected]>
1 parent e1d3718 commit 6da561a

File tree

3 files changed

+237
-0
lines changed

3 files changed

+237
-0
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import AST
2+
import Foundation
3+
4+
extension BuiltinFuncs {
5+
static func numbersRange(ctx: BuiltinContext, args: [AST.RegoValue]) async throws -> AST.RegoValue {
6+
guard args.count == 2 else {
7+
throw BuiltinError.argumentCountMismatch(got: args.count, want: 2)
8+
}
9+
10+
return try generateSequence(args: args, withStep: false)
11+
}
12+
13+
static func numbersRangeStep(ctx: BuiltinContext, args: [AST.RegoValue]) async throws -> AST.RegoValue {
14+
guard args.count == 3 else {
15+
throw BuiltinError.argumentCountMismatch(got: args.count, want: 2)
16+
}
17+
18+
return try generateSequence(args: args, withStep: true)
19+
}
20+
21+
private static func generateSequence(args: [AST.RegoValue], withStep: Bool) throws -> RegoValue {
22+
guard case .number(_) = args[0] else {
23+
throw BuiltinError.argumentTypeMismatch(arg: "a", got: args[0].typeName, want: "number")
24+
}
25+
26+
guard case .number(_) = args[1] else {
27+
throw BuiltinError.argumentTypeMismatch(arg: "b", got: args[1].typeName, want: "number")
28+
}
29+
30+
// NOTE that we are okay with this argument being a float with integer value
31+
// numbers.range_step(1.0, 3.0, 1.0) works just fine
32+
guard let intA = args[0].integerValue else {
33+
throw BuiltinError.evalError(msg: "operand 1 must be integer number but got floating-point number")
34+
}
35+
36+
// NOTE that we are okay with this argument being a float with integer value
37+
// numbers.range_step(1.0, 3.0, 1.0) works just fine
38+
guard let intB = args[1].integerValue else {
39+
throw BuiltinError.evalError(msg: "operand 2 must be integer number but got floating-point number")
40+
}
41+
42+
var step: Int64 = 1
43+
if withStep {
44+
// NOTE that we are okay with this argument being a float with integer value
45+
// numbers.range_step(1.0, 3.0, 1.0) works just fine
46+
guard let stepValue = args[2].integerValue else {
47+
throw BuiltinError.argumentTypeMismatch(arg: "step", got: args[0].typeName, want: "integer")
48+
}
49+
50+
guard stepValue > 0 else {
51+
// Golang error is different: step must be a positive number above zero.
52+
// But positive number is by definition above zero...
53+
// so using a different error message.
54+
// Compliance tests still pass
55+
throw BuiltinError.evalError(msg: "step must be a positive integer")
56+
}
57+
58+
step = stepValue
59+
}
60+
if intB > intA {
61+
return .array(
62+
stride(from: intA, to: intB + 1, by: Int64.Stride(step))
63+
.map({ NSNumber(value: $0).toNumberRegoValue(asInt: true) }))
64+
65+
}
66+
67+
return .array(
68+
stride(from: intA, to: intB - 1, by: -Int64.Stride(step))
69+
.map({ NSNumber(value: $0).toNumberRegoValue(asInt: true) }))
70+
71+
}
72+
73+
}

Sources/Rego/Builtins/Registry.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ public struct BuiltinRegistry {
9494
"hex.encode": BuiltinFuncs.hexEncode,
9595
"hex.decode": BuiltinFuncs.hexDecode,
9696

97+
// Numbers
98+
"numbers.range": BuiltinFuncs.numbersRange,
99+
"numbers.range_step": BuiltinFuncs.numbersRangeStep,
100+
97101
// Objects
98102
"object.get": BuiltinFuncs.objectGet,
99103
"object.keys": BuiltinFuncs.objectKeys,
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import AST
2+
import Foundation
3+
import Testing
4+
5+
@testable import Rego
6+
7+
extension BuiltinTests {
8+
@Suite("BuiltinTests - Numbers", .tags(.builtins))
9+
struct NumbersTests {}
10+
}
11+
12+
extension BuiltinTests.NumbersTests {
13+
static let rangeTests: [BuiltinTests.TestCase] = [
14+
BuiltinTests.TestCase(
15+
description: "when a = b",
16+
name: "numbers.range",
17+
args: [1, 1],
18+
expected: .success([1])
19+
),
20+
BuiltinTests.TestCase(
21+
description: "when a < b",
22+
name: "numbers.range",
23+
args: [1, 3],
24+
expected: .success([1, 2, 3])
25+
),
26+
BuiltinTests.TestCase(
27+
description: "when a > b",
28+
name: "numbers.range",
29+
args: [3, 1],
30+
expected: .success([3, 2, 1])
31+
),
32+
BuiltinTests.TestCase(
33+
description: "with a is not an integer",
34+
name: "numbers.range",
35+
args: [1.2, 3],
36+
expected: .failure(
37+
BuiltinError.evalError(
38+
msg: "operand 1 must be integer number but got floating-point number"))
39+
),
40+
BuiltinTests.TestCase(
41+
description: "when b is not an integer",
42+
name: "numbers.range",
43+
args: [1, 3.14],
44+
expected: .failure(
45+
BuiltinError.evalError(
46+
msg: "operand 2 must be integer number but got floating-point number"))
47+
),
48+
BuiltinTests.TestCase(
49+
description: "when a and b are floats with integer value",
50+
name: "numbers.range",
51+
args: [1.0000, 3.0000],
52+
expected: .success([1, 2, 3])
53+
),
54+
]
55+
56+
static let rangeStepTests: [BuiltinTests.TestCase] = [
57+
BuiltinTests.TestCase(
58+
description: "when a = b",
59+
name: "numbers.range_step",
60+
args: [1, 1, 5],
61+
expected: .success([1])
62+
),
63+
BuiltinTests.TestCase(
64+
description: "when a < b",
65+
name: "numbers.range_step",
66+
args: [1, 10, 3],
67+
expected: .success([1, 4, 7, 10])
68+
),
69+
BuiltinTests.TestCase(
70+
description: "when a < b with large step",
71+
name: "numbers.range_step",
72+
args: [1, 10, 200],
73+
expected: .success([1])
74+
),
75+
BuiltinTests.TestCase(
76+
description: "when a > b",
77+
name: "numbers.range_step",
78+
args: [4, -4, 2],
79+
expected: .success([4, 2, 0, -2, -4])
80+
),
81+
BuiltinTests.TestCase(
82+
description: "when a > b with large step",
83+
name: "numbers.range_step",
84+
args: [2, 0, 100],
85+
expected: .success([2])
86+
),
87+
BuiltinTests.TestCase(
88+
description: "with a is not an integer",
89+
name: "numbers.range_step",
90+
args: [1.2, 3, 1],
91+
expected: .failure(
92+
BuiltinError.evalError(
93+
msg: "operand 1 must be integer number but got floating-point number"))
94+
),
95+
BuiltinTests.TestCase(
96+
description: "when b is not an integer",
97+
name: "numbers.range_step",
98+
args: [1, 3.14, 1],
99+
expected: .failure(
100+
BuiltinError.evalError(
101+
msg: "operand 2 must be integer number but got floating-point number"))
102+
),
103+
BuiltinTests.TestCase(
104+
description: "when step < 0",
105+
name: "numbers.range_step",
106+
args: [3, 1, -1],
107+
expected: .failure(
108+
BuiltinError.evalError(
109+
msg: "step must be a positive integer"))
110+
),
111+
BuiltinTests.TestCase(
112+
description: "when step is not an integer",
113+
name: "numbers.range_step",
114+
args: [1, 3, 1.5],
115+
expected: .failure(
116+
BuiltinError.evalError(
117+
msg: "step must be a positive integer"))
118+
),
119+
BuiltinTests.TestCase(
120+
description: "when a, b and step are floats with integer value",
121+
name: "numbers.range_step",
122+
args: [1.0000, 10.000, 3.00],
123+
expected: .success([1, 4, 7, 10])
124+
),
125+
]
126+
127+
static var allTests: [BuiltinTests.TestCase] {
128+
[
129+
BuiltinTests.generateFailureTests(
130+
builtinName: "numbers.range", sampleArgs: [1, 1], argIndex: 0, argName: "a",
131+
allowedArgTypes: ["number"],
132+
generateNumberOfArgsTest: true),
133+
BuiltinTests.generateFailureTests(
134+
builtinName: "numbers.range", sampleArgs: [1, 1], argIndex: 1, argName: "b",
135+
allowedArgTypes: ["number"],
136+
generateNumberOfArgsTest: false),
137+
rangeTests,
138+
139+
BuiltinTests.generateFailureTests(
140+
builtinName: "numbers.range_step", sampleArgs: [1, 1, 1], argIndex: 0, argName: "a",
141+
allowedArgTypes: ["number"],
142+
generateNumberOfArgsTest: true),
143+
BuiltinTests.generateFailureTests(
144+
builtinName: "numbers.range_step", sampleArgs: [1, 1, 1], argIndex: 1, argName: "b",
145+
allowedArgTypes: ["number"],
146+
generateNumberOfArgsTest: false),
147+
BuiltinTests.generateFailureTests(
148+
builtinName: "numbers.range_step", sampleArgs: [1, 1, 1], argIndex: 2, argName: "step",
149+
allowedArgTypes: ["number"],
150+
generateNumberOfArgsTest: false),
151+
rangeStepTests,
152+
].flatMap { $0 }
153+
}
154+
155+
@Test(arguments: allTests)
156+
func testBuiltins(tc: BuiltinTests.TestCase) async throws {
157+
try await BuiltinTests.testBuiltin(tc: tc)
158+
}
159+
160+
}

0 commit comments

Comments
 (0)