Skip to content
Draft
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
108 changes: 108 additions & 0 deletions Expressif.Testing/Functions/Array/PositionFunctionsTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
using System.Collections;
using Expressif.Functions.Array;
using Expressif.Testing.Conformance;
using Expressif.Values;

namespace Expressif.Testing.Functions.Array;

[TestFixture]
public class WithPositionTest
{
[Conformance]
public void WithPosition_Valid(object? input, object?[]? expected)
=> Assert.That(new WithPosition().Evaluate(input), Is.EqualTo(ToTuples(expected)));

private static object? ToTuples(object?[]? expected)
=> expected?.Select(pair =>
{
var values = (object?[])pair!;
return new TupleValue(int.Parse((string)values[0]!), values[1]);
}).ToArray();

[Test]
public void Evaluate_IsProgressiveAndUsesOneEnumerator()
{
var source = new TrackingEnumerable("a", "b", "c");
var result = ((IEnumerable)new WithPosition().Evaluate(source)!).GetEnumerator();

Assert.That(source.MoveNextCalls, Is.Zero);
Assert.That(result.MoveNext(), Is.True);
Assert.Multiple(() =>
{
Assert.That(source.GetEnumeratorCalls, Is.EqualTo(1));
Assert.That(source.MoveNextCalls, Is.EqualTo(1));
Assert.That(result.Current, Is.EqualTo(new TupleValue(0, "a")));
});
}

private sealed class TrackingEnumerable(params object?[] values) : IEnumerable
{
public int GetEnumeratorCalls { get; private set; }
public int MoveNextCalls { get; private set; }

public IEnumerator GetEnumerator()
{
GetEnumeratorCalls++;
return Enumerate().GetEnumerator();
}

private IEnumerable Enumerate()
{
foreach (var value in values)
{
MoveNextCalls++;
yield return value;
}
}
}
}

[TestFixture]
public class PositionOfTest
{
[Conformance]
public void PositionOf_Valid_Value(object? input, object? value, int? expected)
=> Assert.That(new PositionOf(() => value).Evaluate(input), Is.EqualTo(expected));

[Test]
public void Evaluate_StopsAfterFirstMatch()
{
var source = new ThrowAfterMatchEnumerable();

Assert.That(new PositionOf(() => "match").Evaluate(source), Is.EqualTo(0));
}

private sealed class ThrowAfterMatchEnumerable : IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return "match";
throw new InvalidOperationException("The source should not be enumerated after the first match.");
}
}
}

[TestFixture]
public class ValueAtTest
{
[Conformance]
public void ValueAt_Valid_Position(object? input, int position, object? expected)
=> Assert.That(new ValueAt(() => position).Evaluate(input), Is.EqualTo(expected));

[Test]
public void Evaluate_StopsAtRequestedPosition()
{
var source = new ThrowAfterRequestedPositionEnumerable();

Assert.That(new ValueAt(() => 0).Evaluate(source), Is.EqualTo("first"));
}

private sealed class ThrowAfterRequestedPositionEnumerable : IEnumerable
{
public IEnumerator GetEnumerator()
{
yield return "first";
throw new InvalidOperationException("The source should not be enumerated past the requested position.");
}
}
}
15 changes: 15 additions & 0 deletions Expressif.Testing/Functions/ExpressionFactoryTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,21 @@ public void Instantiate_Lead_Valid()
Assert.That(lead, Is.Not.Null);
}

[Test]
public void Instantiate_PositionFunctions_Valid()
{
var withPosition = new ExpressionFactory().Instantiate("with-position", new Context());
var positionOf = new ExpressionFactory().Instantiate("position-of(b)", new Context());
var valueAt = new ExpressionFactory().Instantiate("value-at(1)", new Context());

Assert.Multiple(() =>
{
Assert.That(GetSingleFunction<WithPosition>(withPosition), Is.Not.Null);
Assert.That(GetSingleFunction<PositionOf>(positionOf).Value.Invoke(), Is.EqualTo("b"));
Assert.That(GetSingleFunction<ValueAt>(valueAt).Position.Invoke(), Is.EqualTo(1));
});
}

[Test]
[TestCase("first-elements(2)", typeof(FirstElements))]
[TestCase("first(2)", typeof(FirstElements))]
Expand Down
82 changes: 82 additions & 0 deletions Expressif/Functions/Array/PositionFunctions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.Collections;
using System.Collections.Generic;

namespace Expressif.Functions.Array;

/// <summary>
/// Returns each input item paired with its zero-based position as a tuple in `(position, value)` order. Preserves input order and cardinality. Position terminology distinguishes sequence locations from indexes used to accelerate searches. Returns `null` when the input cannot be evaluated.
/// </summary>
[Function(prefix: "", aliases: ["with-position"])]
public class WithPosition : BaseArrayFunction
{
protected override object? EvaluateArray(IEnumerable enumerable)
{
// TODO REVIEW Scaffold

Check warning on line 15 in Expressif/Functions/Array/PositionFunctions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=Seddryck_Expressif&issues=AZ_YsK83sEtYd74_MEEW&open=AZ_YsK83sEtYd74_MEEW&pullRequest=508
return Enumerate(enumerable);
}

private static IEnumerable<Expressif.Values.Tuple> Enumerate(IEnumerable source)
{
var position = 0;
foreach (var item in source)
yield return new Expressif.Values.Tuple(position++, item);
}
}

/// <summary>
/// Returns the zero-based position of the first input item equal to the specified value. Returns `null` when no item matches or the input cannot be evaluated.
/// </summary>
[Function(prefix: "", aliases: ["position-of"])]
public class PositionOf : BaseArrayFunction
{
public Func<object?> Value { get; }

/// <param name="value">Specifies the value to locate.</param>
public PositionOf(Func<object?> value)
=> Value = value;

protected override object? EvaluateArray(IEnumerable enumerable)
{
// TODO REVIEW Scaffold

Check warning on line 41 in Expressif/Functions/Array/PositionFunctions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=Seddryck_Expressif&issues=AZ_YsK83sEtYd74_MEEX&open=AZ_YsK83sEtYd74_MEEX&pullRequest=508
var value = Value.Invoke();
var position = 0;
foreach (var item in enumerable)
{
if (EqualityComparer<object?>.Default.Equals(item, value))
return position;

position++;
}

return null;
}
}

/// <summary>
/// Returns the input item at the specified zero-based position. Returns `null` when the position is negative or out of range, or the input cannot be evaluated.
/// </summary>
[Function(prefix: "", aliases: ["value-at"])]
public class ValueAt : BaseArrayFunction
{
public Func<int> Position { get; }

/// <param name="position">Specifies the zero-based position of the item to return.</param>
public ValueAt(Func<int> position)
=> Position = position;

protected override object? EvaluateArray(IEnumerable enumerable)
{
// TODO REVIEW Scaffold

Check warning on line 70 in Expressif/Functions/Array/PositionFunctions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=Seddryck_Expressif&issues=AZ_YsK83sEtYd74_MEEY&open=AZ_YsK83sEtYd74_MEEY&pullRequest=508
var requestedPosition = Position.Invoke();
if (requestedPosition < 0)
return null;

var position = 0;
foreach (var item in enumerable)
if (position++ == requestedPosition)
return item;

return null;
}
}
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,16 @@ Once installed, Expressif files and code blocks will be highlighted automaticall
|Array | lead | array-to-lead |
|Array | map | map |
|Array | pairwise | pairwise |
|Array | position-of | position-of |
|Array | reverse | reverse |
|Array | scan | array-to-scan |
|Array | skip-first-elements | skip-first |
|Array | skip-last-elements | skip-last |
|Array | slice-elements | slice |
|Array | symmetric-difference | symmetric-difference |
|Array | union | union |
|Array | value-at | value-at |
|Array | with-position | with-position |
|IO | creation-datetime | file-to-creation-datetime, file-to-creation-dateTime |
|IO | creation-datetime-utc | file-to-creation-datetime-utc, file-to-creation-dateTime-utc |
|IO | directory | path-to-directory |
Expand Down
36 changes: 36 additions & 0 deletions conformance/functions/array/position-of.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
suite: array
kind: function
operator: position-of
tests:
- id: position-of.valid.value
cases:
- id: position-of.valid.value.special.null
value: (null)
expected:
parameters:
- b
- id: position-of.valid.value.special.empty
value: (empty)
expected:
parameters:
- b
- id: position-of.valid.value.array.empty
value: []
expected:
parameters:
- b
- id: position-of.valid.value.text.match
value: [a, b, c]
expected: 1
parameters:
- b
- id: position-of.valid.value.text.duplicate
value: [a, b, a]
expected: 0
parameters:
- a
- id: position-of.valid.value.text.non-match
value: [a, b, c]
expected:
parameters:
- z
41 changes: 41 additions & 0 deletions conformance/functions/array/value-at.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
suite: array
kind: function
operator: value-at
tests:
- id: value-at.valid.position
cases:
- id: value-at.valid.position.special.null
value: (null)
expected:
parameters:
- 0
- id: value-at.valid.position.special.empty
value: (empty)
expected:
parameters:
- 0
- id: value-at.valid.position.array.empty
value: []
expected:
parameters:
- 0
- id: value-at.valid.position.text.first
value: [a, b, c]
expected: a
parameters:
- 0
- id: value-at.valid.position.text.middle
value: [a, b, c]
expected: b
parameters:
- 1
- id: value-at.valid.position.numeric.negative
value: [a, b, c]
expected:
parameters:
- -1
- id: value-at.valid.position.numeric.out-of-range
value: [a, b, c]
expected:
parameters:
- 10
21 changes: 21 additions & 0 deletions conformance/functions/array/with-position.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
suite: array
kind: function
operator: with-position
tests:
- id: with-position.valid
cases:
- id: with-position.valid.special.null
value: (null)
expected:
- id: with-position.valid.special.empty
value: (empty)
expected:
- id: with-position.valid.array.empty
value: []
expected: []
- id: with-position.valid.array.values
value: [a, b, c]
expected: [[0, a], [1, b], [2, c]]
- id: with-position.valid.array.null-value
value: [a, (null), c]
expected: [[0, a], [1, (null)], [2, c]]
42 changes: 42 additions & 0 deletions docs/_data/function.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,22 @@
"Summary": "Returns each consecutive pair of input values as a tuple. Returns `null` when the input cannot be evaluated.",
"Parameters": []
},
{
"Name": "position-of",
"IsPublic": true,
"Aliases": [
"position-of"
],
"Scope": "Array",
"Summary": "Returns the zero-based position of the first input item equal to the specified value. Returns `null` when no item matches or the input cannot be evaluated.",
"Parameters": [
{
"Name": "value",
"Optional": false,
"Summary": "Specifies the value to locate."
}
]
},
{
"Name": "reverse",
"IsPublic": true,
Expand Down Expand Up @@ -326,6 +342,32 @@
}
]
},
{
"Name": "value-at",
"IsPublic": true,
"Aliases": [
"value-at"
],
"Scope": "Array",
"Summary": "Returns the input item at the specified zero-based position. Returns `null` when the position is negative or out of range, or the input cannot be evaluated.",
"Parameters": [
{
"Name": "position",
"Optional": false,
"Summary": "Specifies the zero-based position of the item to return."
}
]
},
{
"Name": "with-position",
"IsPublic": true,
"Aliases": [
"with-position"
],
"Scope": "Array",
"Summary": "Returns each input item paired with its zero-based position as a tuple in `(position, value)` order. Preserves input order and cardinality. Position terminology distinguishes sequence locations from indexes used to accelerate searches. Returns `null` when the input cannot be evaluated.",
"Parameters": []
},
{
"Name": "creation-datetime",
"IsPublic": true,
Expand Down
2 changes: 1 addition & 1 deletion docs/_data/navigation_boxes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@

- title: Functions and predicates
#Template# desc: List the <function> available functions and <predicate> predicates
desc: List the 201 available functions and 75 predicates
desc: List the 202 available functions and 75 predicates
icon: bookmark
doc: library-index
Loading