unit Myc.Data.Series; interface type TChunkArray = record type PT = ^T; private const ChunkSize = 1024; type TChunk = array of T; private FChunks: TArray; FOffset: Integer; FCount: Integer; function GetItems(Idx: Integer): T; inline; function GetItemRef(Idx: Integer): PT; inline; public constructor Create(const AChunks: TArray; ACount: Integer; AOffset: Integer); class operator Initialize(out Dest: TChunkArray); // Add a single item without creating a temporary array. procedure Add(const Data: T; MaxCount: Integer); overload; // Add items, but ensure that the result has MaxCount items. procedure Add(const Data: array of T; MaxCount: Integer); overload; class function CreateFromArray(const AData: TArray; MaxCount: Integer): TChunkArray; static; // Creates a deep copy of the array, optionally truncating it to the last MaxCount items. function Copy(MaxCount: Integer = -1): TChunkArray; property Count: Integer read FCount; property Items[Idx: Integer]: T read GetItems; default; property ItemRef[Idx: Integer]: PT read GetItemRef; end; // A series where the last added item has index 0. TSeries = record private FArray: TChunkArray; FTotalCount: Int64; function GetCount: Integer; function GetItems(Idx: Integer): T; public constructor Create(const AArray: TChunkArray; ATotalCount: Int64); class operator Initialize(out Dest: TSeries); // Creates a deep copy of the series, optionally truncating it to the last Lookback items. function Copy(Lookback: Integer = -1): TSeries; procedure Add(const Data: T; Lookback: Int64 = -1); overload; procedure Add(const Data: array of T; Lookback: Integer); overload; class function CreateFromArray(const AData: TArray; Lookback: Integer): TSeries; static; property Count: Integer read GetCount; property TotalCount: Int64 read FTotalCount; property Items[Idx: Integer]: T read GetItems; default; end; implementation uses System.Generics.Collections, System.Math; { TChunkArray } constructor TChunkArray.Create(const AChunks: TArray; ACount, AOffset: Integer); begin FChunks := AChunks; FCount := ACount; FOffset := AOffset; Assert(FOffset < ChunkSize); end; procedure TChunkArray.Add(const Data: T; MaxCount: Integer); var newCount, totalNewOffset: Integer; oldLogicalEnd, newLogicalEnd, oldStartChunk, newStartChunk, oldEndChunk, newEndChunk, chunksToRemove, chunksToAdd: Integer; begin if MaxCount < 0 then MaxCount := MaxInt; if MaxCount <= 0 then exit; // 1. Calculate the new logical state (numToAdd is always 1) newCount := Min(FCount + 1, MaxCount); totalNewOffset := FOffset + FCount + 1 - newCount; // 2. Determine if the underlying FChunks array needs restructuring oldLogicalEnd := FOffset + FCount - 1; newLogicalEnd := totalNewOffset + newCount - 1; oldStartChunk := FOffset div ChunkSize; newStartChunk := totalNewOffset div ChunkSize; oldEndChunk := -1; if FCount > 0 then oldEndChunk := oldLogicalEnd div ChunkSize; newEndChunk := -1; if newCount > 0 then newEndChunk := newLogicalEnd div ChunkSize; chunksToRemove := newStartChunk - oldStartChunk; chunksToAdd := newEndChunk - oldEndChunk; // 3. Perform the add operation if (chunksToRemove > 0) or (chunksToAdd > 0) then begin // --- Rebuild Path: Chunks must be added or removed --- var newChunks: TArray; var oldChunkCount := Length(FChunks); var newChunkCount := oldChunkCount - chunksToRemove + chunksToAdd; SetLength(newChunks, newChunkCount); var numChunksToKeep := oldChunkCount - chunksToRemove; if numChunksToKeep > 0 then TArray.Copy(FChunks, newChunks, chunksToRemove, 0, numChunksToKeep); for var i := numChunksToKeep to newChunkCount - 1 do SetLength(newChunks[i], ChunkSize); FChunks := newChunks; // Copy the new data item into the rebuilt chunks var writeStartLogicalIdx := FOffset + FCount; var physicalWriteStartIdx := writeStartLogicalIdx - (chunksToRemove * ChunkSize); var chunkIdx := physicalWriteStartIdx div ChunkSize; var idxInChunk := physicalWriteStartIdx mod ChunkSize; Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds during rebuild'); FChunks[chunkIdx][idxInChunk] := Data; end else begin // --- Fast Path: Window slides within existing chunk allocation --- var writeStartLogicalIdx := FOffset + FCount; var chunkIdx := writeStartLogicalIdx div ChunkSize; var idxInChunk := writeStartLogicalIdx mod ChunkSize; Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds on fast path'); if Length(FChunks[chunkIdx]) = 0 then // Safeguard SetLength(FChunks[chunkIdx], ChunkSize); FChunks[chunkIdx][idxInChunk] := Data; end; // 4. Finalize the new state FOffset := totalNewOffset mod ChunkSize; FCount := newCount; end; class function TChunkArray.CreateFromArray(const AData: TArray; MaxCount: Integer): TChunkArray; begin Result.Add(AData, MaxCount); end; procedure TChunkArray.Add(const Data: array of T; MaxCount: Integer); var dataLen, dataReadOffset, numToAdd, newCount, totalNewOffset: Integer; oldLogicalEnd, newLogicalEnd, oldStartChunk, newStartChunk, oldEndChunk, newEndChunk, chunksToRemove, chunksToAdd: Integer; begin if MaxCount < 0 then MaxCount := MaxInt; dataLen := Length(Data); if (dataLen = 0) or (MaxCount <= 0) then exit; // 1. Trim input data if it's larger than MaxCount dataReadOffset := 0; if dataLen > MaxCount then dataReadOffset := dataLen - MaxCount; numToAdd := dataLen - dataReadOffset; // 2. Calculate the new logical state (total offset and count) newCount := Min(FCount + numToAdd, MaxCount); // Total logical offset of the new window's start totalNewOffset := FOffset + FCount + numToAdd - newCount; // 3. Determine if the underlying FChunks array needs restructuring oldLogicalEnd := FOffset + FCount - 1; newLogicalEnd := totalNewOffset + newCount - 1; oldStartChunk := FOffset div ChunkSize; newStartChunk := totalNewOffset div ChunkSize; oldEndChunk := -1; if FCount > 0 then oldEndChunk := oldLogicalEnd div ChunkSize; newEndChunk := -1; if newCount > 0 then newEndChunk := newLogicalEnd div ChunkSize; chunksToRemove := newStartChunk - oldStartChunk; chunksToAdd := newEndChunk - oldEndChunk; // 4. Perform the add operation if (chunksToRemove > 0) or (chunksToAdd > 0) then begin // --- Rebuild Path: Chunks must be added or removed --- var newChunks: TArray; var oldChunkCount := Length(FChunks); var newChunkCount := oldChunkCount - chunksToRemove + chunksToAdd; SetLength(newChunks, newChunkCount); // Copy references to the chunks that are kept var numChunksToKeep := oldChunkCount - chunksToRemove; if numChunksToKeep > 0 then TArray.Copy(FChunks, newChunks, chunksToRemove, 0, numChunksToKeep); // Allocate new chunks at the end for var i := numChunksToKeep to newChunkCount - 1 do SetLength(newChunks[i], ChunkSize); FChunks := newChunks; // Copy the new data into the rebuilt chunks var writeStartLogicalIdx := FOffset + FCount; var physicalWriteStartIdx := writeStartLogicalIdx - (chunksToRemove * ChunkSize); var remainingToAdd := numToAdd; var currentDataOffset := dataReadOffset; while remainingToAdd > 0 do begin var chunkIdx := physicalWriteStartIdx div ChunkSize; var idxInChunk := physicalWriteStartIdx mod ChunkSize; var spaceInChunk := ChunkSize - idxInChunk; var countToCopy := Min(remainingToAdd, spaceInChunk); Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds during rebuild'); TArray.Copy(Data, FChunks[chunkIdx], currentDataOffset, idxInChunk, countToCopy); inc(physicalWriteStartIdx, countToCopy); inc(currentDataOffset, countToCopy); dec(remainingToAdd, countToCopy); end; end else begin // --- Fast Path: Window slides within existing chunk allocation --- var writeStartLogicalIdx := FOffset + FCount; var remainingToAdd := numToAdd; var currentDataOffset := dataReadOffset; while remainingToAdd > 0 do begin var chunkIdx := writeStartLogicalIdx div ChunkSize; var idxInChunk := writeStartLogicalIdx mod ChunkSize; var spaceInChunk := ChunkSize - idxInChunk; var countToCopy := Min(remainingToAdd, spaceInChunk); Assert(chunkIdx < Length(FChunks), 'Chunk index out of bounds on fast path'); if Length(FChunks[chunkIdx]) = 0 then // Should not happen on fast path, but as a safeguard SetLength(FChunks[chunkIdx], ChunkSize); TArray.Copy(Data, FChunks[chunkIdx], currentDataOffset, idxInChunk, countToCopy); inc(writeStartLogicalIdx, countToCopy); inc(currentDataOffset, countToCopy); dec(remainingToAdd, countToCopy); end; end; // 5. Finalize the new state FOffset := totalNewOffset mod ChunkSize; FCount := newCount; end; function TChunkArray.GetItems(Idx: Integer): T; begin Result := GetItemRef(Idx)^; end; function TChunkArray.Copy(MaxCount: Integer): TChunkArray; var effectiveCount: Integer; logicalStartIndex: Integer; physicalStartIndex, physicalEndIndex: Integer; startChunkIdx, endChunkIdx: Integer; newOffset: Integer; newChunkCount: Integer; i: Integer; begin // 1. Determine the number of items to copy. effectiveCount := FCount; if (MaxCount >= 0) and (MaxCount < FCount) then effectiveCount := MaxCount; if effectiveCount = 0 then begin // Return an empty array. Result := Default(TChunkArray); exit; end; // 2. Calculate physical location of the data to be copied. logicalStartIndex := FCount - effectiveCount; physicalStartIndex := FOffset + logicalStartIndex; physicalEndIndex := physicalStartIndex + effectiveCount - 1; startChunkIdx := physicalStartIndex div ChunkSize; endChunkIdx := physicalEndIndex div ChunkSize; newOffset := physicalStartIndex mod ChunkSize; // 3. Create the new chunk array for the result. newChunkCount := endChunkIdx - startChunkIdx + 1; SetLength(Result.FChunks, newChunkCount); // 4. Copy chunks. Boundary chunks are deep-copied, internal chunks are shared. if newChunkCount = 1 then begin // All data is in a single chunk, which is both a start and end boundary. // It must always be a deep copy. Result.FChunks[0] := System.Copy(FChunks[startChunkIdx]); end else begin // First chunk (start boundary) must be a deep copy. Result.FChunks[0] := System.Copy(FChunks[startChunkIdx]); // Middle, immutable chunks can be shared by reference. // This loop only runs if there are 3 or more chunks in the copy. for i := 1 to newChunkCount - 2 do begin Result.FChunks[i] := FChunks[startChunkIdx + i]; end; // Last chunk (end boundary) must be a deep copy. Result.FChunks[newChunkCount - 1] := System.Copy(FChunks[endChunkIdx]); end; // 5. Finalize the new TChunkArray state. Result.FOffset := newOffset; Result.FCount := effectiveCount; end; function TChunkArray.GetItemRef(Idx: Integer): PT; begin Assert((Idx >= 0) and (Idx < FCount)); var physicalIdx := Idx + FOffset; Result := @FChunks[physicalIdx div ChunkSize][physicalIdx mod ChunkSize]; end; class operator TChunkArray.Initialize(out Dest: TChunkArray); begin Dest.FOffset := 0; Dest.FCount := 0; end; { TSeries } constructor TSeries.Create(const AArray: TChunkArray; ATotalCount: Int64); begin FArray := AArray; FTotalCount := ATotalCount; end; procedure TSeries.Add(const Data: T; Lookback: Int64); begin FArray.Add(Data, Lookback); inc(FTotalCount); end; procedure TSeries.Add(const Data: array of T; Lookback: Integer); begin FArray.Add(Data, Lookback); inc(FTotalCount, Length(Data)); end; function TSeries.Copy(Lookback: Integer): TSeries; begin // Create a copy of the underlying chunk array, optionally truncating it. Result.FArray := FArray.Copy(Lookback); // The new series' total count is the number of items it actually contains. Result.FTotalCount := Result.FArray.Count; end; class function TSeries.CreateFromArray(const AData: TArray; Lookback: Integer): TSeries; begin Result.FArray.Add(AData, Lookback); Result.FTotalCount := Length(AData); end; function TSeries.GetCount: Integer; begin Result := FArray.Count; end; function TSeries.GetItems(Idx: Integer): T; begin // Access is reversed: series index 0 is the last element in the underlying array Assert((Idx >= 0) and (Idx < FArray.Count), 'Index out of bounds'); Result := FArray.Items[FArray.Count - 1 - Idx]; end; class operator TSeries.Initialize(out Dest: TSeries); begin Dest.FTotalCount := 0; end; end.