Refactoring DataFlow, 1st Generic Data records working
This commit is contained in:
@@ -104,6 +104,7 @@
|
|||||||
<DCC_RemoteDebug>true</DCC_RemoteDebug>
|
<DCC_RemoteDebug>true</DCC_RemoteDebug>
|
||||||
<DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck>
|
<DCC_IntegerOverflowCheck>true</DCC_IntegerOverflowCheck>
|
||||||
<DCC_RangeChecking>true</DCC_RangeChecking>
|
<DCC_RangeChecking>true</DCC_RangeChecking>
|
||||||
|
<DCC_Inlining>off</DCC_Inlining>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
|
<PropertyGroup Condition="'$(Cfg_1_Win32)'!=''">
|
||||||
<DCC_RemoteDebug>false</DCC_RemoteDebug>
|
<DCC_RemoteDebug>false</DCC_RemoteDebug>
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
|
<DCC_Define>RELEASE;$(DCC_Define)</DCC_Define>
|
||||||
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
|
<DCC_SymbolReferenceInfo>0</DCC_SymbolReferenceInfo>
|
||||||
<DCC_DebugInformation>0</DCC_DebugInformation>
|
<DCC_DebugInformation>0</DCC_DebugInformation>
|
||||||
|
<DCC_Inlining>auto</DCC_Inlining>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup Condition="'$(Cfg_2_Win32)'!=''">
|
<PropertyGroup Condition="'$(Cfg_2_Win32)'!=''">
|
||||||
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
|
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
|
||||||
@@ -162,6 +164,12 @@
|
|||||||
<Source>
|
<Source>
|
||||||
<Source Name="MainSource">AuraTrader.dpr</Source>
|
<Source Name="MainSource">AuraTrader.dpr</Source>
|
||||||
</Source>
|
</Source>
|
||||||
|
<Excluded_Packages>
|
||||||
|
<Excluded_Packages Name="$(BDSBIN)\bcboffice2k290.bpl">Embarcadero C++Builder Office 2000 Servers Package</Excluded_Packages>
|
||||||
|
<Excluded_Packages Name="$(BDSBIN)\bcbofficexp290.bpl">Embarcadero C++Builder Office XP Servers Package</Excluded_Packages>
|
||||||
|
<Excluded_Packages Name="$(BDSBIN)\dcloffice2k290.bpl">Microsoft Office 2000 Sample Automation Server Wrapper Components</Excluded_Packages>
|
||||||
|
<Excluded_Packages Name="$(BDSBIN)\dclofficexp290.bpl">Microsoft Office XP Sample Automation Server Wrapper Components</Excluded_Packages>
|
||||||
|
</Excluded_Packages>
|
||||||
</Delphi.Personality>
|
</Delphi.Personality>
|
||||||
<Deployment Version="5">
|
<Deployment Version="5">
|
||||||
<DeployFile LocalName="$(BDS)\Redist\iossimulator\libcgunwind.1.0.dylib" Class="DependencyModule">
|
<DeployFile LocalName="$(BDS)\Redist\iossimulator\libcgunwind.1.0.dylib" Class="DependencyModule">
|
||||||
|
|||||||
+243
-11
@@ -50,7 +50,9 @@ uses
|
|||||||
System.Actions,
|
System.Actions,
|
||||||
FMX.ActnList,
|
FMX.ActnList,
|
||||||
DynamicFMXControl,
|
DynamicFMXControl,
|
||||||
Myc.FMX.Chart;
|
Myc.FMX.Chart,
|
||||||
|
Myc.Trade.Indicators,
|
||||||
|
Myc.DataRecord;
|
||||||
|
|
||||||
type
|
type
|
||||||
TForm1 = class(TForm)
|
TForm1 = class(TForm)
|
||||||
@@ -108,6 +110,7 @@ type
|
|||||||
procedure NewWorkspace;
|
procedure NewWorkspace;
|
||||||
function CurrLayout<T: TControl>: T;
|
function CurrLayout<T: TControl>: T;
|
||||||
procedure AlignControl(Control: TControl);
|
procedure AlignControl(Control: TControl);
|
||||||
|
function CreateStrategy1(Timeframe: TTimeframe): IMycProcessor<TDataPoint<TOhlcItem>>;
|
||||||
function CreateStrategy2(Timeframe: TTimeframe): IMycProcessor<TDataPoint<TOhlcItem>>;
|
function CreateStrategy2(Timeframe: TTimeframe): IMycProcessor<TDataPoint<TOhlcItem>>;
|
||||||
|
|
||||||
published
|
published
|
||||||
@@ -124,29 +127,24 @@ type
|
|||||||
constructor Create(AEquity: Double);
|
constructor Create(AEquity: Double);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
type
|
|
||||||
TStaget1Result = record
|
|
||||||
ATR, Hull, SMA, O, H, L, C: Double
|
|
||||||
end;
|
|
||||||
|
|
||||||
var
|
var
|
||||||
Form1: TForm1;
|
Form1: TForm1;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
TestModule,
|
TestModule;
|
||||||
Myc.Trade.Indicators,
|
|
||||||
Myc.DataRecord;
|
|
||||||
|
|
||||||
{$R *.fmx}
|
{$R *.fmx}
|
||||||
|
|
||||||
procedure TForm1.NewWorkspace;
|
procedure TForm1.NewWorkspace;
|
||||||
|
var
|
||||||
|
tab: TTabItem;
|
||||||
begin
|
begin
|
||||||
var ws: IAuraWorkspace := TMycAuraWorkspace.Create('New workspace', tmTesting);
|
var ws: IAuraWorkspace := TMycAuraWorkspace.Create('New workspace', tmTesting);
|
||||||
FApplication.Workspaces.Insert(-1, ws);
|
FApplication.Workspaces.Insert(-1, ws);
|
||||||
|
|
||||||
var tab := TabControl.Add;
|
tab := TabControl.Add;
|
||||||
tab.Text := ws.Caption;
|
tab.Text := ws.Caption;
|
||||||
tab.Tag := NativeInt(ws);
|
tab.Tag := NativeInt(ws);
|
||||||
|
|
||||||
@@ -297,7 +295,7 @@ begin
|
|||||||
Control.Align := TAlignLayout.Top;
|
Control.Align := TAlignLayout.Top;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TForm1.CreateStrategy2(Timeframe: TTimeframe): IMycProcessor<TDataPoint<TOhlcItem>>;
|
function TForm1.CreateStrategy1(Timeframe: TTimeframe): IMycProcessor<TDataPoint<TOhlcItem>>;
|
||||||
type
|
type
|
||||||
TSignal = record
|
TSignal = record
|
||||||
Sig: Double;
|
Sig: Double;
|
||||||
@@ -453,6 +451,12 @@ begin
|
|||||||
panel.AddDoubleSeries(Signal.Field<Double>('Entry').Sender, TAlphaColors.Green, 1);
|
panel.AddDoubleSeries(Signal.Field<Double>('Entry').Sender, TAlphaColors.Green, 1);
|
||||||
panel.AddDoubleSeries(Signal.Field<Double>('SL').Sender, TAlphaColors.Red, 2);
|
panel.AddDoubleSeries(Signal.Field<Double>('SL').Sender, TAlphaColors.Red, 2);
|
||||||
|
|
||||||
|
var mean := TConverter<TArray<Double>, Double>.CreateGeneric(TIndicators.CreateMean());
|
||||||
|
|
||||||
|
TConverter.Join<Double>([Hull.Sender, Sma.Sender]).Link(mean);
|
||||||
|
|
||||||
|
panel.AddDoubleSeries(mean.Sender, TAlphaColors.Blue, 5);
|
||||||
|
|
||||||
var pnlChart := TMycChart.Create(Self);
|
var pnlChart := TMycChart.Create(Self);
|
||||||
AlignControl(pnlChart);
|
AlignControl(pnlChart);
|
||||||
pnlChart.Height := Layout.ChildrenRect.Width * 9 / 24;
|
pnlChart.Height := Layout.ChildrenRect.Width * 9 / 24;
|
||||||
@@ -460,8 +464,236 @@ begin
|
|||||||
|
|
||||||
pnlChart.SetXAxisCounter<Double>(equity.Sender);
|
pnlChart.SetXAxisCounter<Double>(equity.Sender);
|
||||||
|
|
||||||
|
////////////
|
||||||
|
var EMAFactory := TEMA.Create;
|
||||||
|
|
||||||
|
var Params := TDataRecord.CreateFrom(EMAFactory.Params);
|
||||||
|
Params.SetValue<Integer>('Period', 20);
|
||||||
|
|
||||||
|
var indi := EMAFActory.CreateIndicator(Params);
|
||||||
|
|
||||||
|
var EMAConv := TConverter<TDataRecord, TDataRecord>.CreateGeneric(indi);
|
||||||
|
|
||||||
|
var equityEMA :=
|
||||||
|
equity
|
||||||
|
.Chain<TDataRecord>(TConverter.FieldToRecord<Double>(EMAFactory.Input, 'Price'))
|
||||||
|
.Chain<TDataRecord>(EMAConv)
|
||||||
|
.Chain<Double>(TConverter.FieldOfRecord<Double>(EMAFactory.Output, 'MA'));
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
|
||||||
panel := pnlChart.AddPanel;
|
panel := pnlChart.AddPanel;
|
||||||
panel.AddDoubleSeries(equity.Sender, TAlphaColors.Blue, 3);
|
panel.AddDoubleSeries(equity.Sender, TAlphaColors.Blue, 3);
|
||||||
|
panel.AddDoubleSeries(equityEMA.Sender, TAlphaColors.Gray, 2);
|
||||||
|
|
||||||
|
/////
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TForm1.CreateStrategy2(Timeframe: TTimeframe): IMycProcessor<TDataPoint<TOhlcItem>>;
|
||||||
|
type
|
||||||
|
TSignal = record
|
||||||
|
Sig: Double;
|
||||||
|
SL: Double;
|
||||||
|
Entry: Double;
|
||||||
|
pnl: Double;
|
||||||
|
end;
|
||||||
|
var
|
||||||
|
panel: TMycChart.TPanel;
|
||||||
|
begin
|
||||||
|
var ticker := TConverter.CreateIdentity<TDataPoint<TOhlcItem>>;
|
||||||
|
Result := ticker;
|
||||||
|
|
||||||
|
var OhlcPoint := ticker.Chain<TDataPoint<TOhlcItem>>(TConverter.CreateOhlcAggregation(Timeframe));
|
||||||
|
|
||||||
|
var Ohlc := OhlcPoint.Field<TOhlcItem>('Data');
|
||||||
|
|
||||||
|
var Closes := Ohlc.Field<Double>('Close');
|
||||||
|
|
||||||
|
var Hull := Closes.Chain<Double>(TIndicators.CreateHMA(250)).MakeParallel;
|
||||||
|
var Sma := Closes.Chain<Double>(TIndicators.CreateSMA(200)).MakeParallel;
|
||||||
|
|
||||||
|
var Lowest: Double := Double.MaxValue;
|
||||||
|
var Highest: Double := Double.MinValue;
|
||||||
|
|
||||||
|
var ATR := Ohlc.Chain<Double>(TIndicators.CreateATR(50)).MakeParallel;
|
||||||
|
|
||||||
|
// next stage
|
||||||
|
|
||||||
|
var ATREndPoint := TConverter.CreateEndpoint<Double>(ATR.Sender, 5);
|
||||||
|
var ATRSeries: TSeries<Double>;
|
||||||
|
|
||||||
|
var HullEndPoint := TConverter.CreateEndpoint<Double>(Hull.Sender, 5);
|
||||||
|
var HullSeries: TSeries<Double>;
|
||||||
|
|
||||||
|
var SmaEndPoint := TConverter.CreateEndpoint<Double>(Sma.Sender, 5);
|
||||||
|
var SmaSeries: TSeries<Double>;
|
||||||
|
|
||||||
|
var curr: TSignal;
|
||||||
|
curr.SL := Double.NaN;
|
||||||
|
curr.Entry := Double.NaN;
|
||||||
|
|
||||||
|
var lastHull, lastSma: Double;
|
||||||
|
|
||||||
|
var conv :=
|
||||||
|
TConverter.Join<Double>(
|
||||||
|
[Ohlc.Field<Double>('Low').Sender, Ohlc.Field<Double>('High').Sender, Closes.Sender, ATR.Sender, Hull.Sender, Sma.Sender]
|
||||||
|
);
|
||||||
|
|
||||||
|
var Signal :=
|
||||||
|
TConverter<TArray<Double>, TSignal>.CreateGeneric(
|
||||||
|
function(const Values: TArray<Double>): TSignal
|
||||||
|
begin
|
||||||
|
var low := Values[0];
|
||||||
|
var high := Values[1];
|
||||||
|
var close := Values[2];
|
||||||
|
var atr := Values[3];
|
||||||
|
var hull := Values[4];
|
||||||
|
var sma := Values[5];
|
||||||
|
|
||||||
|
if low < Lowest then
|
||||||
|
Lowest := low;
|
||||||
|
if high > Highest then
|
||||||
|
Highest := high;
|
||||||
|
|
||||||
|
Result := curr;
|
||||||
|
Result.Sig := 0;
|
||||||
|
var pnl: double := NaN;
|
||||||
|
|
||||||
|
if (hull < sma) and (lastHull >= lastSma) then
|
||||||
|
begin
|
||||||
|
if curr.Sig > 0 then
|
||||||
|
pnl := close - curr.Entry;
|
||||||
|
|
||||||
|
curr.Sig := -1;
|
||||||
|
curr.SL := Highest;
|
||||||
|
curr.Entry := close;
|
||||||
|
Result := curr;
|
||||||
|
end
|
||||||
|
else if (hull > sma) and (lastHull <= lastSma) then
|
||||||
|
begin
|
||||||
|
if curr.Sig < 0 then
|
||||||
|
pnl := curr.Entry - close;
|
||||||
|
|
||||||
|
curr.Sig := 1;
|
||||||
|
curr.SL := Lowest;
|
||||||
|
curr.Entry := close;
|
||||||
|
Result := curr;
|
||||||
|
end;
|
||||||
|
|
||||||
|
atr := 15 * atr;
|
||||||
|
if curr.Sig > 0 then
|
||||||
|
begin
|
||||||
|
if close > curr.SL then
|
||||||
|
begin
|
||||||
|
if curr.SL < close - atr then
|
||||||
|
curr.SL := close - atr;
|
||||||
|
Result.SL := curr.SL;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if low <= curr.SL then
|
||||||
|
begin
|
||||||
|
pnl := curr.SL - curr.Entry;
|
||||||
|
curr.Sig := 0;
|
||||||
|
Result.Sig := 0;
|
||||||
|
curr.SL := NaN;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else if curr.Sig < 0 then
|
||||||
|
begin
|
||||||
|
if close < curr.SL then
|
||||||
|
begin
|
||||||
|
if curr.SL > close + atr then
|
||||||
|
curr.SL := close + atr;
|
||||||
|
Result.SL := curr.SL;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if high >= curr.SL then
|
||||||
|
begin
|
||||||
|
pnl := curr.Entry - curr.SL;
|
||||||
|
curr.Sig := 0;
|
||||||
|
Result.Sig := 0;
|
||||||
|
curr.SL := NaN;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if Result.Sig <> 0 then
|
||||||
|
begin
|
||||||
|
Lowest := Double.MaxValue;
|
||||||
|
Highest := Double.MinValue;
|
||||||
|
Result.SL := Double.NaN;
|
||||||
|
Result.Entry := Double.NaN;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Result.pnl := pnl;
|
||||||
|
|
||||||
|
lastHull := hull;
|
||||||
|
lastSma := sma;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
|
||||||
|
conv.Link(Signal);
|
||||||
|
|
||||||
|
var pnl := Signal.Field<Double>('pnl');
|
||||||
|
|
||||||
|
var equity: TConverter<Double, Double> := TEquitySum.Create(10000);
|
||||||
|
pnl.Sender.Link(equity);
|
||||||
|
|
||||||
|
var Layout := CurrLayout<TVertScrollBox>;
|
||||||
|
if Layout = nil then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
var Symbol := SelectedSymbol;
|
||||||
|
if Symbol = '' then
|
||||||
|
exit;
|
||||||
|
|
||||||
|
var chart := TMycChart.Create(Self);
|
||||||
|
AlignControl(chart);
|
||||||
|
chart.Height := Layout.ChildrenRect.Width * 9 / 16;
|
||||||
|
chart.Lookback.Value := 50000;
|
||||||
|
|
||||||
|
chart.SetXAxisSeries(M15, OhlcPoint.Field<TDateTime>('Time').Sender);
|
||||||
|
|
||||||
|
panel := chart.AddPanel;
|
||||||
|
panel.AddOhlcSeries(Ohlc.Sender);
|
||||||
|
panel.AddDoubleSeries(Hull.Sender, TAlphaColors.Cornflowerblue, 2);
|
||||||
|
panel.AddDoubleSeries(Sma.Sender, TAlphaColors.Brown, 1.5);
|
||||||
|
panel.AddDoubleSeries(Signal.Field<Double>('Entry').Sender, TAlphaColors.Green, 1);
|
||||||
|
panel.AddDoubleSeries(Signal.Field<Double>('SL').Sender, TAlphaColors.Red, 2);
|
||||||
|
|
||||||
|
var mean := TConverter<TArray<Double>, Double>.CreateGeneric(TIndicators.CreateMean());
|
||||||
|
|
||||||
|
TConverter.Join<Double>([Hull.Sender, Sma.Sender]).Link(mean);
|
||||||
|
|
||||||
|
panel.AddDoubleSeries(mean.Sender, TAlphaColors.Blue, 5);
|
||||||
|
|
||||||
|
var pnlChart := TMycChart.Create(Self);
|
||||||
|
AlignControl(pnlChart);
|
||||||
|
pnlChart.Height := Layout.ChildrenRect.Width * 9 / 24;
|
||||||
|
pnlChart.Lookback.Value := 50000;
|
||||||
|
|
||||||
|
pnlChart.SetXAxisCounter<Double>(equity.Sender);
|
||||||
|
|
||||||
|
////////////
|
||||||
|
var EMAFactory := TEMA.Create;
|
||||||
|
|
||||||
|
var Params := TDataRecord.CreateFrom(EMAFactory.Params);
|
||||||
|
Params.SetValue<Integer>('Period', 20);
|
||||||
|
|
||||||
|
var indi := EMAFActory.CreateIndicator(Params);
|
||||||
|
|
||||||
|
var EMAConv := TConverter<TDataRecord, TDataRecord>.CreateGeneric(indi);
|
||||||
|
|
||||||
|
var equityEMA :=
|
||||||
|
equity
|
||||||
|
.Chain<TDataRecord>(TConverter.FieldToRecord<Double>(EMAFactory.Input, 'Price'))
|
||||||
|
.Chain<TDataRecord>(EMAConv)
|
||||||
|
.Chain<Double>(TConverter.FieldOfRecord<Double>(EMAFactory.Output, 'MA'));
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
|
||||||
|
panel := pnlChart.AddPanel;
|
||||||
|
panel.AddDoubleSeries(equity.Sender, TAlphaColors.Blue, 3);
|
||||||
|
panel.AddDoubleSeries(equityEMA.Sender, TAlphaColors.Gray, 2);
|
||||||
|
|
||||||
/////
|
/////
|
||||||
end;
|
end;
|
||||||
|
|||||||
+27
-196
@@ -11,238 +11,69 @@ uses
|
|||||||
Myc.DataRecord;
|
Myc.DataRecord;
|
||||||
|
|
||||||
type
|
type
|
||||||
// The builder implementation class, now in the global scope.
|
TFieldLayoutComparer = class(TComparer<TDataRecord.TField>)
|
||||||
TDataRecordBuilder = class(TInterfacedObject, TDataRecord.TBuilder.IBuilder)
|
|
||||||
private
|
|
||||||
FStagedValues: TList<TPair<string, TValue>>;
|
|
||||||
public
|
|
||||||
constructor Create;
|
|
||||||
destructor Destroy; override;
|
|
||||||
// IBuilder implementation
|
|
||||||
procedure AddField(const Name: String; const Value: TValue); overload;
|
|
||||||
procedure SetupRecord(out Layout: TArray<TDataRecord.TFieldLayout>; out Buffer: TBytes);
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TField = class(TInterfacedObject, TDataRecord.IField)
|
|
||||||
private
|
|
||||||
FBuffer: TBytes;
|
|
||||||
FFieldLayout: TDataRecord.TFieldLayout;
|
|
||||||
protected
|
|
||||||
function GetRawRef: Pointer; inline;
|
|
||||||
public
|
|
||||||
constructor Create(ABuffer: TBytes; const AField: TDataRecord.TFieldLayout);
|
|
||||||
procedure GetRaw(Dst: Pointer); virtual;
|
|
||||||
procedure SetRaw(Src: Pointer); virtual;
|
|
||||||
property FieldLayout: TDataRecord.TFieldLayout read FFieldLayout;
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TGenericField<T> = class(TField, TDataRecord.IField<T>)
|
|
||||||
public
|
|
||||||
function GetValue: T;
|
|
||||||
procedure SetValue(const Value: T);
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TIntegerField = class(TGenericField<Integer>)
|
|
||||||
public
|
|
||||||
procedure GetRaw(Dst: Pointer); override;
|
|
||||||
procedure SetRaw(Src: Pointer); override;
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TSingleField = class(TGenericField<Single>)
|
|
||||||
public
|
|
||||||
procedure GetRaw(Dst: Pointer); override;
|
|
||||||
procedure SetRaw(Src: Pointer); override;
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TDoubleField = class(TGenericField<Double>)
|
|
||||||
public
|
|
||||||
procedure GetRaw(Dst: Pointer); override;
|
|
||||||
procedure SetRaw(Src: Pointer); override;
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TInt64Field = class(TGenericField<Int64>)
|
|
||||||
public
|
|
||||||
procedure GetRaw(Dst: Pointer); override;
|
|
||||||
procedure SetRaw(Src: Pointer); override;
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TStringField = class(TGenericField<String>)
|
|
||||||
public
|
|
||||||
procedure GetRaw(Dst: Pointer); override;
|
|
||||||
procedure SetRaw(Src: Pointer); override;
|
|
||||||
end;
|
|
||||||
|
|
||||||
type
|
|
||||||
TFieldLayoutComparer = class(TComparer<TDataRecord.TFieldLayout>)
|
|
||||||
strict private
|
strict private
|
||||||
class var
|
class var
|
||||||
FDefaultComparer: IComparer<TDataRecord.TFieldLayout>;
|
FDefaultComparer: IComparer<TDataRecord.TField>;
|
||||||
class constructor CreateClass;
|
class constructor CreateClass;
|
||||||
public
|
public
|
||||||
function Compare(const Left, Right: TDataRecord.TFieldLayout): Integer; override;
|
function Compare(const Left, Right: TDataRecord.TField): Integer; override;
|
||||||
class property DefaultComparer: IComparer<TDataRecord.TFieldLayout> read FDefaultComparer;
|
class property DefaultComparer: IComparer<TDataRecord.TField> read FDefaultComparer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
{ TDataRecordBuilder }
|
(*
|
||||||
|
procedure TIntegerField.GetRaw(const [ref] Buffer: TBytes; Dst: Pointer);
|
||||||
constructor TDataRecordBuilder.Create;
|
|
||||||
begin
|
begin
|
||||||
inherited;
|
PInteger(Dst)^ := PInteger(GetRawRef(Buffer))^;
|
||||||
FStagedValues := TList<TPair<string, TValue>>.Create;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
destructor TDataRecordBuilder.Destroy;
|
procedure TIntegerField.SetRaw(const [ref] Buffer: TBytes; Src: Pointer);
|
||||||
begin
|
begin
|
||||||
FStagedValues.Free;
|
PInteger(GetRawRef(Buffer))^ := PInteger(Src)^;
|
||||||
inherited;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataRecordBuilder.AddField(const Name: String; const Value: TValue);
|
procedure TSingleField.GetRaw(const [ref] Buffer: TBytes; Dst: Pointer);
|
||||||
begin
|
begin
|
||||||
FStagedValues.Add(TPair<string, TValue>.Create(Name, Value));
|
PSingle(Dst)^ := PSingle(GetRawRef(Buffer))^;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataRecordBuilder.SetupRecord(out Layout: TArray<TDataRecord.TFieldLayout>; out Buffer: TBytes);
|
procedure TSingleField.SetRaw(const [ref] Buffer: TBytes; Src: Pointer);
|
||||||
var
|
|
||||||
layoutList: TList<TDataRecord.TFieldLayout>;
|
|
||||||
pair: TPair<string, TValue>;
|
|
||||||
field: TDataRecord.TFieldLayout;
|
|
||||||
begin
|
begin
|
||||||
layoutList := TList<TDataRecord.TFieldLayout>.Create;
|
PSingle(GetRawRef(Buffer))^ := PSingle(Src)^;
|
||||||
try
|
|
||||||
layoutList.Capacity := FStagedValues.Count;
|
|
||||||
var ofs: NativeUInt := 0;
|
|
||||||
for pair in FStagedValues do
|
|
||||||
begin
|
|
||||||
field.Name := pair.Key;
|
|
||||||
field.Size := pair.Value.DataSize;
|
|
||||||
ofs := (ofs + 15) and not 15;
|
|
||||||
field.Offset := ofs;
|
|
||||||
inc(ofs, field.Size);
|
|
||||||
field.TypeInfo := pair.Value.TypeInfo;
|
|
||||||
layoutList.Add(field);
|
|
||||||
end;
|
|
||||||
|
|
||||||
SetLength(Buffer, ofs);
|
|
||||||
|
|
||||||
for var i := 0 to layoutList.Count - 1 do
|
|
||||||
begin
|
|
||||||
var P: PByte := @Buffer[layoutList[i].Offset];
|
|
||||||
FStagedValues[i].Value.ExtractRawData(P);
|
|
||||||
end;
|
|
||||||
|
|
||||||
layoutList.Sort(TFieldLayoutComparer.DefaultComparer);
|
|
||||||
Layout := layoutList.ToArray;
|
|
||||||
finally
|
|
||||||
layoutList.Free;
|
|
||||||
end;
|
|
||||||
FStagedValues.Clear;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TField and descendants... }
|
procedure TDoubleField.GetRaw(const [ref] Buffer: TBytes; Dst: Pointer);
|
||||||
constructor TField.Create(ABuffer: TBytes; const AField: TDataRecord.TFieldLayout);
|
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
PDouble(Dst)^ := PDouble(GetRawRef(Buffer))^;
|
||||||
FBuffer := ABuffer;
|
|
||||||
FFieldLayout := AField;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TField.GetRaw(Dst: Pointer);
|
procedure TDoubleField.SetRaw(const [ref] Buffer: TBytes; Src: Pointer);
|
||||||
var
|
|
||||||
Val: TValue;
|
|
||||||
begin
|
begin
|
||||||
TValue.MakeWithoutCopy(GetRawRef, FFieldLayout.TypeInfo, val, true);
|
PDouble(GetRawRef(Buffer))^ := PDouble(Src)^;
|
||||||
val.ExtractRawData(Dst);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TField.GetRawRef: Pointer;
|
procedure TInt64Field.GetRaw(const [ref] Buffer: TBytes; Dst: Pointer);
|
||||||
begin
|
begin
|
||||||
Result := @FBuffer[FFieldLayout.Offset];
|
PInt64(Dst)^ := PInt64(GetRawRef(Buffer))^;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TField.SetRaw(Src: Pointer);
|
procedure TInt64Field.SetRaw(const [ref] Buffer: TBytes; Src: Pointer);
|
||||||
var
|
|
||||||
Val: TValue;
|
|
||||||
begin
|
begin
|
||||||
TValue.MakeWithoutCopy(Src, FFieldLayout.TypeInfo, val, true);
|
PInt64(GetRawRef(Buffer))^ := PInt64(Src)^;
|
||||||
val.ExtractRawData(GetRawRef);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
{ TGenericField<T> }
|
procedure TStringField.GetRaw(const [ref] Buffer: TBytes; Dst: Pointer);
|
||||||
|
|
||||||
function TGenericField<T>.GetValue: T;
|
|
||||||
type
|
|
||||||
PT = ^T;
|
|
||||||
begin
|
begin
|
||||||
Result := PT(@FBuffer[FFieldLayout.Offset])^;
|
PString(Dst)^ := PString(GetRawRef(Buffer))^;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TGenericField<T>.SetValue(const Value: T);
|
procedure TStringField.SetRaw(const [ref] Buffer: TBytes; Src: Pointer);
|
||||||
type
|
|
||||||
PT = ^T;
|
|
||||||
begin
|
begin
|
||||||
PT(@FBuffer[FFieldLayout.Offset])^ := Value;
|
PString(GetRawRef(Buffer))^ := PString(Src)^;
|
||||||
end;
|
end;
|
||||||
|
*)
|
||||||
procedure TIntegerField.GetRaw(Dst: Pointer);
|
|
||||||
begin
|
|
||||||
PInteger(Dst)^ := PInteger(GetRawRef)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TIntegerField.SetRaw(Src: Pointer);
|
|
||||||
begin
|
|
||||||
PInteger(GetRawRef)^ := PInteger(Src)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TSingleField.GetRaw(Dst: Pointer);
|
|
||||||
begin
|
|
||||||
PSingle(Dst)^ := PSingle(GetRawRef)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TSingleField.SetRaw(Src: Pointer);
|
|
||||||
begin
|
|
||||||
PSingle(GetRawRef)^ := PSingle(Src)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDoubleField.GetRaw(Dst: Pointer);
|
|
||||||
begin
|
|
||||||
PDouble(Dst)^ := PDouble(GetRawRef)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDoubleField.SetRaw(Src: Pointer);
|
|
||||||
begin
|
|
||||||
PDouble(GetRawRef)^ := PDouble(Src)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TInt64Field.GetRaw(Dst: Pointer);
|
|
||||||
begin
|
|
||||||
PInt64(Dst)^ := PInt64(GetRawRef)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TInt64Field.SetRaw(Src: Pointer);
|
|
||||||
begin
|
|
||||||
PInt64(GetRawRef)^ := PInt64(Src)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TStringField.GetRaw(Dst: Pointer);
|
|
||||||
begin
|
|
||||||
PString(Dst)^ := PString(GetRawRef)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TStringField.SetRaw(Src: Pointer);
|
|
||||||
begin
|
|
||||||
PString(GetRawRef)^ := PString(Src)^;
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TFieldLayoutComparer }
|
{ TFieldLayoutComparer }
|
||||||
|
|
||||||
class constructor TFieldLayoutComparer.CreateClass;
|
class constructor TFieldLayoutComparer.CreateClass;
|
||||||
@@ -250,7 +81,7 @@ begin
|
|||||||
FDefaultComparer := TFieldLayoutComparer.Create;
|
FDefaultComparer := TFieldLayoutComparer.Create;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TFieldLayoutComparer.Compare(const Left, Right: TDataRecord.TFieldLayout): Integer;
|
function TFieldLayoutComparer.Compare(const Left, Right: TDataRecord.TField): Integer;
|
||||||
begin
|
begin
|
||||||
Result := TComparer<string>.Default.Compare(Left.Name, Right.Name);
|
Result := TComparer<string>.Default.Compare(Left.Name, Right.Name);
|
||||||
end;
|
end;
|
||||||
|
|||||||
+269
-214
@@ -4,280 +4,335 @@ interface
|
|||||||
|
|
||||||
uses
|
uses
|
||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
|
System.Generics.Collections,
|
||||||
System.Rtti,
|
System.Rtti,
|
||||||
System.TypInfo;
|
System.TypInfo;
|
||||||
|
|
||||||
type
|
type
|
||||||
// A record that provides field-level access to managed data, stored in a byte buffer.
|
TDataFieldType = (dfFloat, dfInteger, dfString, dfTimestamp);
|
||||||
|
|
||||||
TDataRecord = record
|
TDataRecord = record
|
||||||
public
|
public
|
||||||
type
|
type
|
||||||
IField = interface
|
TField = record
|
||||||
procedure GetRaw(Dst: Pointer);
|
|
||||||
procedure SetRaw(Src: Pointer);
|
|
||||||
end;
|
|
||||||
|
|
||||||
IField<T> = interface(IField)
|
|
||||||
{$region 'private'}
|
|
||||||
function GetValue: T;
|
|
||||||
procedure SetValue(const Value: T);
|
|
||||||
{$endregion}
|
|
||||||
property Value: T read GetValue write SetValue;
|
|
||||||
end;
|
|
||||||
|
|
||||||
TFieldLayout = record
|
|
||||||
Name: string;
|
|
||||||
Offset: Integer;
|
|
||||||
Size: Integer;
|
|
||||||
TypeInfo: PTypeInfo;
|
|
||||||
end;
|
|
||||||
|
|
||||||
// The interface helper record that provides the generic AddField<T> method.
|
|
||||||
TBuilder = record
|
|
||||||
type
|
|
||||||
IBuilder = interface
|
|
||||||
procedure AddField(const Name: String; const Value: TValue); overload;
|
|
||||||
procedure SetupRecord(out Layout: TArray<TFieldLayout>; out Buffer: TBytes);
|
|
||||||
end;
|
|
||||||
|
|
||||||
private
|
private
|
||||||
FBuilder: IBuilder;
|
FFieldType: TDataFieldType;
|
||||||
|
FName: string;
|
||||||
|
FOffset: Integer;
|
||||||
|
FSize: Integer;
|
||||||
|
|
||||||
|
procedure Write(SrcType: PTypeInfo; const Src; P: Pointer);
|
||||||
|
procedure Read(Src: Pointer; DstType: PTypeInfo; var Dst);
|
||||||
|
procedure Finalize(P: Pointer);
|
||||||
|
property Offset: Integer read FOffset;
|
||||||
|
property Size: Integer read FSize;
|
||||||
|
|
||||||
public
|
public
|
||||||
constructor Create(const ABuilder: IBuilder);
|
constructor Create(const AName: string; AFieldType: TDataFieldType; AOffset, ASize: Integer);
|
||||||
|
|
||||||
class operator Implicit(const AValue: IBuilder): TBuilder; overload;
|
class function From<T>(const AName: string; Offset: Integer): TField; overload; static;
|
||||||
class operator Implicit(const AValue: TBuilder): IBuilder; overload;
|
class function From(const AName: string; const RttiType: TRttiType; Offset: Integer): TField; overload; static;
|
||||||
|
|
||||||
procedure AddField<T>(const Name: String); overload; inline;
|
property FieldType: TDataFieldType read FFieldType;
|
||||||
procedure AddField<T>(const Name: String; const Value: T); overload; inline;
|
property Name: string read FName;
|
||||||
|
end;
|
||||||
|
|
||||||
function CreateRec: TDataRecord; inline;
|
TLayout = record
|
||||||
|
private
|
||||||
|
FFields: TArray<TField>;
|
||||||
|
public
|
||||||
|
constructor Create(const AFields: TArray<TField>);
|
||||||
|
class function CreateFrom<T>: TLayout; static;
|
||||||
|
function GetBufferSize: Integer;
|
||||||
|
function IndexOf(const Name: String): Integer;
|
||||||
|
property Fields: TArray<TField> read FFields;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
private
|
private
|
||||||
FLayout: TArray<TFieldLayout>;
|
FLayout: TLayout;
|
||||||
FBuffer: TBytes;
|
FBuffer: TBytes;
|
||||||
|
|
||||||
public
|
public
|
||||||
class operator Initialize(out Dest: TDataRecord);
|
constructor Create(const ALayout: TLayout; const ABuffer: TBytes);
|
||||||
|
|
||||||
|
class function CreateFrom<T>: TDataRecord; overload; static;
|
||||||
|
class function CreateFrom<T>(const Src: T): TDataRecord; overload; static;
|
||||||
|
class function CreateFrom(const Layout: TLayout): TDataRecord; overload; static;
|
||||||
|
procedure CopyFrom<T>(const Src: T);
|
||||||
|
|
||||||
|
procedure SetValue<T>(const Name: String; const Value: T); overload;
|
||||||
|
procedure SetValue(Idx: Integer; const Value); overload;
|
||||||
|
|
||||||
|
function GetValue<T>(const Name: String): T; overload;
|
||||||
|
procedure GetValue(Idx: Integer; out Value); overload;
|
||||||
|
|
||||||
class operator Finalize(var Dest: TDataRecord);
|
class operator Finalize(var Dest: TDataRecord);
|
||||||
class operator Assign(var Dest: TDataRecord; const [ref] Src: TDataRecord);
|
|
||||||
|
|
||||||
class function CreateBuilder: TBuilder; static;
|
property Layout: TLayout read FLayout;
|
||||||
|
|
||||||
class function CreateFrom<T>(const Rec: T): TDataRecord; static;
|
|
||||||
procedure AssignTo<T>(var Rec: T);
|
|
||||||
function FitsRecord<T>: Boolean;
|
|
||||||
|
|
||||||
function IndexOf(const Name: String): Integer;
|
|
||||||
function GetField(const Name: String): IField; overload;
|
|
||||||
function GetField<T>(const Name: String): IField<T>; overload;
|
|
||||||
|
|
||||||
property Layout: TArray<TFieldLayout> read FLayout;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
System.Classes,
|
System.Classes,
|
||||||
System.Generics.Collections,
|
|
||||||
Myc.DataRecord.Impl;
|
Myc.DataRecord.Impl;
|
||||||
|
|
||||||
{ TDataRecord.TBuilder }
|
constructor TDataRecord.Create(const ALayout: TLayout; const ABuffer: TBytes);
|
||||||
|
|
||||||
constructor TDataRecord.TBuilder.Create(const ABuilder: IBuilder);
|
|
||||||
begin
|
begin
|
||||||
FBuilder := ABuilder;
|
FLayout := ALayout;
|
||||||
|
FBuffer := ABuffer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataRecord.TBuilder.AddField<T>(const Name: String; const Value: T);
|
procedure TDataRecord.CopyFrom<T>(const Src: T);
|
||||||
begin
|
begin
|
||||||
FBuilder.AddField(Name, TValue.From<T>(Value));
|
var ctx := TRttiContext.Create;
|
||||||
end;
|
var rttiType := ctx.GetType(TypeInfo(T));
|
||||||
|
var rttiFields := rttiType.GetFields;
|
||||||
|
var fields: TArray<TField>;
|
||||||
|
|
||||||
procedure TDataRecord.TBuilder.AddField<T>(const Name: String);
|
for var i := 0 to High(rttiFields) do
|
||||||
begin
|
|
||||||
AddField<T>(Name, Default(T));
|
|
||||||
end;
|
|
||||||
|
|
||||||
function TDataRecord.TBuilder.CreateRec: TDataRecord;
|
|
||||||
begin
|
|
||||||
FBuilder.SetupRecord(Result.FLayout, Result.FBuffer);
|
|
||||||
end;
|
|
||||||
|
|
||||||
class operator TDataRecord.TBuilder.Implicit(const AValue: IBuilder): TBuilder;
|
|
||||||
begin
|
|
||||||
Result.FBuilder := AValue;
|
|
||||||
end;
|
|
||||||
|
|
||||||
class operator TDataRecord.TBuilder.Implicit(const AValue: TBuilder): IBuilder;
|
|
||||||
begin
|
|
||||||
Result := AValue.FBuilder;
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TDataRecord }
|
|
||||||
|
|
||||||
function TDataRecord.GetField<T>(const Name: String): IField<T>;
|
|
||||||
var
|
|
||||||
idx: Integer;
|
|
||||||
begin
|
|
||||||
idx := IndexOf(Name);
|
|
||||||
if idx < 0 then
|
|
||||||
exit;
|
|
||||||
if TypeInfo(T) <> FLayout[idx].TypeInfo then
|
|
||||||
exit;
|
|
||||||
Result := TGenericField<T>.Create(FBuffer, FLayout[idx]);
|
|
||||||
end;
|
|
||||||
|
|
||||||
class function TDataRecord.CreateBuilder: TBuilder;
|
|
||||||
begin
|
|
||||||
// Create the implementation class and wrap it in the helper record.
|
|
||||||
Result := TDataRecord.TBuilder.Create(TDataRecordBuilder.Create);
|
|
||||||
end;
|
|
||||||
|
|
||||||
class function TDataRecord.CreateFrom<T>(const Rec: T): TDataRecord;
|
|
||||||
var
|
|
||||||
ctx: TRttiContext;
|
|
||||||
rttiType: TRttiType;
|
|
||||||
builder: TBuilder.IBuilder;
|
|
||||||
begin
|
|
||||||
// Create a builder to construct the TDataRecord
|
|
||||||
builder := CreateBuilder;
|
|
||||||
|
|
||||||
// Use RTTI to iterate over the fields of the source record/object
|
|
||||||
ctx := TRttiContext.Create;
|
|
||||||
rttiType := ctx.GetType(TypeInfo(T));
|
|
||||||
|
|
||||||
// Add each field and its value to the builder
|
|
||||||
for var field in rttiType.GetFields do
|
|
||||||
begin
|
begin
|
||||||
builder.AddField(field.Name, field.GetValue(Pointer(@Rec)));
|
var rf := rttiFields[i];
|
||||||
end;
|
var idx := FLayout.IndexOf(rf.Name);
|
||||||
|
|
||||||
// Create the final TDataRecord from the builder
|
if (idx >= 0) then
|
||||||
builder.SetupRecord(Result.FLayout, Result.FBuffer);
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecord.AssignTo<T>(var Rec: T);
|
|
||||||
var
|
|
||||||
ctx: TRttiContext;
|
|
||||||
rttiType: TRttiType;
|
|
||||||
fieldValue: TValue;
|
|
||||||
fieldIndex: Integer;
|
|
||||||
layout: TFieldLayout;
|
|
||||||
begin
|
|
||||||
Assert(FitsRecord<T>);
|
|
||||||
|
|
||||||
// Use RTTI to iterate over the fields of the destination record/object
|
|
||||||
ctx := TRttiContext.Create;
|
|
||||||
rttiType := ctx.GetType(TypeInfo(T));
|
|
||||||
|
|
||||||
for var rttiField in rttiType.GetFields do
|
|
||||||
begin
|
|
||||||
// Find the corresponding field in the TDataRecord by name
|
|
||||||
fieldIndex := IndexOf(rttiField.Name);
|
|
||||||
if fieldIndex >= 0 then
|
|
||||||
begin
|
begin
|
||||||
layout := FLayout[fieldIndex];
|
var fld := FLayout.Fields[idx];
|
||||||
// Create a TValue from the data in our buffer
|
|
||||||
TValue.Make(@FBuffer[layout.Offset], layout.TypeInfo, fieldValue);
|
var S: PByte := @Src;
|
||||||
// Assign the value to the destination record's field
|
inc(S, rf.Offset);
|
||||||
rttiField.SetValue(Pointer(@Rec), fieldValue);
|
var P := @FBuffer[FLayout.Fields[idx].Offset];
|
||||||
|
|
||||||
|
fld.Write(rf.FieldType.Handle, S^, P);
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataRecord.FitsRecord<T>: Boolean;
|
class function TDataRecord.CreateFrom(const Layout: TLayout): TDataRecord;
|
||||||
begin
|
begin
|
||||||
var ctx := TRttiContext.Create;
|
var buf: TBytes;
|
||||||
var rttiType := ctx.GetType(TypeInfo(T));
|
SetLength(buf, Layout.GetBufferSize);
|
||||||
|
Result.Create(Layout, buf);
|
||||||
for var rttiField in rttiType.GetFields do
|
|
||||||
begin
|
|
||||||
// Find the corresponding field in the TDataRecord by name
|
|
||||||
var fieldIndex := IndexOf(rttiField.Name);
|
|
||||||
if fieldIndex < 0 then
|
|
||||||
exit(false);
|
|
||||||
|
|
||||||
if rttiField.DataType.Handle <> FLayout[fieldIndex].TypeInfo then
|
|
||||||
exit(false);
|
|
||||||
end;
|
|
||||||
exit(true);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataRecord.GetField(const Name: String): IField;
|
{ TDataRecord }
|
||||||
var
|
|
||||||
idx: Integer;
|
class function TDataRecord.CreateFrom<T>: TDataRecord;
|
||||||
begin
|
begin
|
||||||
idx := IndexOf(Name);
|
Result := CreateFrom(TLayout.CreateFrom<T>);
|
||||||
if idx < 0 then
|
|
||||||
exit;
|
|
||||||
var typeInfo := FLayout[idx].TypeInfo;
|
|
||||||
case typeInfo.Kind of
|
|
||||||
tkInteger: Result := TIntegerField.Create(FBuffer, FLayout[idx]);
|
|
||||||
tkFloat:
|
|
||||||
case GetTypeData(typeInfo).FloatType of
|
|
||||||
ftSingle: Result := TSingleField.Create(FBuffer, FLayout[idx]);
|
|
||||||
ftDouble: Result := TDoubleField.Create(FBuffer, FLayout[idx]);
|
|
||||||
end;
|
|
||||||
tkInt64: Result := TInt64Field.Create(FBuffer, FLayout[idx]);
|
|
||||||
tkString: Result := TStringField.Create(FBuffer, FLayout[idx]);
|
|
||||||
end;
|
|
||||||
if not Assigned(Result) then
|
|
||||||
Result := TField.Create(FBuffer, FLayout[idx]);
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
function TDataRecord.IndexOf(const Name: String): Integer;
|
class function TDataRecord.CreateFrom<T>(const Src: T): TDataRecord;
|
||||||
var
|
|
||||||
dummyLayout: TFieldLayout;
|
|
||||||
begin
|
begin
|
||||||
dummyLayout.Name := Name;
|
Result := CreateFrom<T>;
|
||||||
if not TArray.BinarySearch<TFieldLayout>(FLayout, dummyLayout, Result, TFieldLayoutComparer.DefaultComparer) then
|
Result.CopyFrom<T>(Src);
|
||||||
Result := -1;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
class operator TDataRecord.Assign(var Dest: TDataRecord; const [ref] Src: TDataRecord);
|
procedure TDataRecord.GetValue(Idx: Integer; out Value);
|
||||||
begin
|
begin
|
||||||
Finalize(Dest);
|
var P := @FBuffer[FLayout.Fields[Idx].Offset];
|
||||||
|
case FLayout.Fields[Idx].FieldType of
|
||||||
Dest.FLayout := Src.FLayout;
|
dfFloat: Double(Value) := PDouble(P)^;
|
||||||
SetLength(Dest.FBuffer, Length(Src.FBuffer));
|
dfInteger: Int64(Value) := PInt64(Value)^;
|
||||||
for var i := 0 to High(Dest.FLayout) do
|
dfString: String(Value) := PString(Value)^;
|
||||||
begin
|
dfTimestamp: TDateTime(Value) := PDateTime(Value)^;
|
||||||
Assert(Dest.FLayout[i].Name = Src.Layout[i].Name);
|
else
|
||||||
Assert(Dest.FLayout[i].Offset = Src.Layout[i].Offset);
|
Assert(false);
|
||||||
Assert(Dest.FLayout[i].Size = Src.Layout[i].Size);
|
|
||||||
Assert(Dest.FLayout[i].TypeInfo = Src.Layout[i].TypeInfo);
|
|
||||||
|
|
||||||
var ofs := Dest.FLayout[i].Offset;
|
|
||||||
var P: PByte := @Dest.FBuffer[ofs];
|
|
||||||
var Q: PByte := @Src.FBuffer[ofs];
|
|
||||||
var Val: TValue;
|
|
||||||
TValue.Make(Q, Dest.FLayout[i].TypeInfo, val);
|
|
||||||
val.ExtractRawData(P);
|
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
class operator TDataRecord.Initialize(out Dest: TDataRecord);
|
procedure TDataRecord.SetValue(Idx: Integer; const Value);
|
||||||
begin
|
begin
|
||||||
Dest.FLayout := nil;
|
var P := @FBuffer[FLayout.Fields[Idx].Offset];
|
||||||
Dest.FBuffer := nil;
|
case FLayout.Fields[Idx].FieldType of
|
||||||
|
dfFloat: PDouble(P)^ := Double(Value);
|
||||||
|
dfInteger: PInt64(P)^ := Int64(Value);
|
||||||
|
dfString: PString(P)^ := String(Value);
|
||||||
|
dfTimestamp: PDateTime(P)^ := TDateTime(Value);
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDataRecord.GetValue<T>(const Name: String): T;
|
||||||
|
begin
|
||||||
|
var idx := FLayout.IndexOf(Name);
|
||||||
|
if (idx >= 0) then
|
||||||
|
FLayout.Fields[idx].Read(@FBuffer[FLayout.Fields[idx].Offset], TypeInfo(T), Result);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDataRecord.SetValue<T>(const Name: String; const Value: T);
|
||||||
|
begin
|
||||||
|
var idx := FLayout.IndexOf(Name);
|
||||||
|
if (idx >= 0) then
|
||||||
|
FLayout.Fields[idx].Write(TypeInfo(T), Value, @FBuffer[FLayout.Fields[idx].Offset]);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
class operator TDataRecord.Finalize(var Dest: TDataRecord);
|
class operator TDataRecord.Finalize(var Dest: TDataRecord);
|
||||||
begin
|
begin
|
||||||
if (Dest.FBuffer = nil) or (Dest.FLayout = nil) then
|
for var i := 0 to High(Dest.FLayout.Fields) do
|
||||||
Exit;
|
Dest.FLayout.Fields[i].Finalize(@Dest.FBuffer[Dest.FLayout.Fields[i].Offset]);
|
||||||
|
end;
|
||||||
|
|
||||||
for var layout in Dest.FLayout do
|
constructor TDataRecord.TField.Create(const AName: string; AFieldType: TDataFieldType; AOffset, ASize: Integer);
|
||||||
begin
|
begin
|
||||||
var P: PByte := @Dest.FBuffer[layout.Offset];
|
FName := AName;
|
||||||
var Val: TValue;
|
FFieldType := AFieldType;
|
||||||
// IsMoved has to be false, because we want the local TValue to own the field and destroy it when it looses scope!
|
FOffset := AOffset;
|
||||||
TValue.MakeWithoutCopy(P, layout.TypeInfo, val, false);
|
FSize := ASize;
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDataRecord.TField.Finalize(P: Pointer);
|
||||||
|
begin
|
||||||
|
case FFieldType of
|
||||||
|
dfFloat: PDouble(P)^ := 0;
|
||||||
|
dfInteger: PInt64(P)^ := 0;
|
||||||
|
dfString: PString(P)^ := '';
|
||||||
|
dfTimestamp: PDateTime(P)^ := 0;
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
end;
|
end;
|
||||||
Dest.FLayout := nil;
|
end;
|
||||||
Dest.FBuffer := nil;
|
|
||||||
|
procedure TDataRecord.TField.Write(SrcType: PTypeInfo; const Src; P: Pointer);
|
||||||
|
begin
|
||||||
|
case FFieldType of
|
||||||
|
dfFloat:
|
||||||
|
begin
|
||||||
|
Assert(SrcType.Kind = tkFloat);
|
||||||
|
case GetTypeData(SrcType).FloatType of
|
||||||
|
ftSingle: PDouble(P)^ := PSingle(@Src)^;
|
||||||
|
ftDouble: PDouble(P)^ := PDouble(@Src)^;
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
dfInteger:
|
||||||
|
begin
|
||||||
|
case SrcType.Kind of
|
||||||
|
tkInteger: PInt64(P)^ := PInteger(@Src)^;
|
||||||
|
tkInt64: PInt64(P)^ := PInt64(@Src)^;
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
dfString:
|
||||||
|
begin
|
||||||
|
Assert(SrcType.Kind in [tkString, tkLString, tkUString, tkWString]);
|
||||||
|
PString(P)^ := PString(@Src)^;
|
||||||
|
end;
|
||||||
|
dfTimestamp:
|
||||||
|
begin
|
||||||
|
Assert(SrcType.Kind = tkFloat);
|
||||||
|
PDateTime(P)^ := PDateTime(@Src)^;
|
||||||
|
end;
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TDataRecord.TField.From(const AName: string; const RttiType: TRttiType; Offset: Integer): TField;
|
||||||
|
begin
|
||||||
|
case rttiType.TypeKind of
|
||||||
|
tkInteger, tkInt64: Result.Create(AName, dfInteger, Offset, sizeof(Int64));
|
||||||
|
tkFloat:
|
||||||
|
if (rttiType.Name = 'TDateTime') then
|
||||||
|
Result.Create(AName, dfTimestamp, Offset, sizeof(TDateTime))
|
||||||
|
else
|
||||||
|
Result.Create(AName, dfFloat, Offset, sizeof(Double));
|
||||||
|
tkString, tkWString, tkLString, tkUString: Result.Create(AName, dfString, Offset, sizeof(String));
|
||||||
|
else
|
||||||
|
raise Exception.Create('Type not supported');
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TDataRecord.TField.From<T>(const AName: string; Offset: Integer): TField;
|
||||||
|
begin
|
||||||
|
var ctx := TRttiContext.Create;
|
||||||
|
Result := From(AName, ctx.GetType(TypeInfo(T)), Offset);
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure TDataRecord.TField.Read(Src: Pointer; DstType: PTypeInfo; var Dst);
|
||||||
|
begin
|
||||||
|
case FFieldType of
|
||||||
|
dfFloat:
|
||||||
|
begin
|
||||||
|
Assert(DstType.Kind = tkFloat);
|
||||||
|
case GetTypeData(DstType).FloatType of
|
||||||
|
ftSingle: PSingle(@Dst)^ := PDouble(Src)^;
|
||||||
|
ftDouble: PDouble(@Dst)^ := PDouble(Src)^;
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
dfInteger:
|
||||||
|
begin
|
||||||
|
case DstType.Kind of
|
||||||
|
tkInteger: PInteger(@Dst)^ := PInt64(Src)^;
|
||||||
|
tkInt64: PInt64(@Dst)^ := PInt64(Src)^;
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
dfString:
|
||||||
|
begin
|
||||||
|
Assert(DstType.Kind in [tkString, tkLString, tkUString, tkWString]);
|
||||||
|
PString(@Dst)^ := PString(Src)^;
|
||||||
|
end;
|
||||||
|
dfTimestamp:
|
||||||
|
begin
|
||||||
|
Assert(DstType.Kind = tkFloat);
|
||||||
|
PDateTime(@Dst)^ := PDateTime(Src)^;
|
||||||
|
end;
|
||||||
|
else
|
||||||
|
Assert(false);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
constructor TDataRecord.TLayout.Create(const AFields: TArray<TField>);
|
||||||
|
begin
|
||||||
|
FFields := AFields;
|
||||||
|
end;
|
||||||
|
|
||||||
|
{ TLayout }
|
||||||
|
|
||||||
|
class function TDataRecord.TLayout.CreateFrom<T>: TLayout;
|
||||||
|
begin
|
||||||
|
var ctx := TRttiContext.Create;
|
||||||
|
var rttiType := ctx.GetType(TypeInfo(T));
|
||||||
|
var rttiFields := rttiType.GetFields;
|
||||||
|
var fields: TArray<TField>;
|
||||||
|
|
||||||
|
// Add each field and its value to the builder
|
||||||
|
var ofs := 0;
|
||||||
|
SetLength(fields, Length(rttiFields));
|
||||||
|
for var i := 0 to High(rttiFields) do
|
||||||
|
begin
|
||||||
|
var rf := rttiFields[i];
|
||||||
|
fields[i] := TField.From(rf.Name, rf.FieldType, ofs);
|
||||||
|
inc(ofs, fields[i].Size);
|
||||||
|
ofs := (ofs + 7) and not 7;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Result.Create(fields);
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDataRecord.TLayout.GetBufferSize: Integer;
|
||||||
|
begin
|
||||||
|
if (Length(FFields) = 0) then
|
||||||
|
exit(0);
|
||||||
|
|
||||||
|
var n := High(FFields);
|
||||||
|
Result := FFields[n].FOffset + FFields[n].Size;
|
||||||
|
Result := (Result + 7) and not 7;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TDataRecord.TLayout.IndexOf(const Name: String): Integer;
|
||||||
|
begin
|
||||||
|
for var i := 0 to High(FFields) do
|
||||||
|
if (FFields[i].Name = Name) then
|
||||||
|
exit(i);
|
||||||
|
exit(-1);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ interface
|
|||||||
uses
|
uses
|
||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
System.SyncObjs,
|
System.SyncObjs,
|
||||||
|
System.Generics.Collections,
|
||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Mutable,
|
Myc.Mutable,
|
||||||
Myc.Core.Notifier,
|
Myc.Core.Notifier,
|
||||||
@@ -147,13 +148,11 @@ type
|
|||||||
|
|
||||||
// A processor implementation that is owned by a controller.
|
// A processor implementation that is owned by a controller.
|
||||||
TMycContainedProcessor<T> = class(TContainedObject, IMycProcessor<T>)
|
TMycContainedProcessor<T> = class(TContainedObject, IMycProcessor<T>)
|
||||||
type
|
|
||||||
TProc = function(const Value: T): TState of object;
|
|
||||||
private
|
private
|
||||||
FProc: TProc;
|
FProc: TConstFunc<T, TState>;
|
||||||
function ProcessData(const Value: T): TState;
|
function ProcessData(const Value: T): TState;
|
||||||
public
|
public
|
||||||
constructor Create(const Controller: IInterface; const AProc: TProc);
|
constructor Create(const Controller: IInterface; const AProc: TConstFunc<T, TState>);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
// Endpoint that collects data into a series.
|
// Endpoint that collects data into a series.
|
||||||
@@ -216,6 +215,33 @@ type
|
|||||||
property Timeframe: TTimeframe read GetTimeframe;
|
property Timeframe: TTimeframe read GetTimeframe;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// Endpoint that collects data into a series.
|
||||||
|
TMycDataJoin<T> = class(TInterfacedObject, TDataProvider<TArray<T>>.IDataProvider)
|
||||||
|
type
|
||||||
|
PItem = ^TItem;
|
||||||
|
TItem = record
|
||||||
|
Next: PItem;
|
||||||
|
Value: T;
|
||||||
|
end;
|
||||||
|
|
||||||
|
private
|
||||||
|
FReceivers: array of record
|
||||||
|
DataProvider: TDataProvider<T>;
|
||||||
|
Processor: TMycContainedProcessor<T>;
|
||||||
|
Tag: TDataProvider<T>.TTag;
|
||||||
|
Queue: TQueue<T>;
|
||||||
|
end;
|
||||||
|
|
||||||
|
FSender: TMycContainedDataProvider<TArray<T>>;
|
||||||
|
FLock: TSpinLock;
|
||||||
|
FCount: Integer;
|
||||||
|
function ProcessData(Idx: Integer; const Value: T): TState;
|
||||||
|
public
|
||||||
|
constructor Create(const ADataProviders: TArray<TDataProvider<T>>);
|
||||||
|
destructor Destroy; override;
|
||||||
|
property Sender: TMycContainedDataProvider<TArray<T>> read FSender implements TDataProvider<TArray<T>>.IDataProvider;
|
||||||
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
@@ -401,12 +427,6 @@ begin
|
|||||||
var TypeT := Context.GetType(TypeInfo(T));
|
var TypeT := Context.GetType(TypeInfo(T));
|
||||||
|
|
||||||
var Fields := Context.GetType(TypeInfo(S)).GetFields;
|
var Fields := Context.GetType(TypeInfo(S)).GetFields;
|
||||||
var name := '';
|
|
||||||
if AFieldName = 'Time' then
|
|
||||||
for var i := 0 to High(Fields) do
|
|
||||||
begin
|
|
||||||
name := name + ' ' + Fields[i].Name;
|
|
||||||
end;
|
|
||||||
|
|
||||||
Assert(Assigned(Field), 'Field ' + AFieldName + ' not found');
|
Assert(Assigned(Field), 'Field ' + AFieldName + ' not found');
|
||||||
Assert(Field.FieldType.TypeKind = TypeT.TypeKind, 'Incorrect type');
|
Assert(Field.FieldType.TypeKind = TypeT.TypeKind, 'Incorrect type');
|
||||||
@@ -444,7 +464,7 @@ end;
|
|||||||
|
|
||||||
{ TMycContainedProcessor<T> }
|
{ TMycContainedProcessor<T> }
|
||||||
|
|
||||||
constructor TMycContainedProcessor<T>.Create(const Controller: IInterface; const AProc: TProc);
|
constructor TMycContainedProcessor<T>.Create(const Controller: IInterface; const AProc: TConstFunc<T, TState>);
|
||||||
begin
|
begin
|
||||||
inherited Create(Controller);
|
inherited Create(Controller);
|
||||||
FProc := AProc;
|
FProc := AProc;
|
||||||
@@ -803,4 +823,79 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ TMycDataJoin<T> }
|
||||||
|
|
||||||
|
constructor TMycDataJoin<T>.Create(const ADataProviders: TArray<TDataProvider<T>>);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
|
||||||
|
SetLength(FReceivers, Length(ADataProviders));
|
||||||
|
|
||||||
|
FLock := TSpinLock.Create(false);
|
||||||
|
FCount := Length(FReceivers);
|
||||||
|
|
||||||
|
FSender := TMycContainedDataProvider<TArray<T>>.Create(Self);
|
||||||
|
|
||||||
|
var cFunc :=
|
||||||
|
function(Idx: Integer): TConstFunc<T, TState>
|
||||||
|
begin
|
||||||
|
Result := function(const Value: T): TState begin ProcessData(Idx, Value); end
|
||||||
|
end;
|
||||||
|
|
||||||
|
for var i := 0 to High(FReceivers) do
|
||||||
|
with FReceivers[i] do
|
||||||
|
begin
|
||||||
|
Queue := TQueue<T>.Create;
|
||||||
|
DataProvider := ADataProviders[i];
|
||||||
|
Processor := TMycContainedProcessor<T>.Create(Self, cFunc(i));
|
||||||
|
Tag := DataProvider.Link(Processor);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
destructor TMycDataJoin<T>.Destroy;
|
||||||
|
begin
|
||||||
|
for var i := High(FReceivers) downto 0 do
|
||||||
|
with FReceivers[i] do
|
||||||
|
begin
|
||||||
|
DataProvider.Unlink(FReceivers[i].Tag);
|
||||||
|
Processor.Free;
|
||||||
|
Queue.Free;
|
||||||
|
end;
|
||||||
|
|
||||||
|
FSender.Free;
|
||||||
|
inherited;
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TMycDataJoin<T>.ProcessData(Idx: Integer; const Value: T): TState;
|
||||||
|
begin
|
||||||
|
var Arr: TArray<T>;
|
||||||
|
|
||||||
|
FLock.Enter;
|
||||||
|
try
|
||||||
|
if FReceivers[Idx].Queue.Count = 0 then
|
||||||
|
dec(FCount);
|
||||||
|
|
||||||
|
if FCount = 0 then
|
||||||
|
begin
|
||||||
|
SetLength(Arr, Length(FReceivers));
|
||||||
|
Arr[Idx] := Value;
|
||||||
|
FCount := Length(FReceivers);
|
||||||
|
for var i := 0 to High(FReceivers) do
|
||||||
|
if i <> Idx then
|
||||||
|
begin
|
||||||
|
Arr[i] := FReceivers[i].Queue.Dequeue;
|
||||||
|
if FReceivers[i].Queue.Count > 0 then
|
||||||
|
dec(FCount);
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
FReceivers[Idx].Queue.Enqueue(Value);
|
||||||
|
finally
|
||||||
|
FLock.Exit;
|
||||||
|
end;
|
||||||
|
|
||||||
|
if Arr <> nil then
|
||||||
|
FSender.Broadcast(Arr);
|
||||||
|
end;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ uses
|
|||||||
Myc.Signals,
|
Myc.Signals,
|
||||||
Myc.Mutable,
|
Myc.Mutable,
|
||||||
Myc.Trade.Types,
|
Myc.Trade.Types,
|
||||||
Myc.Trade.DataArray;
|
Myc.Trade.DataArray,
|
||||||
|
Myc.DataRecord;
|
||||||
|
|
||||||
type
|
type
|
||||||
// A generic interface for components that process data of type T.
|
// A generic interface for components that process data of type T.
|
||||||
@@ -119,6 +120,24 @@ type
|
|||||||
class function CreateSequence<T>(Count: Integer; const Parent: TDataProvider<T>): TArray<TConverter<T, T>>; overload; static;
|
class function CreateSequence<T>(Count: Integer; const Parent: TDataProvider<T>): TArray<TConverter<T, T>>; overload; static;
|
||||||
|
|
||||||
class function Parallel<T>(Parent: TDataProvider<T>): TConverter<T, T>; static;
|
class function Parallel<T>(Parent: TDataProvider<T>): TConverter<T, T>; static;
|
||||||
|
|
||||||
|
class function FieldToRecord<T>(const Layout: TDataRecord.TLayout; const Name: String): TConverter<T, TDataRecord>; static;
|
||||||
|
class function FieldOfRecord<T>(const Layout: TDataRecord.TLayout; const Name: String): TConverter<TDataRecord, T>; static;
|
||||||
|
|
||||||
|
type
|
||||||
|
TRecordMapping = record
|
||||||
|
Layout: TDataRecord.TLayout;
|
||||||
|
Fields: array of record
|
||||||
|
FromField, ToField: String
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function DataMapping(
|
||||||
|
const Inputs: TArray<TRecordMapping>;
|
||||||
|
const Output: TDataRecord.TLayout
|
||||||
|
): TConverter<TArray<TDataRecord>, TDataRecord>; static;
|
||||||
|
|
||||||
|
class function Join<T>(const DataProviders: TArray<TDataProvider<T>>): TDataProvider<TArray<T>>; static;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
@@ -315,6 +334,69 @@ begin
|
|||||||
Result := TOhlcAggregation.Create(Timeframe);
|
Result := TOhlcAggregation.Create(Timeframe);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
class function TConverter.DataMapping(
|
||||||
|
const Inputs: TArray<TRecordMapping>;
|
||||||
|
const Output: TDataRecord.TLayout
|
||||||
|
): TConverter<TArray<TDataRecord>, TDataRecord>;
|
||||||
|
var
|
||||||
|
Idxs: array of array of record
|
||||||
|
FromIdx, ToIdx: Integer
|
||||||
|
end;
|
||||||
|
begin
|
||||||
|
SetLength(Idxs, Length(Inputs));
|
||||||
|
for var i := 0 to High(Idxs) do
|
||||||
|
begin
|
||||||
|
SetLength(Idxs[i], Length(Inputs[i].Fields));
|
||||||
|
for var j := 0 to High(Idxs[i]) do
|
||||||
|
begin
|
||||||
|
Idxs[i][j].FromIdx := Inputs[i].Layout.IndexOf(Inputs[i].Fields[j].FromField);
|
||||||
|
Idxs[i][j].ToIdx := Output.IndexOf(Inputs[i].Fields[j].ToField);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
Result :=
|
||||||
|
TConverter<TArray<TDataRecord>, TDataRecord>.CreateGeneric(
|
||||||
|
function(const Inputs: TArray<TDataRecord>): TDataRecord
|
||||||
|
var
|
||||||
|
tmp: array[0..63] of Byte;
|
||||||
|
begin
|
||||||
|
Result := TDataRecord.CreateFrom(Output);
|
||||||
|
for var i := 0 to High(Idxs) do
|
||||||
|
begin
|
||||||
|
for var j := 0 to High(Idxs[i]) do
|
||||||
|
begin
|
||||||
|
Inputs[i].GetValue(Idxs[i][j].FromIdx, tmp);
|
||||||
|
Result.SetValue(Idxs[i][j].ToIdx, tmp);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TConverter.FieldOfRecord<T>(const Layout: TDataRecord.TLayout; const Name: String): TConverter<TDataRecord, T>;
|
||||||
|
begin
|
||||||
|
var idx := Layout.IndexOf(Name);
|
||||||
|
Result := TConverter<TDataRecord, T>.CreateGeneric(function(const Value: TDataRecord): T begin Value.GetValue(idx, Result); end);
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TConverter.FieldToRecord<T>(const Layout: TDataRecord.TLayout; const Name: String): TConverter<T, TDataRecord>;
|
||||||
|
begin
|
||||||
|
var idx := Layout.IndexOf(Name);
|
||||||
|
Result :=
|
||||||
|
TConverter<T, TDataRecord>.CreateGeneric(
|
||||||
|
function(const Value: T): TDataRecord
|
||||||
|
begin
|
||||||
|
Result := TDataRecord.CreateFrom(Layout);
|
||||||
|
Result.SetValue(idx, Value);
|
||||||
|
end
|
||||||
|
);
|
||||||
|
end;
|
||||||
|
|
||||||
|
class function TConverter.Join<T>(const DataProviders: TArray<TDataProvider<T>>): TDataProvider<TArray<T>>;
|
||||||
|
begin
|
||||||
|
Result := TMycDataJoin<T>.Create(DataProviders);
|
||||||
|
end;
|
||||||
|
|
||||||
class function TConverter.Parallel<T>(Parent: TDataProvider<T>): TConverter<T, T>;
|
class function TConverter.Parallel<T>(Parent: TDataProvider<T>): TConverter<T, T>;
|
||||||
begin
|
begin
|
||||||
Result := TMycParallelConverter<T>.Create;
|
Result := TMycParallelConverter<T>.Create;
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ interface
|
|||||||
uses
|
uses
|
||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
System.Math,
|
System.Math,
|
||||||
|
System.Generics.Collections,
|
||||||
|
System.Rtti,
|
||||||
|
Myc.DataRecord,
|
||||||
Myc.Trade.Types,
|
Myc.Trade.Types,
|
||||||
Myc.Trade.DataArray;
|
Myc.Trade.DataArray;
|
||||||
|
|
||||||
@@ -78,8 +81,72 @@ type
|
|||||||
const AtrFunc: TConstFunc<TOhlcItem, Double>;
|
const AtrFunc: TConstFunc<TOhlcItem, Double>;
|
||||||
Multiplier: Double
|
Multiplier: Double
|
||||||
): TConstFunc<TOhlcItem, TKeltnerChannelsResult>; overload; static;
|
): TConstFunc<TOhlcItem, TKeltnerChannelsResult>; overload; static;
|
||||||
|
|
||||||
|
class function CreateMean: TConstFunc<TArray<Double>, Double>; static;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
TIndicatorFactory = class
|
||||||
|
type
|
||||||
|
TFunc = TConstFunc<TDataRecord, TDataRecord>;
|
||||||
|
private
|
||||||
|
FParams: TDataRecord.TLayout;
|
||||||
|
FInput: TDataRecord.TLayout;
|
||||||
|
FOutput: TDataRecord.TLayout;
|
||||||
|
|
||||||
|
public
|
||||||
|
constructor Create(const AParams, AInput, AOutput: TDataRecord.TLayout);
|
||||||
|
|
||||||
|
function CreateIndicator(const Params: TDataRecord): TFunc; virtual; abstract;
|
||||||
|
|
||||||
|
property Params: TDataRecord.TLayout read FParams;
|
||||||
|
property Input: TDataRecord.TLayout read FInput;
|
||||||
|
property Output: TDataRecord.TLayout read FOutput;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TEMA = class(TIndicatorFactory)
|
||||||
|
type
|
||||||
|
TParam = record
|
||||||
|
Period: Integer;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TInput = record
|
||||||
|
Price: Double;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TResult = record
|
||||||
|
MA: Double;
|
||||||
|
end;
|
||||||
|
|
||||||
|
public
|
||||||
|
constructor Create;
|
||||||
|
function CreateIndicator(const Params: TDataRecord): TIndicatorFactory.TFunc; override;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TMACD = class
|
||||||
|
type
|
||||||
|
TParam = record
|
||||||
|
Fast: TConstFunc<Double, Double>;
|
||||||
|
Slow: TConstFunc<Double, Double>;
|
||||||
|
Signal: TConstFunc<Double, Double>;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TInput = record
|
||||||
|
Price: Double;
|
||||||
|
end;
|
||||||
|
|
||||||
|
TResult = record
|
||||||
|
MacdLine: Double;
|
||||||
|
SignalLine: Double;
|
||||||
|
Histogram: Double;
|
||||||
|
end;
|
||||||
|
|
||||||
|
public
|
||||||
|
class function CreateMACD(const Param: TParam): TConstFunc<TInput, TResult>; static;
|
||||||
|
end;
|
||||||
|
|
||||||
|
var
|
||||||
|
Registry: TList<TIndicatorFactory>;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
{ TIndicators }
|
{ TIndicators }
|
||||||
@@ -466,4 +533,93 @@ begin
|
|||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
class function TIndicators.CreateMean: TConstFunc<TArray<Double>, Double>;
|
||||||
|
begin
|
||||||
|
Result :=
|
||||||
|
function(const Value: TArray<Double>): Double
|
||||||
|
begin
|
||||||
|
if Length(Value) = 0 then
|
||||||
|
exit(NaN);
|
||||||
|
Result := Value[0];
|
||||||
|
for var i := 1 to High(Value) do
|
||||||
|
Result := Result + Value[i];
|
||||||
|
Result := Result / Length(Value);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
// Creates a MACD indicator from three provided moving average functions.
|
||||||
|
class function TMACD.CreateMACD(const Param: TParam): TConstFunc<TInput, TResult>;
|
||||||
|
begin
|
||||||
|
Result :=
|
||||||
|
function(const Input: TInput): TResult
|
||||||
|
var
|
||||||
|
fastVal, slowVal: Double;
|
||||||
|
begin
|
||||||
|
fastVal := Param.Fast(Input.Price);
|
||||||
|
slowVal := Param.Slow(Input.Price);
|
||||||
|
|
||||||
|
if IsNan(slowVal) then // slowVal will be the last one to become non-NaN
|
||||||
|
begin
|
||||||
|
Result.MacdLine := Double.NaN;
|
||||||
|
Result.SignalLine := Double.NaN;
|
||||||
|
Result.Histogram := Double.NaN;
|
||||||
|
end
|
||||||
|
else
|
||||||
|
begin
|
||||||
|
Result.MacdLine := fastVal - slowVal;
|
||||||
|
Result.SignalLine := Param.Signal(Result.MacdLine);
|
||||||
|
if not IsNan(Result.SignalLine) then
|
||||||
|
Result.Histogram := Result.MacdLine - Result.SignalLine
|
||||||
|
else
|
||||||
|
Result.Histogram := Double.NaN;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
constructor TEMA.Create;
|
||||||
|
begin
|
||||||
|
inherited
|
||||||
|
Create(TDataRecord.TLayout.CreateFrom<TParam>, TDataRecord.TLayout.CreateFrom<TInput>, TDataRecord.TLayout.CreateFrom<TResult>)
|
||||||
|
end;
|
||||||
|
|
||||||
|
function TEMA.CreateIndicator(const Params: TDataRecord): TIndicatorFactory.TFunc;
|
||||||
|
begin
|
||||||
|
var Period := Params.GetValue<Integer>('Period');
|
||||||
|
|
||||||
|
var Input := TDataRecord.TLayout.CreateFrom<TInput>;
|
||||||
|
var inPrice := Input.IndexOf('Price');
|
||||||
|
|
||||||
|
var Output := TDataRecord.TLayout.CreateFrom<TResult>;
|
||||||
|
var outMA := Output.IndexOf('MA');
|
||||||
|
|
||||||
|
var CalcEMA := TIndicators.CreateEMA(Period);
|
||||||
|
|
||||||
|
Result :=
|
||||||
|
function(const Input: TDataRecord): TDataRecord
|
||||||
|
begin
|
||||||
|
var Price: Double;
|
||||||
|
Input.GetValue(inPrice, Price);
|
||||||
|
|
||||||
|
var MA := CalcEMA(Price);
|
||||||
|
|
||||||
|
Result := TDataRecord.CreateFrom(Output);
|
||||||
|
Result.SetValue(outMA, MA);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|
||||||
|
constructor TIndicatorFactory.Create(const AParams, AInput, AOutput: TDataRecord.TLayout);
|
||||||
|
begin
|
||||||
|
inherited Create;
|
||||||
|
FParams := AParams;
|
||||||
|
FInput := AInput;
|
||||||
|
FOutput := AOutput;
|
||||||
|
end;
|
||||||
|
|
||||||
|
initialization
|
||||||
|
Registry := TList<TIndicatorFactory>.Create;
|
||||||
|
Registry.Add(TEMA.Create);
|
||||||
|
|
||||||
|
finalization
|
||||||
|
Registry.Free;
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
+1
-2
@@ -30,8 +30,7 @@ uses
|
|||||||
Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas',
|
Myc.Trade.DataStream in '..\Src\Myc.Trade.DataStream.pas',
|
||||||
Myc.Mutable in '..\Src\Myc.Mutable.pas',
|
Myc.Mutable in '..\Src\Myc.Mutable.pas',
|
||||||
Test.Core.Mutable in 'Test.Core.Mutable.pas',
|
Test.Core.Mutable in 'Test.Core.Mutable.pas',
|
||||||
TestDataRecord in 'TestDataRecord.pas',
|
TestDataRecord in 'TestDataRecord.pas';
|
||||||
TestDataRecord.RawAccess in 'TestDataRecord.RawAccess.pas';
|
|
||||||
|
|
||||||
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
|
{ keep comment here to protect the following conditional from being removed by the IDE when adding a unit }
|
||||||
{$IFNDEF TESTINSIGHT}
|
{$IFNDEF TESTINSIGHT}
|
||||||
|
|||||||
@@ -131,7 +131,6 @@ $(PreBuildEvent)]]></PreBuildEvent>
|
|||||||
<DCCReference Include="..\Src\Myc.Mutable.pas"/>
|
<DCCReference Include="..\Src\Myc.Mutable.pas"/>
|
||||||
<DCCReference Include="Test.Core.Mutable.pas"/>
|
<DCCReference Include="Test.Core.Mutable.pas"/>
|
||||||
<DCCReference Include="TestDataRecord.pas"/>
|
<DCCReference Include="TestDataRecord.pas"/>
|
||||||
<DCCReference Include="TestDataRecord.RawAccess.pas"/>
|
|
||||||
<BuildConfiguration Include="Base">
|
<BuildConfiguration Include="Base">
|
||||||
<Key>Base</Key>
|
<Key>Base</Key>
|
||||||
</BuildConfiguration>
|
</BuildConfiguration>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ unit TestDataRecord.RawAccess;
|
|||||||
interface
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
|
System.SysuTils,
|
||||||
DUnitX.TestFramework,
|
DUnitX.TestFramework,
|
||||||
Myc.DataRecord;
|
Myc.DataRecord;
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ uses
|
|||||||
type
|
type
|
||||||
// A simple interface for testing managed type behavior.
|
// A simple interface for testing managed type behavior.
|
||||||
IMyTestInterface = interface
|
IMyTestInterface = interface
|
||||||
|
['{1D23E456-AB78-4CD9-89EF-0123456789AB}'] // GUID for good practice, not strictly needed here
|
||||||
function GetValue: integer;
|
function GetValue: integer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
@@ -38,6 +40,8 @@ type
|
|||||||
function GetValue: integer;
|
function GetValue: integer;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
{ TMyTestInterfaceImpl }
|
||||||
|
|
||||||
constructor TMyTestInterfaceImpl.Create(const AValue: Integer);
|
constructor TMyTestInterfaceImpl.Create(const AValue: Integer);
|
||||||
begin
|
begin
|
||||||
inherited Create;
|
inherited Create;
|
||||||
@@ -52,86 +56,118 @@ end;
|
|||||||
procedure TTestDataRecordRawAccess.TestRawAccess_Integer;
|
procedure TTestDataRecordRawAccess.TestRawAccess_Integer;
|
||||||
var
|
var
|
||||||
rec: TDataRecord;
|
rec: TDataRecord;
|
||||||
|
buffer: TBytes;
|
||||||
field: TDataRecord.IField;
|
field: TDataRecord.IField;
|
||||||
readValue, newValue: Integer;
|
initialValue, readValue, newValue: Integer;
|
||||||
begin
|
begin
|
||||||
// Arrange: Create a record with an integer field.
|
// Arrange: Create a record layout and its buffer.
|
||||||
var builder := TDataRecord.CreateBuilder;
|
var builder := TDataRecord.CreateBuilder;
|
||||||
builder.AddField<Integer>('IntField', 123);
|
builder.AddField<Integer>('IntField');
|
||||||
rec := builder.CreateRec;
|
rec := builder.CreateRec;
|
||||||
field := rec.GetField('IntField');
|
field := rec.GetField('IntField');
|
||||||
Assert.IsNotNull(field, 'Field should exist');
|
Assert.IsNotNull(field, 'Field should exist');
|
||||||
|
|
||||||
// Act & Assert (GetRaw): Read the initial value via raw pointer.
|
SetLength(buffer, rec.GetBufferSize);
|
||||||
readValue := 0;
|
rec.Initialize(buffer);
|
||||||
field.GetRaw(@readValue);
|
try
|
||||||
Assert.AreEqual(123, readValue, 'GetRaw should retrieve the correct integer value');
|
// Set initial value using raw access
|
||||||
|
initialValue := 123;
|
||||||
|
field.SetRaw(buffer, @initialValue);
|
||||||
|
|
||||||
// Act (SetRaw): Set a new value via raw pointer.
|
// Act & Assert (GetRaw): Read the initial value via raw pointer.
|
||||||
newValue := 456;
|
readValue := 0;
|
||||||
field.SetRaw(@newValue);
|
field.GetRaw(buffer, @readValue);
|
||||||
|
Assert.AreEqual(123, readValue, 'GetRaw should retrieve the correct integer value');
|
||||||
|
|
||||||
// Assert: Verify the new value using the typed accessor.
|
// Act (SetRaw): Set a new value via raw pointer.
|
||||||
var typedField := rec.GetField<Integer>('IntField');
|
newValue := 456;
|
||||||
Assert.AreEqual(456, typedField.Value, 'SetRaw should update the integer value correctly');
|
field.SetRaw(buffer, @newValue);
|
||||||
|
|
||||||
|
// Assert: Verify the new value using the typed accessor.
|
||||||
|
var typedField := rec.GetField<Integer>('IntField');
|
||||||
|
Assert.AreEqual(456, typedField.Value[buffer], 'SetRaw should update the integer value correctly');
|
||||||
|
finally
|
||||||
|
rec.Finalize(buffer);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataRecordRawAccess.TestRawAccess_Interface;
|
procedure TTestDataRecordRawAccess.TestRawAccess_Interface;
|
||||||
var
|
var
|
||||||
rec: TDataRecord;
|
rec: TDataRecord;
|
||||||
|
buffer: TBytes;
|
||||||
field: TDataRecord.IField;
|
field: TDataRecord.IField;
|
||||||
initialObj, readObj, newObj: IMyTestInterface;
|
initialObj, readObj, newObj: IMyTestInterface;
|
||||||
begin
|
begin
|
||||||
// Arrange: Create a record with an interface field.
|
// Arrange: Create a record layout and its buffer.
|
||||||
initialObj := TMyTestInterfaceImpl.Create(10);
|
|
||||||
var builder := TDataRecord.CreateBuilder;
|
var builder := TDataRecord.CreateBuilder;
|
||||||
builder.AddField<IMyTestInterface>('IField', initialObj);
|
builder.AddField<IMyTestInterface>('IField');
|
||||||
rec := builder.CreateRec;
|
rec := builder.CreateRec;
|
||||||
field := rec.GetField('IField');
|
field := rec.GetField('IField');
|
||||||
Assert.IsNotNull(field, 'Field should exist');
|
Assert.IsNotNull(field, 'Field should exist');
|
||||||
|
|
||||||
// Act & Assert (GetRaw): Read the initial interface via raw pointer.
|
SetLength(buffer, rec.GetBufferSize);
|
||||||
readObj := nil;
|
rec.Initialize(buffer);
|
||||||
field.GetRaw(@readObj);
|
try
|
||||||
Assert.IsNotNull(readObj, 'GetRaw should retrieve a non-nil interface');
|
// Set initial value
|
||||||
Assert.AreSame(initialObj, readObj, 'GetRaw should retrieve the same interface instance');
|
initialObj := TMyTestInterfaceImpl.Create(10);
|
||||||
Assert.AreEqual(10, readObj.GetValue, 'Value of retrieved interface should be correct');
|
field.SetRaw(buffer, @initialObj);
|
||||||
|
|
||||||
// Act (SetRaw): Set a new interface via raw pointer.
|
// Act & Assert (GetRaw): Read the initial interface via raw pointer.
|
||||||
newObj := TMyTestInterfaceImpl.Create(20);
|
readObj := nil;
|
||||||
field.SetRaw(@newObj);
|
field.GetRaw(buffer, @readObj);
|
||||||
|
Assert.IsNotNull(readObj, 'GetRaw should retrieve a non-nil interface');
|
||||||
|
Assert.AreSame(initialObj, readObj, 'GetRaw should retrieve the same interface instance');
|
||||||
|
Assert.AreEqual(10, readObj.GetValue, 'Value of retrieved interface should be correct');
|
||||||
|
|
||||||
// Assert: Verify the new interface is set correctly.
|
// Act (SetRaw): Set a new interface via raw pointer.
|
||||||
var typedField := rec.GetField<IMyTestInterface>('IField');
|
newObj := TMyTestInterfaceImpl.Create(20);
|
||||||
Assert.AreSame(newObj, typedField.Value, 'SetRaw should update to the new interface instance');
|
field.SetRaw(buffer, @newObj);
|
||||||
Assert.AreEqual(20, typedField.Value.GetValue, 'Value of new interface should be correct');
|
|
||||||
|
// Assert: Verify the new interface is set correctly.
|
||||||
|
var typedField := rec.GetField<IMyTestInterface>('IField');
|
||||||
|
Assert.AreSame(newObj, typedField.Value[buffer], 'SetRaw should update to the new interface instance');
|
||||||
|
Assert.AreEqual(20, typedField.Value[buffer].GetValue, 'Value of new interface should be correct');
|
||||||
|
finally
|
||||||
|
rec.Finalize(buffer);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TTestDataRecordRawAccess.TestRawAccess_String;
|
procedure TTestDataRecordRawAccess.TestRawAccess_String;
|
||||||
var
|
var
|
||||||
rec: TDataRecord;
|
rec: TDataRecord;
|
||||||
|
buffer: TBytes;
|
||||||
field: TDataRecord.IField;
|
field: TDataRecord.IField;
|
||||||
readString, newString: string;
|
initialString, readString, newString: string;
|
||||||
begin
|
begin
|
||||||
// Arrange: Create a record with a string field.
|
// Arrange: Create a record layout and its buffer.
|
||||||
var builder := TDataRecord.CreateBuilder;
|
var builder := TDataRecord.CreateBuilder;
|
||||||
builder.AddField<string>('StrField', 'Hello');
|
builder.AddField<string>('StrField');
|
||||||
rec := builder.CreateRec;
|
rec := builder.CreateRec;
|
||||||
field := rec.GetField('StrField');
|
field := rec.GetField('StrField');
|
||||||
Assert.IsNotNull(field, 'Field should exist');
|
Assert.IsNotNull(field, 'Field should exist');
|
||||||
|
|
||||||
// Act & Assert (GetRaw): Read the initial string via raw pointer.
|
SetLength(buffer, rec.GetBufferSize);
|
||||||
readString := '';
|
rec.Initialize(buffer);
|
||||||
field.GetRaw(@readString);
|
try
|
||||||
Assert.AreEqual('Hello', readString, 'GetRaw should retrieve the correct string value');
|
// Set initial value
|
||||||
|
initialString := 'Hello';
|
||||||
|
field.SetRaw(buffer, @initialString);
|
||||||
|
|
||||||
// Act (SetRaw): Set a new string via raw pointer.
|
// Act & Assert (GetRaw): Read the initial string via raw pointer.
|
||||||
newString := 'World';
|
readString := '';
|
||||||
field.SetRaw(@newString);
|
field.GetRaw(buffer, @readString);
|
||||||
|
Assert.AreEqual('Hello', readString, 'GetRaw should retrieve the correct string value');
|
||||||
|
|
||||||
// Assert: Verify the new string using the typed accessor.
|
// Act (SetRaw): Set a new string via raw pointer.
|
||||||
var typedField := rec.GetField<string>('StrField');
|
newString := 'World';
|
||||||
Assert.AreEqual('World', typedField.Value, 'SetRaw should update the string value correctly');
|
field.SetRaw(buffer, @newString);
|
||||||
|
|
||||||
|
// Assert: Verify the new string using the typed accessor.
|
||||||
|
var typedField := rec.GetField<string>('StrField');
|
||||||
|
Assert.AreEqual('World', typedField.Value[buffer], 'SetRaw should update the string value correctly');
|
||||||
|
finally
|
||||||
|
rec.Finalize(buffer);
|
||||||
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
initialization
|
initialization
|
||||||
|
|||||||
+103
-429
@@ -3,472 +3,146 @@ unit TestDataRecord;
|
|||||||
interface
|
interface
|
||||||
|
|
||||||
uses
|
uses
|
||||||
|
DunitX.TestFramework,
|
||||||
System.SysUtils,
|
System.SysUtils,
|
||||||
System.Classes,
|
System.DateUtils,
|
||||||
System.Variants,
|
|
||||||
DUnitX.TestFramework,
|
|
||||||
Myc.DataRecord;
|
Myc.DataRecord;
|
||||||
|
|
||||||
type
|
type
|
||||||
// Helper types for various TypeKind tests
|
// A record that contains all supported data types for testing
|
||||||
TTestEnum = (teOne, teTwo, teThree);
|
TMyTestRecord = record
|
||||||
TTestSet = set of TTestEnum;
|
MyInteger: Integer;
|
||||||
|
MyInt64: Int64;
|
||||||
TTestRecord = record
|
MySingle: Single;
|
||||||
I: Integer;
|
MyDouble: Double;
|
||||||
S: string;
|
MyString: string;
|
||||||
end;
|
MyTimestamp: TDateTime;
|
||||||
|
|
||||||
TTestMRecord = record
|
|
||||||
private
|
|
||||||
FData: TArray<Byte>;
|
|
||||||
class operator Initialize(out Dest: TTestMRecord);
|
|
||||||
class operator Finalize(var Dest: TTestMRecord);
|
|
||||||
end;
|
|
||||||
|
|
||||||
ITestInterface = interface
|
|
||||||
['{B1E5119C-A2B4-43C1-99C1-39B6A687363E}']
|
|
||||||
function GetValue: Integer;
|
|
||||||
end;
|
|
||||||
|
|
||||||
TTestClass = class(TInterfacedObject, ITestInterface)
|
|
||||||
private
|
|
||||||
FValue: Integer;
|
|
||||||
public
|
|
||||||
constructor Create(AValue: Integer);
|
|
||||||
function GetValue: Integer;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
TDataRecordTests = class
|
TTestDataRecord = class(TObject)
|
||||||
private
|
|
||||||
function CreateRecord<T>(const FieldName: string; const Value: T): TDataRecord;
|
|
||||||
public
|
public
|
||||||
// Test procedures are unchanged
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase('TestZero', '0')]
|
procedure TestLayoutCreation;
|
||||||
[TestCase('TestPositive', '123')]
|
|
||||||
[TestCase('TestNegative', '-456')]
|
|
||||||
procedure Test_tkInteger_Succeeds(const AValue: Integer);
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
[TestCase('TestZero', '0')]
|
procedure TestCreateFromInstanceAndGetValue;
|
||||||
[TestCase('TestPositive', '1234567890123')]
|
|
||||||
[TestCase('TestNegative', '-9876543210987')]
|
|
||||||
procedure Test_tkInt64_Succeeds(const AValue: Int64);
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
procedure Test_tkFloat_Single_Succeeds;
|
procedure TestSetValueAndGetValue;
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkFloat_Double_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCase('Test_A', 'A')]
|
|
||||||
procedure Test_tkChar_Succeeds(const AValue: Char);
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCase('TestEmpty', '')]
|
|
||||||
[TestCase('TestSimple', 'Hello World')]
|
|
||||||
procedure Test_tkString_NonGeneric_Succeeds(const AValue: string);
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[TestCase('TestLeak', 'This test will leak memory')]
|
|
||||||
procedure Test_tkString_Generic_LeaksMemory(const AValue: string);
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkEnumeration_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkSet_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkRecord_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkMRecord_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkDynArray_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkInterface_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkClass_Succeeds_With_Manual_Cleanup;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_tkVariant_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_Assign_CreatesIndependentCopy;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
procedure Test_Reassign_ReleasesOldData;
|
|
||||||
|
|
||||||
// New tests for RTTI-based creation and assignment
|
|
||||||
[Test]
|
|
||||||
[IgnoreMemoryLeaks]
|
|
||||||
procedure Test_CreateFrom_AssignTo_Succeeds;
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[IgnoreMemoryLeaks]
|
|
||||||
procedure Test_Assign_FromCreateFrom_IsIndependentCopy;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
implementation
|
implementation
|
||||||
|
|
||||||
uses
|
uses
|
||||||
System.TypInfo,
|
|
||||||
System.Rtti;
|
System.Rtti;
|
||||||
|
|
||||||
{ TDataRecordTests }
|
{ TTestDataRecord }
|
||||||
|
|
||||||
function TDataRecordTests.CreateRecord<T>(const FieldName: string; const Value: T): TDataRecord;
|
procedure TTestDataRecord.TestLayoutCreation;
|
||||||
var
|
var
|
||||||
// Use the TBuilder helper record. Its lifetime is managed automatically.
|
layout: TDataRecord.TRecordLayout;
|
||||||
builder: TDataRecord.TBuilder;
|
idx: Integer;
|
||||||
|
expectedSize: Integer;
|
||||||
begin
|
begin
|
||||||
builder := TDataRecord.CreateBuilder;
|
// Test: Create layout from a record type
|
||||||
builder.AddField<T>(FieldName, Value);
|
layout := TDataRecord.TRecordLayout.CreateFrom<TMyTestRecord>;
|
||||||
Result := builder.CreateRec;
|
|
||||||
|
// Assert: Check field count
|
||||||
|
Assert.IsTrue(Length(layout.Fields) = 6, 'Layout should have 6 fields');
|
||||||
|
|
||||||
|
// Assert: Check specific fields for correct type mapping
|
||||||
|
idx := layout.IndexOf('MyInteger');
|
||||||
|
Assert.IsTrue(idx > -1, 'Field MyInteger not found');
|
||||||
|
Assert.AreEqual(TDataFieldType.dfInteger, layout.Fields[idx].FieldType, 'MyInteger should be mapped to dfInteger');
|
||||||
|
|
||||||
|
idx := layout.IndexOf('MyString');
|
||||||
|
Assert.IsTrue(idx > -1, 'Field MyString not found');
|
||||||
|
Assert.AreEqual(TDataFieldType.dfString, layout.Fields[idx].FieldType, 'MyString should be mapped to dfString');
|
||||||
|
|
||||||
|
idx := layout.IndexOf('MySingle');
|
||||||
|
Assert.IsTrue(idx > -1, 'Field MySingle not found');
|
||||||
|
Assert.AreEqual(TDataFieldType.dfFloat, layout.Fields[idx].FieldType, 'MySingle should be mapped to dfFloat');
|
||||||
|
|
||||||
|
idx := layout.IndexOf('MyTimestamp');
|
||||||
|
Assert.IsTrue(idx > -1, 'Field MyTimestamp not found');
|
||||||
|
Assert.AreEqual(TDataFieldType.dfTimestamp, layout.Fields[idx].FieldType, 'MyTimestamp should be mapped to dfTimestamp');
|
||||||
|
|
||||||
|
// Assert: Check total buffer size (6 fields * 8 bytes each on x64 due to type promotion and alignment)
|
||||||
|
expectedSize := (sizeof(Int64) * 2) + (sizeof(Double) * 2) + sizeof(String) + sizeof(TDateTime);
|
||||||
|
Assert.AreEqual(expectedSize, layout.GetBufferSize, 'Buffer size calculation is incorrect');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkInteger_Succeeds(const AValue: Integer);
|
procedure TTestDataRecord.TestCreateFromInstanceAndGetValue;
|
||||||
var
|
var
|
||||||
rec: TDataRecord;
|
srcRecord: TMyTestRecord;
|
||||||
field: TDataRecord.IField<Integer>;
|
dataRecord: TDataRecord;
|
||||||
begin
|
begin
|
||||||
rec := CreateRecord<Integer>('Value', AValue);
|
// Setup: Create and populate a source record
|
||||||
field := rec.GetField<Integer>('Value');
|
srcRecord.MyInteger := 12345;
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
srcRecord.MyInt64 := 9876543210;
|
||||||
Assert.AreEqual(AValue, field.Value, 'Value should be equal');
|
srcRecord.MySingle := 123.456;
|
||||||
|
srcRecord.MyDouble := 789.012345;
|
||||||
|
srcRecord.MyString := 'Hello Delphi!';
|
||||||
|
srcRecord.MyTimestamp := EncodeDateTime(2024, 7, 19, 10, 30, 0, 0);
|
||||||
|
|
||||||
|
// Test: Create a TDataRecord from the source record instance
|
||||||
|
dataRecord := TDataRecord.CreateFrom<TMyTestRecord>(srcRecord);
|
||||||
|
|
||||||
|
// Assert: Retrieve each value and check for correctness
|
||||||
|
Assert.AreEqual(srcRecord.MyInt64, dataRecord.GetValue<Int64>('MyInt64'), 'Int64 value mismatch');
|
||||||
|
Assert.AreEqual(srcRecord.MyInteger, dataRecord.GetValue<Integer>('MyInteger'), 'Integer value mismatch');
|
||||||
|
Assert.AreEqual(srcRecord.MyString, dataRecord.GetValue<string>('MyString'), 'String value mismatch');
|
||||||
|
Assert.AreEqual(srcRecord.MyTimestamp, dataRecord.GetValue<TDateTime>('MyTimestamp'), 'TDateTime value mismatch');
|
||||||
|
|
||||||
|
// Assert: Check float values with a tolerance for precision differences
|
||||||
|
Assert.AreEqual(srcRecord.MySingle, dataRecord.GetValue<Single>('MySingle'), 1e-6, 'Single value mismatch');
|
||||||
|
Assert.AreEqual(srcRecord.MyDouble, dataRecord.GetValue<Double>('MyDouble'), 1e-9, 'Double value mismatch');
|
||||||
|
|
||||||
|
// Assert: Test type promotion (reading an Integer as Int64)
|
||||||
|
Assert.IsTrue(srcRecord.MyInteger = dataRecord.GetValue<Int64>('MyInteger'), 'Integer to Int64 promotion failed');
|
||||||
end;
|
end;
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkInt64_Succeeds(const AValue: Int64);
|
procedure TTestDataRecord.TestSetValueAndGetValue;
|
||||||
var
|
var
|
||||||
rec: TDataRecord;
|
dataRecord: TDataRecord;
|
||||||
field: TDataRecord.IField<Int64>;
|
testInt: Integer;
|
||||||
|
testInt64: Int64;
|
||||||
|
testSingle: Single;
|
||||||
|
testDouble: Double;
|
||||||
|
testString: string;
|
||||||
|
testTimestamp: TDateTime;
|
||||||
begin
|
begin
|
||||||
rec := CreateRecord<Int64>('Value', AValue);
|
// Setup: Create an empty TDataRecord with the layout of TMyTestRecord
|
||||||
field := rec.GetField<Int64>('Value');
|
dataRecord := TDataRecord.CreateFrom<TMyTestRecord>;
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.AreEqual(AValue, field.Value, 'Value should be equal');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkFloat_Single_Succeeds;
|
// Setup: Define test values
|
||||||
const
|
testInt := 54321;
|
||||||
TEST_VAL: Single = 3.14;
|
testInt64 := 12345678901;
|
||||||
var
|
testSingle := 987.654;
|
||||||
rec: TDataRecord;
|
testDouble := 543.210987;
|
||||||
field: TDataRecord.IField<Single>;
|
testString := 'Testing SetValue';
|
||||||
begin
|
testTimestamp := Now;
|
||||||
rec := CreateRecord<Single>('Value', TEST_VAL);
|
|
||||||
field := rec.GetField<Single>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.AreEqual(TEST_VAL, field.Value, 'Value should be equal');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkFloat_Double_Succeeds;
|
// Test: Set values for each field by name
|
||||||
const
|
dataRecord.SetValue<Integer>('MyInteger', testInt);
|
||||||
TEST_VAL: Double = 123.456;
|
dataRecord.SetValue<Int64>('MyInt64', testInt64);
|
||||||
var
|
dataRecord.SetValue<Single>('MySingle', testSingle);
|
||||||
rec: TDataRecord;
|
dataRecord.SetValue<Double>('MyDouble', testDouble);
|
||||||
field: TDataRecord.IField<Double>;
|
dataRecord.SetValue<string>('MyString', testString);
|
||||||
begin
|
dataRecord.SetValue<TDateTime>('MyTimestamp', testTimestamp);
|
||||||
rec := CreateRecord<Double>('Value', TEST_VAL);
|
|
||||||
field := rec.GetField<Double>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.AreEqual(TEST_VAL, field.Value, 'Value should be equal');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkChar_Succeeds(const AValue: Char);
|
// Assert: Retrieve the values and check if they match what was set
|
||||||
var
|
Assert.AreEqual(testInt, dataRecord.GetValue<Integer>('MyInteger'), 'SetValue failed for Integer');
|
||||||
rec: TDataRecord;
|
Assert.AreEqual(testInt64, dataRecord.GetValue<Int64>('MyInt64'), 'SetValue failed for Int64');
|
||||||
field: TDataRecord.IField<Char>;
|
Assert.AreEqual(testString, dataRecord.GetValue<string>('MyString'), 'SetValue failed for String');
|
||||||
begin
|
Assert.AreEqual(testTimestamp, dataRecord.GetValue<TDateTime>('MyTimestamp'), 'SetValue failed for TDateTime');
|
||||||
rec := CreateRecord<Char>('Value', AValue);
|
|
||||||
field := rec.GetField<Char>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.AreEqual(AValue, field.Value, 'Value should be equal');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkString_NonGeneric_Succeeds(const AValue: string);
|
Assert.AreEqual(testSingle, dataRecord.GetValue<Single>('MySingle'), 1e-6, 'SetValue failed for Single');
|
||||||
var
|
Assert.AreEqual(testDouble, dataRecord.GetValue<Double>('MyDouble'), 1e-9, 'SetValue failed for Double');
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField;
|
|
||||||
typedField: TDataRecord.IField<string>;
|
|
||||||
begin
|
|
||||||
rec := CreateRecord<string>('Value', AValue);
|
|
||||||
field := rec.GetField('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
typedField := rec.GetField<string>('Value');
|
|
||||||
Assert.AreEqual(AValue, typedField.Value, 'Value should be equal');
|
|
||||||
typedField.Value := 'New Value';
|
|
||||||
Assert.AreEqual('New Value', typedField.Value, 'Value should be updated');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkString_Generic_LeaksMemory(const AValue: string);
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<string>;
|
|
||||||
begin
|
|
||||||
rec := CreateRecord<string>('Value', AValue);
|
|
||||||
field := rec.GetField<string>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.AreEqual(AValue, field.Value, 'Initial value should be correct');
|
|
||||||
field.Value := 'A new string that will be leaked';
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkEnumeration_Succeeds;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<TTestEnum>;
|
|
||||||
begin
|
|
||||||
rec := CreateRecord<TTestEnum>('Value', teTwo);
|
|
||||||
field := rec.GetField<TTestEnum>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.AreEqual(teTwo, field.Value, 'Value should be teTwo');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkSet_Succeeds;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<TTestSet>;
|
|
||||||
testSet: TTestSet;
|
|
||||||
begin
|
|
||||||
testSet := [teOne, teThree];
|
|
||||||
rec := CreateRecord<TTestSet>('Value', testSet);
|
|
||||||
field := rec.GetField<TTestSet>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.IsTrue(testSet = field.Value, 'Value should be [teOne, teThree]');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkRecord_Succeeds;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<TTestRecord>;
|
|
||||||
testRec, resultRec: TTestRecord;
|
|
||||||
begin
|
|
||||||
testRec.I := 42;
|
|
||||||
testRec.S := 'Hello Record';
|
|
||||||
rec := CreateRecord<TTestRecord>('Value', testRec);
|
|
||||||
field := rec.GetField<TTestRecord>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
resultRec := field.Value;
|
|
||||||
Assert.AreEqual(testRec.I, resultRec.I, 'Record.I should match');
|
|
||||||
Assert.AreEqual(testRec.S, resultRec.S, 'Record.S should match');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkMRecord_Succeeds;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<TTestMRecord>;
|
|
||||||
testMRec, resultMRec: TTestMRecord;
|
|
||||||
begin
|
|
||||||
SetLength(testMRec.FData, 10);
|
|
||||||
testMRec.FData[0] := 1;
|
|
||||||
rec := CreateRecord<TTestMRecord>('Value', testMRec);
|
|
||||||
field := rec.GetField<TTestMRecord>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
resultMRec := field.Value;
|
|
||||||
Assert.AreEqual(Length(testMRec.FData), Length(resultMRec.FData), 'MRecord.FData length should match');
|
|
||||||
Assert.AreEqual(testMRec.FData[0], resultMRec.FData[0], 'MRecord.FData content should match');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkDynArray_Succeeds;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<TArray<Integer>>;
|
|
||||||
testArr, resultArr: TArray<Integer>;
|
|
||||||
begin
|
|
||||||
testArr := [10, 20, 30];
|
|
||||||
rec := CreateRecord<TArray<Integer>>('Value', testArr);
|
|
||||||
field := rec.GetField<TArray<Integer>>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
resultArr := field.Value;
|
|
||||||
Assert.AreEqual(Length(testArr), Length(resultArr), 'DynArray length should match');
|
|
||||||
Assert.AreEqual(testArr[1], resultArr[1], 'DynArray content should match');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkInterface_Succeeds;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<ITestInterface>;
|
|
||||||
testIntf, resultIntf: ITestInterface;
|
|
||||||
begin
|
|
||||||
testIntf := TTestClass.Create(1337);
|
|
||||||
rec := CreateRecord<ITestInterface>('Value', testIntf);
|
|
||||||
field := rec.GetField<ITestInterface>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
resultIntf := field.Value;
|
|
||||||
Assert.IsNotNull(resultIntf, 'Result interface should not be null');
|
|
||||||
Assert.AreEqual(1337, resultIntf.GetValue, 'Interface method should return correct value');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkClass_Succeeds_With_Manual_Cleanup;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<TTestClass>;
|
|
||||||
testObj, resultObj: TTestClass;
|
|
||||||
begin
|
|
||||||
testObj := TTestClass.Create(99);
|
|
||||||
try
|
|
||||||
rec := CreateRecord<TTestClass>('Value', testObj);
|
|
||||||
field := rec.GetField<TTestClass>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
resultObj := field.Value;
|
|
||||||
Assert.AreSame(testObj, resultObj, 'Should be the same object instance');
|
|
||||||
Assert.AreEqual(99, resultObj.GetValue, 'Object method should return correct value');
|
|
||||||
finally
|
|
||||||
testObj.Free;
|
|
||||||
end;
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_tkVariant_Succeeds;
|
|
||||||
var
|
|
||||||
rec: TDataRecord;
|
|
||||||
field: TDataRecord.IField<Variant>;
|
|
||||||
testVar: Variant;
|
|
||||||
begin
|
|
||||||
testVar := 'Hello Variant';
|
|
||||||
rec := CreateRecord<Variant>('Value', testVar);
|
|
||||||
field := rec.GetField<Variant>('Value');
|
|
||||||
Assert.IsNotNull(field, 'Field should not be null');
|
|
||||||
Assert.AreEqual(testVar, field.Value, 'Variant value should match');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_Assign_CreatesIndependentCopy;
|
|
||||||
var
|
|
||||||
rec1, rec2: TDataRecord;
|
|
||||||
field1, field2: TDataRecord.IField<string>;
|
|
||||||
begin
|
|
||||||
// Create original record
|
|
||||||
rec1 := CreateRecord<string>('Value', 'Original');
|
|
||||||
|
|
||||||
// Assign to a new record, invoking operator Assign. This covers 'CreateFromRec'.
|
|
||||||
rec2 := rec1;
|
|
||||||
|
|
||||||
// Verify the copy
|
|
||||||
Assert.IsTrue(Length(rec2.Layout) = 1, 'Copied record should have one field');
|
|
||||||
Assert.AreEqual('Value', rec2.Layout[0].Name, 'Field name should be copied');
|
|
||||||
field2 := rec2.GetField<string>('Value');
|
|
||||||
Assert.IsNotNull(field2, 'Field should exist in copied record');
|
|
||||||
Assert.AreEqual('Original', field2.Value, 'Field value should be copied');
|
|
||||||
|
|
||||||
// Modify the copy and check for independence (deep copy)
|
|
||||||
field2.Value := 'Modified';
|
|
||||||
|
|
||||||
field1 := rec1.GetField<string>('Value');
|
|
||||||
Assert.AreEqual('Original', field1.Value, 'Original record should not be modified');
|
|
||||||
Assert.AreEqual('Modified', field2.Value, 'Copied record should reflect modification');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_Reassign_ReleasesOldData;
|
|
||||||
var
|
|
||||||
rec1, rec2: TDataRecord;
|
|
||||||
field: TDataRecord.IField<string>;
|
|
||||||
begin
|
|
||||||
// Create two independent records with managed string fields.
|
|
||||||
// DUnitX will report a leak if the memory for rec1 is not freed upon reassignment.
|
|
||||||
rec1 := CreateRecord<string>('OldValue', 'This should be released');
|
|
||||||
rec2 := CreateRecord<string>('NewValue', 'This should remain');
|
|
||||||
|
|
||||||
// Re-assign rec1. This should Finalize the old rec1 and Assign the new data.
|
|
||||||
rec1 := rec2;
|
|
||||||
|
|
||||||
// Verify the state of the reassigned rec1
|
|
||||||
Assert.AreEqual(-1, rec1.IndexOf('OldValue'), 'Old field should not exist anymore');
|
|
||||||
Assert.AreNotEqual(-1, rec1.IndexOf('NewValue'), 'New field should exist');
|
|
||||||
|
|
||||||
field := rec1.GetField<string>('NewValue');
|
|
||||||
Assert.AreEqual('This should remain', field.Value, 'Value should be from the new record');
|
|
||||||
|
|
||||||
// Also check that rec2 is unaffected by the assignment
|
|
||||||
field := rec2.GetField<string>('NewValue');
|
|
||||||
Assert.AreEqual('This should remain', field.Value, 'Source record for assignment should be unchanged');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_CreateFrom_AssignTo_Succeeds;
|
|
||||||
var
|
|
||||||
srcRec, dstRec: TTestRecord;
|
|
||||||
dataRec: TDataRecord;
|
|
||||||
fieldI: TDataRecord.IField<Integer>;
|
|
||||||
fieldS: TDataRecord.IField<string>;
|
|
||||||
begin
|
|
||||||
// Setup a standard record with data
|
|
||||||
srcRec.I := 123;
|
|
||||||
srcRec.S := 'Hello from RTTI';
|
|
||||||
|
|
||||||
// Create a TDataRecord from the standard record via RTTI
|
|
||||||
dataRec := TDataRecord.CreateFrom<TTestRecord>(srcRec);
|
|
||||||
|
|
||||||
// Verify that the data was correctly transferred into the TDataRecord
|
|
||||||
Assert.IsTrue(Length(dataRec.Layout) = 2, 'Layout should have 2 fields');
|
|
||||||
fieldI := dataRec.GetField<Integer>('I');
|
|
||||||
Assert.IsNotNull(fieldI, 'Integer field should exist');
|
|
||||||
Assert.AreEqual(srcRec.I, fieldI.Value, 'Integer value should match');
|
|
||||||
|
|
||||||
fieldS := dataRec.GetField<string>('S');
|
|
||||||
Assert.IsNotNull(fieldS, 'String field should exist');
|
|
||||||
Assert.AreEqual(srcRec.S, fieldS.Value, 'String value should match');
|
|
||||||
|
|
||||||
// Assign the data back from the TDataRecord to another standard record
|
|
||||||
dataRec.AssignTo<TTestRecord>(dstRec);
|
|
||||||
|
|
||||||
// Verify that the data was correctly restored
|
|
||||||
Assert.AreEqual(srcRec.I, dstRec.I, 'Assigned back record integer should match');
|
|
||||||
Assert.AreEqual(srcRec.S, dstRec.S, 'Assigned back record string should match');
|
|
||||||
end;
|
|
||||||
|
|
||||||
procedure TDataRecordTests.Test_Assign_FromCreateFrom_IsIndependentCopy;
|
|
||||||
var
|
|
||||||
srcRec: TTestRecord;
|
|
||||||
rec1, rec2: TDataRecord;
|
|
||||||
field1, field2: TDataRecord.IField<string>;
|
|
||||||
begin
|
|
||||||
// Setup a record and create a TDataRecord from it
|
|
||||||
srcRec.I := 456;
|
|
||||||
srcRec.S := 'Original RTTI value';
|
|
||||||
rec1 := TDataRecord.CreateFrom<TTestRecord>(srcRec);
|
|
||||||
|
|
||||||
// Assign the TDataRecord to another, invoking operator Assign
|
|
||||||
rec2 := rec1;
|
|
||||||
|
|
||||||
// Modify the string field in the copy
|
|
||||||
field2 := rec2.GetField<string>('S');
|
|
||||||
Assert.IsNotNull(field2, 'Field must exist in copy');
|
|
||||||
field2.Value := 'Modified in copy';
|
|
||||||
|
|
||||||
// Verify that the original TDataRecord is unchanged
|
|
||||||
field1 := rec1.GetField<string>('S');
|
|
||||||
Assert.IsNotNull(field1, 'Field must exist in original');
|
|
||||||
Assert.AreEqual('Original RTTI value', field1.Value, 'Original record should not be modified');
|
|
||||||
Assert.AreEqual('Modified in copy', field2.Value, 'Copied record should reflect modification');
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TTestClass }
|
|
||||||
constructor TTestClass.Create(AValue: Integer);
|
|
||||||
begin
|
|
||||||
inherited Create;
|
|
||||||
FValue := AValue;
|
|
||||||
end;
|
|
||||||
function TTestClass.GetValue: Integer;
|
|
||||||
begin
|
|
||||||
Result := FValue;
|
|
||||||
end;
|
|
||||||
|
|
||||||
{ TTestMRecord }
|
|
||||||
class operator TTestMRecord.Finalize(var Dest: TTestMRecord);
|
|
||||||
begin
|
|
||||||
Dest.FData := nil;
|
|
||||||
end;
|
|
||||||
class operator TTestMRecord.Initialize(out Dest: TTestMRecord);
|
|
||||||
begin
|
|
||||||
Dest.FData := nil;
|
|
||||||
end;
|
end;
|
||||||
|
|
||||||
initialization
|
initialization
|
||||||
TDUnitX.RegisterTestFixture(TDataRecordTests);
|
TDUnitX.RegisterTestFixture(TTestDataRecord);
|
||||||
|
|
||||||
end.
|
end.
|
||||||
|
|||||||
Reference in New Issue
Block a user