Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions Expressif.Testing/Functions/Array/GenerateTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Expressif.Functions;
using Expressif.Functions.Array;
using Expressif.Predicates;
using Expressif.Testing.Conformance;
using System.Globalization;

namespace Expressif.Testing.Functions.Array;

[TestFixture]
public class GenerateTest
{
[Conformance]
public void Generate_Valid_While_Next_OptionalResult(object? input, string[] parameters, decimal[] expected)
{
var seed = input is string text && decimal.TryParse(text, NumberStyles.Number, CultureInfo.InvariantCulture, out var numeric)
? numeric
: input;
var result = parameters.Length == 2
? new ExpressionFactory().Create($"generate(while := {parameters[0]}, next := {parameters[1]})").Evaluate(seed)
: new ExpressionFactory().Create($"generate(while := {parameters[0]}, next := {parameters[1]}, result := {parameters[2]})").Evaluate(seed);

Assert.That(result, Is.EqualTo(expected));
}

[Test]
public void Evaluate_UsesCurrentSeedForResultAndNextInThatOrder()
{
var calls = new List<string>();
var generate = new Generate(
() => new DelegatedPredicate(value => { calls.Add($"while:{value}"); return (int)value! <= 2; }),
() => new DelegatedFunction(value => { calls.Add($"next:{value}"); return (int)value! + 1; }),
() => new DelegatedFunction(value => { calls.Add($"result:{value}"); return (int)value! * 10; }));

Assert.That(generate.Evaluate(1), Is.EqualTo(new object?[] { 10, 20 }));
Assert.That(calls, Is.EqualTo(new[] { "while:1", "result:1", "next:1", "while:2", "result:2", "next:2", "while:3" }));
}

[Test]
public void Evaluate_NullSeedCanProduceEmptyArray()
=> Assert.That(
new Generate(() => new DelegatedPredicate(value => value is not null), () => new DelegatedFunction(value => value)).Evaluate(null),
Is.EqualTo(System.Array.Empty<object?>()));

[Test]
public void Instantiate_ResultMayPrecedeNext()
{
var generate = new ExpressionFactory().Create(
"generate(while := less-than(3), result := multiply(10), next := add(1))");

Assert.That(generate.Evaluate(1), Is.EqualTo(new object?[] { 10m, 20m }));
}

private sealed class DelegatedFunction(Func<object?, object?> implementation) : IFunction
{
public object? Evaluate(object? value) => implementation(value);
}

private sealed class DelegatedPredicate(Func<object?, bool> implementation) : IPredicate
{
public bool Evaluate(object? value) => implementation(value);
object? IFunction.Evaluate(object? value) => Evaluate(value);
}
}
46 changes: 46 additions & 0 deletions Expressif/Functions/Array/Generate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using Expressif.Predicates;
using System;
using System.Collections.Generic;

namespace Expressif.Functions.Array;

/// <summary>
/// Generates an array by repeatedly transforming a seed while a condition is satisfied.
/// </summary>
[Function(prefix: "", aliases: ["generate"])]
public class Generate : IFunction<object?, object?[]>
{
public Func<IPredicate> While { get; }
public Func<IFunction> Next { get; }
public Func<IFunction>? Result { get; }

/// <param name="while">Specifies the predicate that determines whether the current seed is included.</param>
/// <param name="next">Specifies the expression that produces the next seed.</param>
public Generate(Func<IPredicate> @while, Func<IFunction> next)
: this(@while, next, null) { }

/// <param name="while">Specifies the predicate that determines whether the current seed is included.</param>
/// <param name="next">Specifies the expression that produces the next seed.</param>
/// <param name="result">Specifies the expression that produces the value appended for the current seed.</param>
public Generate(Func<IPredicate> @while, Func<IFunction> next, Func<IFunction>? result)
=> (While, Next, Result) = (@while, next, result);

public object?[] Evaluate(object? value)
{
var condition = While.Invoke();
var next = Next.Invoke();
var result = Result?.Invoke();
var output = new List<object?>();
var seed = value;

while (condition.Evaluate(seed))
{
output.Add(result is null ? seed : result.Evaluate(seed));
seed = next.Evaluate(seed);
}

return output.ToArray();
}

object? IFunction.Evaluate(object? value) => Evaluate(value);
}
33 changes: 33 additions & 0 deletions Expressif/Functions/FunctionFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,9 @@ private IFunction InstantiateOrWrapAggregation(Bindings.Function function, ICont
if (function.Arguments.Any(x => x.Name is not null) && name == "adjacent")
function = new Bindings.Function(name, ParameterArgumentBinder.Bind(TypeMapper.Execute(name), function.Arguments).Parameters);

if (function.Arguments.Any(x => x.Name is not null) && name == "generate")
function = new Bindings.Function(name, ParameterArgumentBinder.Bind(TypeMapper.Execute(name), function.Arguments).Parameters);

if (name.Equals("record", StringComparison.OrdinalIgnoreCase))
return BuildRecordFunction(function, context);
if (name.Equals("with", StringComparison.OrdinalIgnoreCase))
Expand All @@ -240,6 +243,8 @@ private IFunction InstantiateOrWrapAggregation(Bindings.Function function, ICont
return BuildCoalesceFunction(function, context);
if (name.Equals("adjacent", StringComparison.OrdinalIgnoreCase))
return BuildAdjacentFunction(function, context);
if (name.Equals("generate", StringComparison.OrdinalIgnoreCase))
return BuildGenerateFunction(function, context);

if (name.Equals("extend", StringComparison.OrdinalIgnoreCase))
{
Expand Down Expand Up @@ -479,6 +484,34 @@ private IFunction BuildAdjacentFunction(Bindings.Function function, IContext con
return new Adjacent(() => new LexicallyBoundTupleFunction(operation));
}

private IFunction BuildGenerateFunction(Bindings.Function function, IContext context)
{
if (function.Parameters.Length is < 2 or > 3)
throw new MissingOrUnexpectedParametersFunctionException(function.Name, function.Parameters.Length);

var condition = BuildPredicateProvider(function.Parameters[0], context, function.Name);
if (!TryGetOpenExpression(function.Parameters[1], out var next))
{
throw new ArgumentException(
$"The function named '{function.Name}' expects parameter 'next' to be an open expression.",
nameof(function));
}

Func<IFunction>? result = null;
if (function.Parameters.Length == 3)
{
if (!TryGetOpenExpression(function.Parameters[2], out var projection))
{
throw new ArgumentException(
$"The function named '{function.Name}' expects parameter 'result' to be an open expression.",
nameof(function));
}
result = BuildTransformationProvider(projection, context);
}

return new Generate(condition, BuildTransformationProvider(next, context), result);
}

private bool TryBuildBinaryCallable(string name, IContext context, [NotNullWhen(true)] out IFunction? callable)
{
var parameterized = new Bindings.Function(name, [new TupleProjectionParameter(0)]);
Expand Down
43 changes: 43 additions & 0 deletions conformance/functions/array/generate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
suite: array
kind: function
operator: generate
tests:
- id: generate.valid.while.next.optional-result
cases:
- id: generate.valid.while.next.optional-result.numeric.increasing
value: 1
expected: [1, 2, 3, 4, 5]
parameters:
- less-than-or-equal(5)
- add(1)
- id: generate.valid.while.next.optional-result.numeric.steps
value: 10
expected: [10, 20, 30, 40]
parameters:
- less-than(50)
- add(10)
- id: generate.valid.while.next.optional-result.numeric.empty
value: 10
expected: []
parameters:
- less-than(5)
- add(1)
- id: generate.valid.while.next.optional-result.numeric.projected
value: 1
expected: [2, 4, 6]
parameters:
- less-than-or-equal(3)
- add(1)
- multiply(2)
- id: generate.valid.while.next.optional-result.special.null
value: (null)
expected: []
parameters:
- "!null"
- add(1)
- id: generate.valid.while.next.optional-result.special.empty
value: (empty)
expected: []
parameters:
- "!empty"
- add(1)
34 changes: 34 additions & 0 deletions docs/_data/function.json
Original file line number Diff line number Diff line change
Expand Up @@ -3894,5 +3894,39 @@
"Examples": [
"\"Crème brûlée recipe\" | slug → \"creme-brulee-recipe\""
]
},
{
"Name": "generate",
"IsPublic": true,
"Aliases": [
"generate"
],
"Scope": "array",
"Input": "any",
"Output": "array",
"Summary": "Generates an array by repeatedly transforming a seed while a condition is satisfied.",
"Parameters": [
{
"Name": "while",
"Type": "predicate",
"Optional": false,
"Summary": "Specifies the predicate that determines whether the current seed is included."
},
{
"Name": "next",
"Type": "expression",
"Optional": false,
"Summary": "Specifies the expression that produces the next seed."
},
{
"Name": "result",
"Type": "expression",
"Optional": true,
"Summary": "Specifies the expression that produces the value appended for the current seed."
}
],
"Examples": [
"1 | generate(while := less-than-or-equal(3), next := add(1)) → {1, 2, 3}"
]
}
]
Loading