Ast RTL: Map, Reduce, Where, Any

This commit is contained in:
Michael Schimmel
2025-09-18 16:09:51 +02:00
parent 1be9591a61
commit 5f110e4408
3 changed files with 233 additions and 51 deletions
+140 -50
View File
@@ -17,55 +17,8 @@ uses
implementation
type
// Implements a lazily evaluated, read-only series based on a source series and a mapper function.
TMapSeries = class(TInterfacedObject, ISeries)
private
FSourceSeries: ISeries;
FMapperFunc: TDataValue.TFunc;
// ISeries
function GetCount: Int64;
function GetTotalCount: Int64;
function GetItems(Idx: Integer): TScalar;
public
constructor Create(const ASourceSeries: ISeries; const AMapperFunc: TDataValue.TFunc);
end;
{ TMapSeries }
constructor TMapSeries.Create(const ASourceSeries: ISeries; const AMapperFunc: TDataValue.TFunc);
begin
inherited Create;
FSourceSeries := ASourceSeries;
FMapperFunc := AMapperFunc;
end;
function TMapSeries.GetCount: Int64;
begin
Result := FSourceSeries.Count;
end;
function TMapSeries.GetTotalCount: Int64;
begin
Result := FSourceSeries.TotalCount;
end;
function TMapSeries.GetItems(Idx: Integer): TScalar;
var
sourceValue: TScalar;
argArray: TArray<TDataValue>;
mappedValue: TDataValue;
begin
// Get the original item, apply the mapper function, and return the result.
sourceValue := FSourceSeries.Items[Idx];
argArray := [TDataValue(sourceValue)];
mappedValue := FMapperFunc(argArray);
if mappedValue.Kind <> vkScalar then
raise EInvalidCast.Create('Map function did not return a scalar value.');
Result := mappedValue.AsScalar;
end;
uses
System.Generics.Collections;
//--------------------------------------------------------------------------------------------------
//== Native Function Implementations
@@ -179,7 +132,141 @@ begin
if Args[1].Kind <> vkMethod then
raise EArgumentException.Create('The second argument to Map must be a function.');
Result := TDataValue.FromSeries(TMapSeries.Create(sourceArg.AsSeries, Args[1].AsMethod()));
Result := TDataValue.FromSeries(TDataValue.Map(sourceArg.AsSeries, Args[1].AsMethod()));
end;
// Reduces a series into a single value.
function NativeReduce(const Args: TArray<TDataValue>): TDataValue;
var
sourceArg: TDataValue;
sourceSeries: ISeries;
accumulator: TDataValue;
reducerFunc: TDataValue.TFunc;
i: Integer;
currentItem: TScalar;
reducerArgs: TArray<TDataValue>;
begin
if Length(Args) <> 3 then
raise EArgumentException.Create('Reduce requires exactly three arguments: a series, an initial value, and a reducer function.');
sourceArg := Args[0];
if sourceArg.Kind <> vkSeries then
raise EArgumentException.Create('The first argument to Reduce must be a series.');
if Args[2].Kind <> vkMethod then
raise EArgumentException.Create('The third argument to Reduce must be a function.');
// Extract arguments
sourceSeries := sourceArg.AsSeries;
accumulator := Args[1];
reducerFunc := Args[2].AsMethod();
// Iterate from oldest to newest element (index Count-1 to 0)
for i := sourceSeries.Count - 1 downto 0 do
begin
currentItem := sourceSeries.Items[i];
reducerArgs := [accumulator, TDataValue(currentItem)];
accumulator := reducerFunc(reducerArgs);
end;
Result := accumulator;
end;
// Filters a series based on a predicate.
function NativeWhere(const Args: TArray<TDataValue>): TDataValue;
var
sourceArg: TDataValue;
sourceSeries: ISeries;
predicateFunc: TDataValue.TFunc;
matchingIndices: TList<Integer>;
i: Integer;
item: TScalar;
predicateResult: TDataValue;
indexSeries: ISeries;
mapperFunc: TDataValue.TFunc;
finalSeries: ISeries;
begin
if Length(Args) <> 2 then
raise EArgumentException.Create('Where requires exactly two arguments: a series and a predicate function.');
sourceArg := Args[0];
if sourceArg.Kind <> vkSeries then
raise EArgumentException.Create('The first argument to Where must be a series.');
if Args[1].Kind <> vkMethod then
raise EArgumentException.Create('The second argument to Where must be a function.');
sourceSeries := sourceArg.AsSeries;
predicateFunc := Args[1].AsMethod();
// 1. Eagerly build the index of matching items
matchingIndices := TList<Integer>.Create;
try
for i := sourceSeries.Count - 1 downto 0 do
begin
item := sourceSeries.Items[i];
predicateResult := predicateFunc([TDataValue(item)]);
if (predicateResult.Kind = vkScalar) and (predicateResult.AsScalar.Value.AsBoolean) then
matchingIndices.Add(i);
end;
// 2. Create a helper series from the index
indexSeries := TIndexSeries.Create(matchingIndices.ToArray);
finally
matchingIndices.Free;
end;
// 3. Define a mapper to transform an index back to a value from the original series
mapperFunc :=
function(const AArgs: TArray<TDataValue>): TDataValue
var
idx: Integer;
begin
idx := AArgs[0].AsScalar.Value.AsInteger;
Result := TDataValue(sourceSeries.Items[idx]);
end;
// 4. Use TMapSeries to create the final, filtered series
finalSeries := TDataValue.Map(indexSeries, mapperFunc);
Result := TDataValue.FromSeries(finalSeries);
end;
// Checks if any element in a series satisfies a predicate.
function NativeAny(const Args: TArray<TDataValue>): TDataValue;
var
sourceArg: TDataValue;
sourceSeries: ISeries;
predicateFunc: TDataValue.TFunc;
i: Integer;
item: TScalar;
predicateResult: TDataValue;
begin
if Length(Args) <> 2 then
raise EArgumentException.Create('Any requires exactly two arguments: a series and a predicate function.');
sourceArg := Args[0];
if sourceArg.Kind <> vkSeries then
raise EArgumentException.Create('The first argument to Any must be a series.');
if Args[1].Kind <> vkMethod then
raise EArgumentException.Create('The second argument to Any must be a function.');
sourceSeries := sourceArg.AsSeries;
predicateFunc := Args[1].AsMethod();
for i := 0 to sourceSeries.Count - 1 do
begin
item := sourceSeries.Items[i];
predicateResult := predicateFunc([TDataValue(item)]);
if (predicateResult.Kind = vkScalar) and (predicateResult.AsScalar.Value.AsBoolean) then
begin
Result := TScalar.FromBoolean(True);
exit;
end;
end;
Result := TScalar.FromBoolean(False);
end;
//--------------------------------------------------------------------------------------------------
@@ -194,6 +281,9 @@ begin
AScope.Define('Floor', TDataValue(NativeFloor));
AScope.Define('Sign', TDataValue(NativeSign));
AScope.Define('Map', TDataValue(NativeMap));
AScope.Define('Reduce', TDataValue(NativeReduce));
AScope.Define('Where', TDataValue(NativeWhere));
AScope.Define('Any', TDataValue(NativeAny));
end;
initialization
+38
View File
@@ -254,6 +254,16 @@ type
property TotalCount: Int64 read GetTotalCount;
end;
TIndexSeries = class(TInterfacedObject, ISeries)
private
FIndexArray: TArray<Integer>;
function GetCount: Int64;
function GetTotalCount: Int64;
function GetItems(Idx: Integer): TScalar;
public
constructor Create(const AIndexArray: TArray<Integer>);
end;
TBinaryOperatorHelper = record helper for TBinaryOperator
function ToString: string;
end;
@@ -1557,6 +1567,34 @@ begin
Result := FTotalCount;
end;
{ TIndexSeries }
constructor TIndexSeries.Create(const AIndexArray: TArray<Integer>);
begin
inherited Create;
FIndexArray := AIndexArray;
end;
function TIndexSeries.GetCount: Int64;
begin
Result := Length(FIndexArray);
end;
function TIndexSeries.GetTotalCount: Int64;
begin
Result := GetCount;
end;
function TIndexSeries.GetItems(Idx: Integer): TScalar;
var
sourceIndex: Integer;
begin
// The series maintains the "0 is newest" convention.
// The index array was built from oldest to newest, so we access it in reverse.
sourceIndex := FIndexArray[GetCount - 1 - Idx];
Result := TScalar.FromInteger(sourceIndex);
end;
initialization
Assert(sizeof(TScalarValue) = 8);
+55 -1
View File
@@ -63,6 +63,8 @@ type
class function Defer(const Value: TDataValue; const Calc: TFunc): TDataValue; overload; static;
class function Map(const SourceSeries: ISeries; const MapperFunc: TFunc): ISeries; static;
class function FromRecordSeries(const AValue: IRecordSeries): TDataValue; static; inline;
class function FromRecord(const [ref] AValue: TScalarRecord): TDataValue; static; inline;
class function FromSeries(const AValue: ISeries): TDataValue; static; inline;
@@ -79,7 +81,6 @@ type
property Kind: TDataValueKind read GetKind;
end;
type
TDataValueKindHelper = record helper for TDataValueKind
public
function ToString: string;
@@ -112,6 +113,18 @@ type
constructor Create(const AVal: TScalar; const ACalc: TDataValue.TFunc);
end;
TMapSeries = class(TInterfacedObject, ISeries)
private
FSourceSeries: ISeries;
FMapperFunc: TDataValue.TFunc;
// ISeries
function GetCount: Int64;
function GetTotalCount: Int64;
function GetItems(Idx: Integer): TScalar;
public
constructor Create(const ASourceSeries: ISeries; const AMapperFunc: TDataValue.TFunc);
end;
constructor TManagedLazy.Create(AKind: TDataValueKind; const AVal: IInterface; const ACalc: TDataValue.TFunc);
begin
inherited Create;
@@ -329,6 +342,11 @@ begin
Result := TDataValueKind(FKind);
end;
class function TDataValue.Map(const SourceSeries: ISeries; const MapperFunc: TFunc): ISeries;
begin
Result := TMapSeries.Create(SourceSeries, MapperFunc);
end;
function TDataValue.ToString: String;
var
sb: TStringBuilder;
@@ -430,4 +448,40 @@ begin
end;
end;
{ TMapSeries }
constructor TMapSeries.Create(const ASourceSeries: ISeries; const AMapperFunc: TDataValue.TFunc);
begin
inherited Create;
FSourceSeries := ASourceSeries;
FMapperFunc := AMapperFunc;
end;
function TMapSeries.GetCount: Int64;
begin
Result := FSourceSeries.Count;
end;
function TMapSeries.GetTotalCount: Int64;
begin
Result := FSourceSeries.TotalCount;
end;
function TMapSeries.GetItems(Idx: Integer): TScalar;
var
sourceValue: TScalar;
argArray: TArray<TDataValue>;
mappedValue: TDataValue;
begin
// Get the original item, apply the mapper function, and return the result.
sourceValue := FSourceSeries.Items[Idx];
argArray := [TDataValue(sourceValue)];
mappedValue := FMapperFunc(argArray);
if mappedValue.Kind <> vkScalar then
raise EInvalidCast.Create('Map function did not return a scalar value.');
Result := mappedValue.AsScalar;
end;
end.