Polished DataProvider & Converter, RTTI field access
This commit is contained in:
+71
-357
@@ -3,10 +3,8 @@ unit Myc.Trade.DataPoint;
|
||||
interface
|
||||
|
||||
uses
|
||||
System.TimeSpan,
|
||||
Myc.Signals,
|
||||
Myc.Core.Notifier,
|
||||
Myc.Trade.Types;
|
||||
Myc.Core.Notifier;
|
||||
|
||||
type
|
||||
// Represents a time-stamped data point in a series.
|
||||
@@ -25,14 +23,39 @@ type
|
||||
function ProcessData(const Value: T): TState; virtual; abstract;
|
||||
end;
|
||||
|
||||
TTag = Pointer;
|
||||
TDataProvider<T> = record
|
||||
type
|
||||
TTag = Pointer;
|
||||
IDataProvider = interface
|
||||
function Link(const Receiver: IMycProcessor<T>): TTag;
|
||||
procedure Unlink(Tag: TTag);
|
||||
end;
|
||||
|
||||
IMycDataProvider<T> = interface
|
||||
function Link(const Receiver: IMycProcessor<T>): TTag;
|
||||
procedure Unlink(Tag: TTag);
|
||||
{$REGION 'private'}
|
||||
strict private
|
||||
class var
|
||||
FNull: IDataProvider;
|
||||
class constructor CreateClass;
|
||||
private
|
||||
FDataProvider: IDataProvider;
|
||||
{$ENDREGION}
|
||||
public
|
||||
constructor Create(const ADataProvider: IDataProvider);
|
||||
|
||||
// Managed record operators
|
||||
class operator Initialize(out Dest: TDataProvider<T>);
|
||||
class operator Implicit(const A: IDataProvider): TDataProvider<T>; overload;
|
||||
class operator Implicit(const A: TDataProvider<T>): IDataProvider; overload;
|
||||
|
||||
// Wrapper for IMycDataProvider methods
|
||||
function Link(const Receiver: IMycProcessor<T>): TTag; inline;
|
||||
procedure Unlink(Tag: TTag); inline;
|
||||
|
||||
// Provides access to the null object instance.
|
||||
class property Null: IDataProvider read FNull;
|
||||
end;
|
||||
|
||||
TMycDataProvider<T> = class abstract(TContainedObject, IMycDataProvider<T>)
|
||||
TMycDataProvider<T> = class abstract(TContainedObject, TDataProvider<T>.IDataProvider)
|
||||
private
|
||||
FListeners: TMycNotifyList<IMycProcessor<T>>;
|
||||
public
|
||||
@@ -42,9 +65,9 @@ type
|
||||
function Broadcast(const Value: T): TState;
|
||||
procedure Notify(const Func: TMycNotifyList<IMycProcessor<T>>.TNotifyProc);
|
||||
// Link a Processor
|
||||
function Link(const Processor: IMycProcessor<T>): TTag;
|
||||
function Link(const Processor: IMycProcessor<T>): TDataProvider<T>.TTag;
|
||||
// Unlink a linked strategy
|
||||
procedure Unlink(Tag: TTag);
|
||||
procedure Unlink(Tag: TDataProvider<T>.TTag);
|
||||
end;
|
||||
|
||||
TMycGenericProcessor<T> = class(TMycProcessor<T>)
|
||||
@@ -68,216 +91,24 @@ type
|
||||
constructor Create(const Controller: IInterface; const AProc: TProc);
|
||||
end;
|
||||
|
||||
IMycConverter<S, T> = interface(IMycProcessor<S>)
|
||||
function GetSender: IMycDataProvider<T>;
|
||||
property Sender: IMycDataProvider<T> read GetSender;
|
||||
end;
|
||||
|
||||
TMycConverter<S, T> = class abstract(TMycProcessor<S>, IMycConverter<S, T>)
|
||||
private
|
||||
FSender: TMycDataProvider<T>;
|
||||
function GetSender: IMycDataProvider<T>;
|
||||
protected
|
||||
function ProcessData(const Value: S): TState; override; abstract;
|
||||
|
||||
// Broadcasts the given data to all linked processors.
|
||||
function Broadcast(const Value: T): TState;
|
||||
|
||||
TNullDataProvider<T> = class(TInterfacedObject, TDataProvider<T>.IDataProvider)
|
||||
public
|
||||
constructor Create;
|
||||
destructor Destroy; override;
|
||||
property Sender: IMycDataProvider<T> read GetSender;
|
||||
end;
|
||||
|
||||
// An immutable time-ordered series of data points.
|
||||
// The most recent element has the logical index 0.
|
||||
IDataSeries<T> = interface
|
||||
function GetCount: Int64;
|
||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||
function GetTotalCount: Int64;
|
||||
function GetLookback: Int64;
|
||||
// Add data and result the new series.
|
||||
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): IDataSeries<T>;
|
||||
property Count: Int64 read GetCount;
|
||||
// Accesses data points by their logical index.
|
||||
// Index 0 is the newest element, Index (Count - 1) is the oldest.
|
||||
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
||||
// The maximum number of adressable items. Count will never be bigger than the Lookback.
|
||||
property Lookback: Int64 read GetLookback;
|
||||
// The total number of items ever added to the series.
|
||||
property TotalCount: Int64 read GetTotalCount;
|
||||
end;
|
||||
|
||||
// Interface Helper for IDataSeries<T>.
|
||||
// Provides a safe, value-type-like wrapper around the interface.
|
||||
TDataSeries<T> = record
|
||||
type
|
||||
TConvertFunc<S> = reference to function(const Val: TDataPoint<T>): S;
|
||||
private
|
||||
FDataSeries: IDataSeries<T>;
|
||||
function GetCount: Int64;
|
||||
function GetItems(Idx: Int64): TDataPoint<T>;
|
||||
function GetData(Idx: Int64): T;
|
||||
function GetTime(Idx: Int64): TDateTime;
|
||||
function GetTotalCount: Int64;
|
||||
function GetLookback: Int64;
|
||||
class function GetNull: IDataSeries<T>; static;
|
||||
public
|
||||
constructor Create(ADataSeries: IDataSeries<T>);
|
||||
|
||||
class operator Initialize(out Dest: TDataSeries<T>);
|
||||
class operator Finalize(var Dest: TDataSeries<T>);
|
||||
|
||||
class operator Implicit(const A: TDataSeries<T>): IDataSeries<T>;
|
||||
class operator Implicit(const A: IDataSeries<T>): TDataSeries<T>;
|
||||
|
||||
class function CreateDataSeries(Lookback: Int64; const Data: TArray<TDataPoint<T>> = nil): TDataSeries<T>; static;
|
||||
|
||||
function Add(const Data: TArray<TDataPoint<T>>): TDataSeries<T>; overload;
|
||||
function Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): TDataSeries<T>; overload;
|
||||
function Convert<S>(const Func: TConvertFunc<S>): TDataSeries<S>;
|
||||
|
||||
// Searches for a data point by its timestamp.
|
||||
// Returns the logical index of the matching item.
|
||||
// If no exact match, returns the index of the item immediately preceding the timestamp.
|
||||
// Returns -1 if the timestamp is before the oldest item in the series.
|
||||
function IndexOf(TimeStamp: TDateTime): Int64;
|
||||
|
||||
function ToArray: TArray<TDataPoint<T>>;
|
||||
function ToDataArray: TArray<T>;
|
||||
|
||||
class property Null: IDataSeries<T> read GetNull;
|
||||
property Count: Int64 read GetCount;
|
||||
property TotalCount: Int64 read GetTotalCount;
|
||||
property Lookback: Int64 read GetLookback;
|
||||
property Items[Idx: Int64]: TDataPoint<T> read GetItems; default;
|
||||
property Data[Idx: Int64]: T read GetData;
|
||||
property Time[Idx: Int64]: TDateTime read GetTime;
|
||||
end;
|
||||
|
||||
TDataSeriesDoubleHelper = record helper for TDataSeries<Double>
|
||||
function ToOhlc(TimeFrame: TTimeSpan): TDataSeries<TOhlcItem>;
|
||||
end;
|
||||
|
||||
TDataSeriesOhlcHelper = record helper for TDataSeries<TOhlcItem>
|
||||
function ToOpen: TDataSeries<Single>;
|
||||
function ToClose: TDataSeries<Single>;
|
||||
function ToHigh: TDataSeries<Single>;
|
||||
function ToLow: TDataSeries<Single>;
|
||||
function ToVolume: TDataSeries<Single>;
|
||||
function Link(const Receiver: IMycProcessor<T>): TDataProvider<T>.TTag;
|
||||
procedure Unlink(Tag: TDataProvider<T>.TTag);
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
||||
uses
|
||||
System.SysUtils,
|
||||
System.Math,
|
||||
System.Generics.Collections,
|
||||
Myc.Trade.DataArray,
|
||||
Myc.Trade.Core.DataPoint;
|
||||
{ TNullDataProvider }
|
||||
|
||||
// Optimized helper function using direct TTimeSpan features and integer arithmetic.
|
||||
function CeilToTimeSpan(const ATime: TDateTime; const ATimeSpan: TTimeSpan): TDateTime;
|
||||
var
|
||||
timeSinceMidnight: TTimeSpan;
|
||||
timeSpanTicks: Int64;
|
||||
numIntervals, ceiledTicks: Int64;
|
||||
function TNullDataProvider<T>.Link(const Receiver: IMycProcessor<T>): TDataProvider<T>.TTag;
|
||||
begin
|
||||
timeSpanTicks := ATimeSpan.Ticks;
|
||||
Assert(timeSpanTicks > 0, 'TimeSpan must be positive.');
|
||||
|
||||
// Get the time portion of ATime directly as a TTimeSpan.
|
||||
timeSinceMidnight := TTimeSpan.Subtract(ATime, Trunc(ATime));
|
||||
|
||||
// Using integer arithmetic to find the ceiling is robust.
|
||||
// This is a standard formula for integer ceiling division: (numerator + denominator - 1) / denominator
|
||||
numIntervals := (timeSinceMidnight.Ticks + timeSpanTicks - 1) div timeSpanTicks;
|
||||
|
||||
ceiledTicks := numIntervals * timeSpanTicks;
|
||||
|
||||
// Construct the final DateTime from the date part and the new, aligned time part.
|
||||
Result := Trunc(ATime) + TTimeSpan.FromTicks(ceiledTicks);
|
||||
Result := nil;
|
||||
end;
|
||||
|
||||
function TDataSeriesDoubleHelper.ToOhlc(TimeFrame: TTimeSpan): TDataSeries<TOhlcItem>;
|
||||
var
|
||||
ohlcPoints: TList<TDataPoint<TOhlcItem>>;
|
||||
currentBar: TOhlcItem;
|
||||
windowEndTime: TDateTime;
|
||||
sourceIdx: Int64;
|
||||
firstPointInBar: Boolean;
|
||||
procedure TNullDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag);
|
||||
begin
|
||||
if Self.Count = 0 then
|
||||
exit(TDataSeries<TOhlcItem>.Create(TNullDataSeries<TOhlcItem>.Null));
|
||||
|
||||
ohlcPoints := TList<TDataPoint<TOhlcItem>>.Create;
|
||||
try
|
||||
if TimeFrame.Ticks <= 0 then
|
||||
raise EArgumentException.Create('Invalid TimeFrame for OHLC aggregation.');
|
||||
|
||||
sourceIdx := Self.Count - 1; // Start with the oldest data point
|
||||
firstPointInBar := True;
|
||||
windowEndTime := 0;
|
||||
|
||||
// Iterate through all source points chronologically (oldest to newest)
|
||||
while sourceIdx >= 0 do
|
||||
begin
|
||||
var P := Self.Items[sourceIdx];
|
||||
|
||||
if firstPointInBar then
|
||||
begin
|
||||
windowEndTime := CeilToTimeSpan(P.Time, TimeFrame);
|
||||
currentBar.Create(P.Data, P.Data, P.Data, P.Data, 0);
|
||||
firstPointInBar := False;
|
||||
end;
|
||||
|
||||
if P.Time >= windowEndTime then
|
||||
begin
|
||||
ohlcPoints.Add(TDataPoint<TOhlcItem>.Create(windowEndTime, currentBar));
|
||||
firstPointInBar := True;
|
||||
Continue; // Re-evaluate the same point for the next bar
|
||||
end;
|
||||
|
||||
currentBar.High := Max(currentBar.High, P.Data);
|
||||
currentBar.Low := Min(currentBar.Low, P.Data);
|
||||
currentBar.Close := P.Data;
|
||||
currentBar.Volume := currentBar.Volume + 1;
|
||||
|
||||
Dec(sourceIdx);
|
||||
end;
|
||||
|
||||
if not firstPointInBar then
|
||||
ohlcPoints.Add(TDataPoint<TOhlcItem>.Create(windowEndTime, currentBar));
|
||||
|
||||
Result := TDataSeries<TOhlcItem>.CreateDataSeries(ohlcPoints.Count, ohlcPoints.ToArray);
|
||||
finally
|
||||
ohlcPoints.Free;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TDataSeriesOhlcHelper.ToClose: TDataSeries<Single>;
|
||||
begin
|
||||
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Close; end);
|
||||
end;
|
||||
|
||||
function TDataSeriesOhlcHelper.ToHigh: TDataSeries<Single>;
|
||||
begin
|
||||
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.High; end);
|
||||
end;
|
||||
|
||||
function TDataSeriesOhlcHelper.ToLow: TDataSeries<Single>;
|
||||
begin
|
||||
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Low; end);
|
||||
end;
|
||||
|
||||
function TDataSeriesOhlcHelper.ToOpen: TDataSeries<Single>;
|
||||
begin
|
||||
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Open; end);
|
||||
end;
|
||||
|
||||
function TDataSeriesOhlcHelper.ToVolume: TDataSeries<Single>;
|
||||
begin
|
||||
Result := Self.Convert<Single>(function(const Ohlc: TDataPoint<TOhlcItem>): Single begin Result := Ohlc.Data.Volume; end);
|
||||
// Do nothing in the null implementation.
|
||||
end;
|
||||
|
||||
{ TDataPoint<T> }
|
||||
@@ -288,148 +119,53 @@ begin
|
||||
Data := AData;
|
||||
end;
|
||||
|
||||
constructor TDataSeries<T>.Create(ADataSeries: IDataSeries<T>);
|
||||
{ TDataProvider<T> }
|
||||
|
||||
class constructor TDataProvider<T>.CreateClass;
|
||||
begin
|
||||
if Assigned(ADataSeries) then
|
||||
FDataSeries := ADataSeries
|
||||
else
|
||||
FDataSeries := Null;
|
||||
// Create the singleton null object instance.
|
||||
FNull := TNullDataProvider<T>.Create;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>; First, Count: Integer): TDataSeries<T>;
|
||||
constructor TDataProvider<T>.Create(const ADataProvider: IDataProvider);
|
||||
begin
|
||||
{$ifdef DEBUG}
|
||||
for var i := First + 1 to First + Count - 1 do
|
||||
Assert(Data[i].Time >= Data[i - 1].Time, 'Input array for Add is not chronologically sorted');
|
||||
if FDataSeries.Count > 0 then
|
||||
Assert(Data[First].Time >= FDataSeries[0].Time, 'First new item is older than last existing item');
|
||||
{$endif}
|
||||
|
||||
Result := FDataSeries.Add(Data, First, Count);
|
||||
FDataProvider := ADataProvider;
|
||||
// Ensure that the internal interface is never nil.
|
||||
if not Assigned(FDataProvider) then
|
||||
FDataProvider := FNull;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.Add(const Data: TArray<TDataPoint<T>>): TDataSeries<T>;
|
||||
class operator TDataProvider<T>.Initialize(out Dest: TDataProvider<T>);
|
||||
begin
|
||||
Result := Add(Data, 0, Length(Data));
|
||||
// Initialize new record instances with the null object.
|
||||
Dest.FDataProvider := FNull;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.Convert<S>(const Func: TConvertFunc<S>): TDataSeries<S>;
|
||||
begin
|
||||
Result := TConvertSeries<T, S>.Create(FDataSeries, Func);
|
||||
end;
|
||||
|
||||
class function TDataSeries<T>.CreateDataSeries(Lookback: Int64; const Data: TArray<TDataPoint<T>> = nil): TDataSeries<T>;
|
||||
begin
|
||||
Result :=
|
||||
TMycDataSeries<T>.CreateDataSeries(Lookback, TMycDataArray<TDataPoint<T>>.CreateFromArray(Data, 0, Length(Data)), Length(Data));
|
||||
end;
|
||||
|
||||
class operator TDataSeries<T>.Finalize(var Dest: TDataSeries<T>);
|
||||
begin
|
||||
Dest.FDataSeries := nil;
|
||||
end;
|
||||
|
||||
class operator TDataSeries<T>.Initialize(out Dest: TDataSeries<T>);
|
||||
begin
|
||||
Dest.FDataSeries := Null;
|
||||
end;
|
||||
|
||||
class operator TDataSeries<T>.Implicit(const A: TDataSeries<T>): IDataSeries<T>;
|
||||
begin
|
||||
Result := A.FDataSeries;
|
||||
end;
|
||||
|
||||
class operator TDataSeries<T>.Implicit(const A: IDataSeries<T>): TDataSeries<T>;
|
||||
class operator TDataProvider<T>.Implicit(const A: IDataProvider): TDataProvider<T>;
|
||||
begin
|
||||
// Allow implicit conversion from the interface to the helper.
|
||||
Result.Create(A);
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetCount: Int64;
|
||||
class operator TDataProvider<T>.Implicit(const A: TDataProvider<T>): IDataProvider;
|
||||
begin
|
||||
Result := FDataSeries.GetCount;
|
||||
// Allow implicit conversion from the helper to the interface.
|
||||
Result := A.FDataProvider;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetItems(Idx: Int64): TDataPoint<T>;
|
||||
function TDataProvider<T>.Link(const Receiver: IMycProcessor<T>): TTag;
|
||||
begin
|
||||
Result := FDataSeries[Idx];
|
||||
// Forward the call to the wrapped interface.
|
||||
Result := FDataProvider.Link(Receiver);
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetData(Idx: Int64): T;
|
||||
procedure TDataProvider<T>.Unlink(Tag: TTag);
|
||||
begin
|
||||
Result := FDataSeries[Idx].Data;
|
||||
// Forward the call to the wrapped interface.
|
||||
FDataProvider.Unlink(Tag);
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetLookback: Int64;
|
||||
begin
|
||||
Result := FDataSeries.Lookback;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetTime(Idx: Int64): TDateTime;
|
||||
begin
|
||||
Result := FDataSeries[Idx].Time;
|
||||
end;
|
||||
|
||||
class function TDataSeries<T>.GetNull: IDataSeries<T>;
|
||||
begin
|
||||
Result := TNullDataSeries<T>.Null;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.GetTotalCount: Int64;
|
||||
begin
|
||||
Result := FDataSeries.GetTotalCount;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.IndexOf(TimeStamp: TDateTime): Int64;
|
||||
var
|
||||
low, high, mid: Int64;
|
||||
dataPointTime: TDateTime;
|
||||
begin
|
||||
Result := -1;
|
||||
if Count = 0 then
|
||||
Exit;
|
||||
|
||||
low := 0;
|
||||
high := Count - 1;
|
||||
|
||||
while (low <= high) do
|
||||
begin
|
||||
mid := low + (high - low) div 2;
|
||||
dataPointTime := FDataSeries[mid].Time;
|
||||
|
||||
if (dataPointTime = TimeStamp) then
|
||||
begin
|
||||
Result := mid;
|
||||
break;
|
||||
end
|
||||
else if (dataPointTime < TimeStamp) then
|
||||
begin
|
||||
Result := mid;
|
||||
high := mid - 1;
|
||||
end
|
||||
else
|
||||
begin
|
||||
low := mid + 1;
|
||||
end;
|
||||
end;
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.ToArray: TArray<TDataPoint<T>>;
|
||||
begin
|
||||
var n := FDataSeries.Count;
|
||||
SetLength(Result, n);
|
||||
dec(n);
|
||||
for var i := 0 to n do
|
||||
Result[i] := FDataSeries[n - i];
|
||||
end;
|
||||
|
||||
function TDataSeries<T>.ToDataArray: TArray<T>;
|
||||
begin
|
||||
var n := FDataSeries.Count;
|
||||
SetLength(Result, n);
|
||||
dec(n);
|
||||
for var i := 0 to n do
|
||||
Result[i] := FDataSeries[n - i].Data;
|
||||
end;
|
||||
{ TMycGenericProcessor<T> }
|
||||
|
||||
constructor TMycGenericProcessor<T>.Create(const AProc: TProc);
|
||||
begin
|
||||
@@ -442,6 +178,8 @@ begin
|
||||
Result := FProc(Value);
|
||||
end;
|
||||
|
||||
{ TMycContainedProcessor<T> }
|
||||
|
||||
constructor TMycContainedProcessor<T>.Create(const Controller: IInterface; const AProc: TProc);
|
||||
begin
|
||||
inherited Create(Controller);
|
||||
@@ -453,7 +191,7 @@ begin
|
||||
Result := FProc(Value);
|
||||
end;
|
||||
|
||||
{ TMycDataProvider<S, T> }
|
||||
{ TMycDataProvider<T> }
|
||||
|
||||
constructor TMycDataProvider<T>.Create(const Controller: IInterface);
|
||||
begin
|
||||
@@ -504,7 +242,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
function TMycDataProvider<T>.Link(const Processor: IMycProcessor<T>): TTag;
|
||||
function TMycDataProvider<T>.Link(const Processor: IMycProcessor<T>): TDataProvider<T>.TTag;
|
||||
begin
|
||||
// Add the Processor to the notification list
|
||||
FListeners.Lock;
|
||||
@@ -515,7 +253,7 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
procedure TMycDataProvider<T>.Unlink(Tag: TTag);
|
||||
procedure TMycDataProvider<T>.Unlink(Tag: TDataProvider<T>.TTag);
|
||||
begin
|
||||
FListeners.Lock;
|
||||
try
|
||||
@@ -525,28 +263,4 @@ begin
|
||||
end;
|
||||
end;
|
||||
|
||||
{ TMycConverter<S, T> }
|
||||
|
||||
constructor TMycConverter<S, T>.Create;
|
||||
begin
|
||||
inherited Create;
|
||||
FSender := TMycDataProvider<T>.Create(Self);
|
||||
end;
|
||||
|
||||
destructor TMycConverter<S, T>.Destroy;
|
||||
begin
|
||||
FSender.Free;
|
||||
inherited Destroy;
|
||||
end;
|
||||
|
||||
function TMycConverter<S, T>.Broadcast(const Value: T): TState;
|
||||
begin
|
||||
Result := FSender.Broadcast(Value);
|
||||
end;
|
||||
|
||||
function TMycConverter<S, T>.GetSender: IMycDataProvider<T>;
|
||||
begin
|
||||
Result := FSender;
|
||||
end;
|
||||
|
||||
end.
|
||||
|
||||
Reference in New Issue
Block a user