Files
MycLib/Src/AST/Myc.Ast.RTL.Series.pas
T
2026-01-13 23:17:32 +01:00

269 lines
9.8 KiB
ObjectPascal

unit Myc.Ast.RTL.Series;
interface
uses
System.SysUtils,
System.Generics.Collections,
Myc.Data.Scalar,
Myc.Data.Value,
Myc.Data.Series,
Myc.Ast.Attributes;
type
TRtlSeriesFunctions = record
public
// --- Functional / Series ---
[TRtlExport('memoize', Pure)]
[AstDoc('Creates a memoized version of a function for performance.')]
[AstSignature('((any) -> any) -> ((any) -> any)')]
class function Memoize(const Args: TArray<TDataValue>): TDataValue; static;
[TRtlExport('map', Pure)]
[AstDoc('Applies a function to every element of a series.')]
[AstSignature('(series, (any) -> any) -> series')]
class function Map(const Args: TArray<TDataValue>): TDataValue; static;
[TRtlExport('reduce', Pure)]
[AstDoc('Collapses a series into a single value using a reducer function.')]
[AstSignature('(series, any, (any, any) -> any) -> any')]
class function Reduce(const Args: TArray<TDataValue>): TDataValue; static;
[TRtlExport('where', Pure)]
[AstDoc('Filters a series based on a predicate function.')]
[AstSignature('(series, (any) -> boolean) -> series')]
class function Where(const Args: TArray<TDataValue>): TDataValue; static;
[TRtlExport('any', Pure)]
[AstDoc('Returns true if at least one element satisfies the predicate.')]
[AstSignature('(series, (any) -> boolean) -> boolean')]
class function Any(const Args: TArray<TDataValue>): TDataValue; static;
[TRtlExport('count', Pure)]
[AstDoc('Returns the current number of elements in a series.')]
[AstSignature('(series) -> number')]
[AstScriptExample('(count prices)')]
class function Count(const Args: TArray<TDataValue>): TDataValue; static;
end;
implementation
{ TRtlSeriesFunctions }
class function TRtlSeriesFunctions.Memoize(const Args: TArray<TDataValue>): TDataValue;
var
funcToMemoize: TDataValue.TFunc;
memoizedFunc: TDataValue.TFunc;
begin
if Length(Args) <> 1 then
raise EArgumentException.Create('Memoize requires exactly one argument.');
if Args[0].Kind <> vkMethod then
raise EArgumentException.Create('The argument to Memoize must be a function.');
// We store the dictionary inside a TObjVal wrapped in a TDataValue to keep it alive
// as part of the closure's captured state (or rather, accessible via the closure).
// Note: In a pure closure implementation, capturing a local variable would be enough,
// but here we ensure explicit lifetime management via the TDataValue ownership mechanism if needed.
var cCache: TDataValue;
cCache.FromObj(TDictionary<Int64, TDataValue>.Create);
funcToMemoize := Args[0].AsMethod();
memoizedFunc :=
function(const AArgs: TArray<TDataValue>): TDataValue
var
argScalar: TScalar;
key: Int64;
begin
if (Length(AArgs) <> 1) or (AArgs[0].Kind <> vkScalar) then
raise EArgumentException.Create('This memoized function can only be called with a single scalar argument.');
argScalar := AArgs[0].AsScalar;
if argScalar.Kind <> TScalar.TKind.Ordinal then
raise EArgumentException.Create('This memoized function expects an ordinal argument for caching.');
key := argScalar.Value.AsInt64;
// Unsafe cast is safe here because we created it above
var cache := TDictionary<Int64, TDataValue>(cCache.AsObject);
if cache.TryGetValue(key, Result) then
exit;
Result := funcToMemoize(AArgs);
cache.Add(key, Result);
end;
Result := TDataValue(memoizedFunc);
end;
class function TRtlSeriesFunctions.Map(const Args: TArray<TDataValue>): TDataValue;
var
sourceArg: TDataValue;
begin
if Length(Args) <> 2 then
raise EArgumentException.Create('Map requires exactly two arguments: a series and a function.');
sourceArg := Args[0];
if not (sourceArg.Kind in [vkSeries]) then
raise EArgumentException.Create('The first argument to Map must be a series.');
if Args[1].Kind <> vkMethod then
raise EArgumentException.Create('The second argument to Map must be a function.');
Result := TDataValue.Map(sourceArg.AsSeries, Args[1].AsMethod());
end;
class function TRtlSeriesFunctions.Reduce(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.');
sourceSeries := sourceArg.AsSeries;
accumulator := Args[1];
reducerFunc := Args[2].AsMethod();
// Reduce typically iterates from oldest to newest (index count-1 downto 0 in standard array,
// but TSeries logic might define 0 as newest. Assuming standard iteration order here).
// Note: Based on previous TSeries implementation, Index 0 is newest.
// Usually Reduce goes chronologically: Oldest (High) -> Newest (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;
class function TRtlSeriesFunctions.Where(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;
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();
matchingIndices := TList<Integer>.Create;
try
// We scan all items. Order doesn't strictly matter for building the index list
// if we just want to filter, but keeping order is good.
// Iterating 0 to Count-1 (Newest to Oldest) or vice versa depends on desired output series order.
// TIndexSeries usually expects indices.
for i := 0 to sourceSeries.Count - 1 do
begin
item := sourceSeries.Items[i];
// Perf warning: Allocation of array for every call
predicateResult := predicateFunc([TDataValue(item)]);
if (predicateResult.Kind = vkScalar) and (Boolean(predicateResult.AsScalar)) then
matchingIndices.Add(i);
end;
indexSeries := TIndexSeries.Create(matchingIndices.ToArray);
finally
matchingIndices.Free;
end;
// Create a lazy mapper that looks up the original value based on the index
mapperFunc :=
function(const AArgs: TArray<TDataValue>): TDataValue
var
idx: Integer;
begin
idx := AArgs[0].AsScalar.Value.AsInt64;
Result := TDataValue(sourceSeries.Items[idx]);
end;
// Result is a new series containing only the filtered items
Result := TDataValue.Map(indexSeries, mapperFunc);
end;
class function TRtlSeriesFunctions.Any(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 (Boolean(predicateResult.AsScalar)) then
begin
Result := TScalar.FromBoolean(True);
exit;
end;
end;
Result := TScalar.FromBoolean(False);
end;
class function TRtlSeriesFunctions.Count(const Args: TArray<TDataValue>): TDataValue;
begin
if Length(Args) <> 1 then
raise EArgumentException.Create('Count requires one argument: a series.');
var sourceArg := Args[0];
case sourceArg.Kind of
vkSeries: Result := sourceArg.AsSeries.Count;
vkRecordSeries: Result := sourceArg.AsRecordSeries.Count;
else
raise EArgumentException.Create('The argument to Count must be a series.');
end;
end;
end.