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; 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; 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; 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; 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; 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; static; end; implementation { TRtlSeriesFunctions } class function TRtlSeriesFunctions.Memoize(const Args: TArray): 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.Create); funcToMemoize := Args[0].AsMethod(); memoizedFunc := function(const AArgs: TArray): 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(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; 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; var sourceArg: TDataValue; sourceSeries: ISeries; accumulator: TDataValue; reducerFunc: TDataValue.TFunc; i: Integer; currentItem: TScalar; reducerArgs: TArray; 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; var sourceArg: TDataValue; sourceSeries: ISeries; predicateFunc: TDataValue.TFunc; matchingIndices: TList; 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.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 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; 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; 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.