From c9e28a946d72eb0b7b5d0ae0cfdb72543df19386 Mon Sep 17 00:00:00 2001 From: Michael Schimmel Date: Mon, 25 Aug 2025 14:48:06 +0200 Subject: [PATCH] Data Types --- Src/Data/Myc.Data.Types.Arrays.pas | 22 +++++ Src/Data/Myc.Data.Types.Enum.pas | 132 ++++++++++++++++++++++++++ Src/Data/Myc.Data.Types.Float.pas | 6 ++ Src/Data/Myc.Data.Types.Ordinal.pas | 6 ++ Src/Data/Myc.Data.Types.Records.pas | 26 +++++ Src/Data/Myc.Data.Types.Text.pas | 6 ++ Src/Data/Myc.Data.Types.Timestamp.pas | 6 ++ Src/Data/Myc.Data.Types.Tuple.pas | 22 +++++ Src/Data/Myc.Data.Types.pas | 36 ++++++- Src/Data/TestDataTypes.pas | 115 +++++++++++++++++++++- 10 files changed, 374 insertions(+), 3 deletions(-) create mode 100644 Src/Data/Myc.Data.Types.Enum.pas diff --git a/Src/Data/Myc.Data.Types.Arrays.pas b/Src/Data/Myc.Data.Types.Arrays.pas index 8fd46be..495f7bb 100644 --- a/Src/Data/Myc.Data.Types.Arrays.pas +++ b/Src/Data/Myc.Data.Types.Arrays.pas @@ -28,6 +28,7 @@ type FArrayType: IDataArrayType; FItems: TArray; function GetDataType: IDataType; + function GetAsString: string; function GetElementCount: Integer; function GetItem(Idx: Integer): IDataValue; public @@ -57,6 +58,27 @@ begin Result := FArrayType; end; +function TImplDataArrayValue.GetAsString: string; +var + sb: TStringBuilder; + i: Integer; +begin + sb := TStringBuilder.Create; + try + sb.Append('['); + for i := 0 to High(FItems) do + begin + sb.Append(FItems[i].AsString); + if (i < High(FItems)) then + sb.Append(', '); + end; + sb.Append(']'); + Result := sb.ToString; + finally + sb.Free; + end; +end; + function TImplDataArrayValue.GetElementCount: Integer; begin Result := Length(FItems); diff --git a/Src/Data/Myc.Data.Types.Enum.pas b/Src/Data/Myc.Data.Types.Enum.pas new file mode 100644 index 0000000..a4a56ed --- /dev/null +++ b/Src/Data/Myc.Data.Types.Enum.pas @@ -0,0 +1,132 @@ +unit Myc.Data.Types.Enum; + +interface + +uses + System.SysUtils, + System.Generics.Collections, + Myc.Data.Types; + +type + TImplDataEnumValue = class(TInterfacedObject, IDataEnumValue) + private + FDataType: IDataEnumType; + FValue: Integer; + function GetDataType: IDataType; + function GetAsString: string; + function GetValue: Integer; + public + constructor Create(const ADataType: IDataEnumType; const AValue: Integer); + end; + + TImplDataEnumType = class(TInterfacedObject, IDataEnumType) + private + FName: string; + FIdentifiers: TArray; + FIdentifierMap: TDictionary; + function GetName: String; + function GetKind: TDataKind; + function GetIdentifier(Idx: Integer): string; + function GetIdentifierCount: Integer; + function IndexOf(const AIdentifier: string): Integer; + function CreateValue(const AValue: Integer): IDataEnumValue; overload; + function CreateValue(const AIdentifier: string): IDataEnumValue; overload; + public + constructor Create(const AName: string; const AIdentifiers: array of string); + destructor Destroy; override; + end; + +implementation + +{ TImplDataEnumType } + +constructor TImplDataEnumType.Create(const AName: string; const AIdentifiers: array of string); +var + i: Integer; +begin + inherited Create; + FName := AName; + FIdentifierMap := TDictionary.Create; + SetLength(FIdentifiers, Length(AIdentifiers)); + for i := 0 to High(AIdentifiers) do + begin + if FIdentifierMap.ContainsKey(AIdentifiers[i]) then + raise EArgumentException.CreateFmt('Duplicate identifier: %s', [AIdentifiers[i]]); + FIdentifiers[i] := AIdentifiers[i]; + FIdentifierMap.Add(AIdentifiers[i], i); + end; +end; + +destructor TImplDataEnumType.Destroy; +begin + FIdentifierMap.Free; + inherited; +end; + +function TImplDataEnumType.GetName: String; +begin + Result := FName; +end; + +function TImplDataEnumType.GetKind: TDataKind; +begin + Result := dkEnum; +end; + +function TImplDataEnumType.GetIdentifier(Idx: Integer): string; +begin + Result := FIdentifiers[Idx]; +end; + +function TImplDataEnumType.GetIdentifierCount: Integer; +begin + Result := Length(FIdentifiers); +end; + +function TImplDataEnumType.IndexOf(const AIdentifier: string): Integer; +begin + if not FIdentifierMap.TryGetValue(AIdentifier, Result) then + Result := -1; +end; + +function TImplDataEnumType.CreateValue(const AValue: Integer): IDataEnumValue; +begin + if (AValue < 0) or (AValue >= Length(FIdentifiers)) then + raise EArgumentException.Create('Invalid enum value'); + Result := TImplDataEnumValue.Create(Self, AValue); +end; + +function TImplDataEnumType.CreateValue(const AIdentifier: string): IDataEnumValue; +var + idx: Integer; +begin + idx := IndexOf(AIdentifier); + if idx < 0 then + raise EArgumentException.CreateFmt('Unknown identifier: %s', [AIdentifier]); + Result := CreateValue(idx); +end; + +{ TImplDataEnumValue } + +constructor TImplDataEnumValue.Create(const ADataType: IDataEnumType; const AValue: Integer); +begin + FDataType := ADataType; + FValue := AValue; +end; + +function TImplDataEnumValue.GetDataType: IDataType; +begin + Result := FDataType; +end; + +function TImplDataEnumValue.GetAsString: string; +begin + Result := FDataType.Identifiers[FValue]; +end; + +function TImplDataEnumValue.GetValue: Integer; +begin + Result := FValue; +end; + +end. diff --git a/Src/Data/Myc.Data.Types.Float.pas b/Src/Data/Myc.Data.Types.Float.pas index 094d30c..1434870 100644 --- a/Src/Data/Myc.Data.Types.Float.pas +++ b/Src/Data/Myc.Data.Types.Float.pas @@ -16,6 +16,7 @@ type // IDataValue function GetDataType: IDataType; + function GetAsString: string; // IDataFloatValue function GetValue: Double; @@ -49,6 +50,11 @@ begin Result := TImplDataFloatType.Singleton; end; +function TImplDataFloatValue.GetAsString: string; +begin + Result := FloatToStr(FValue); +end; + function TImplDataFloatValue.GetValue: Double; begin Result := FValue; diff --git a/Src/Data/Myc.Data.Types.Ordinal.pas b/Src/Data/Myc.Data.Types.Ordinal.pas index de27d31..96e41e2 100644 --- a/Src/Data/Myc.Data.Types.Ordinal.pas +++ b/Src/Data/Myc.Data.Types.Ordinal.pas @@ -16,6 +16,7 @@ type // IDataValue function GetDataType: IDataType; + function GetAsString: string; // IDataOrdinalValue function GetValue: Int64; @@ -49,6 +50,11 @@ begin Result := TImplDataOrdinalType.Singleton; end; +function TImplDataOrdinalValue.GetAsString: string; +begin + Result := FValue.ToString; +end; + function TImplDataOrdinalValue.GetValue: Int64; begin Result := FValue; diff --git a/Src/Data/Myc.Data.Types.Records.pas b/Src/Data/Myc.Data.Types.Records.pas index 54b8036..5cfe38c 100644 --- a/Src/Data/Myc.Data.Types.Records.pas +++ b/Src/Data/Myc.Data.Types.Records.pas @@ -41,6 +41,7 @@ type FDataType: IDataRecordType; FItems: TArray; function GetDataType: IDataType; + function GetAsString: string; function GetItem(Idx: Integer): IDataValue; public constructor Create(const ADataType: IDataRecordType; const AItems: array of IDataValue); @@ -97,6 +98,31 @@ begin Result := FDataType; end; +function TImplDataRecordValue.GetAsString: string; +var + sb: TStringBuilder; + i: Integer; + recordType: IDataRecordType; +begin + recordType := FDataType; + sb := TStringBuilder.Create; + try + sb.Append('<'); + for i := 0 to High(FItems) do + begin + sb.Append(recordType.Fields[i].Name); + sb.Append(': '); + sb.Append(FItems[i].AsString); + if (i < High(FItems)) then + sb.Append(', '); + end; + sb.Append('>'); + Result := sb.ToString; + finally + sb.Free; + end; +end; + function TImplDataRecordValue.GetItem(Idx: Integer): IDataValue; begin Result := FItems[Idx]; diff --git a/Src/Data/Myc.Data.Types.Text.pas b/Src/Data/Myc.Data.Types.Text.pas index 5739b76..74237eb 100644 --- a/Src/Data/Myc.Data.Types.Text.pas +++ b/Src/Data/Myc.Data.Types.Text.pas @@ -23,6 +23,7 @@ type private FValue: string; function GetDataType: IDataType; + function GetAsString: string; function GetValue: string; public constructor Create(const AValue: string); @@ -65,6 +66,11 @@ begin Result := TImplDataTextType.Singleton; end; +function TImplDataTextValue.GetAsString: string; +begin + Result := FValue; +end; + function TImplDataTextValue.GetValue: string; begin Result := FValue; diff --git a/Src/Data/Myc.Data.Types.Timestamp.pas b/Src/Data/Myc.Data.Types.Timestamp.pas index 377c453..d18b959 100644 --- a/Src/Data/Myc.Data.Types.Timestamp.pas +++ b/Src/Data/Myc.Data.Types.Timestamp.pas @@ -23,6 +23,7 @@ type private FValue: TDateTime; function GetDataType: IDataType; + function GetAsString: string; function GetValue: TDateTime; public constructor Create(const AValue: TDateTime); @@ -65,6 +66,11 @@ begin Result := TImplDataTimestampType.Singleton; end; +function TImplDataTimestampValue.GetAsString: string; +begin + Result := DateTimeToStr(FValue); +end; + function TImplDataTimestampValue.GetValue: TDateTime; begin Result := FValue; diff --git a/Src/Data/Myc.Data.Types.Tuple.pas b/Src/Data/Myc.Data.Types.Tuple.pas index f68220b..f905e20 100644 --- a/Src/Data/Myc.Data.Types.Tuple.pas +++ b/Src/Data/Myc.Data.Types.Tuple.pas @@ -12,6 +12,7 @@ type private FItems: TArray; function GetDataType: IDataType; + function GetAsString: string; function GetItemCount: Integer; function GetItem(Idx: Integer): IDataValue; public @@ -51,6 +52,27 @@ begin Result := TImplDataTupleType.Singleton; end; +function TImplDataTupleValue.GetAsString: string; +var + sb: TStringBuilder; + i: Integer; +begin + sb := TStringBuilder.Create; + try + sb.Append('('); + for i := 0 to High(FItems) do + begin + sb.Append(FItems[i].AsString); + if (i < High(FItems)) then + sb.Append(', '); + end; + sb.Append(')'); + Result := sb.ToString; + finally + sb.Free; + end; +end; + function TImplDataTupleValue.GetItem(Idx: Integer): IDataValue; begin Result := FItems[Idx]; diff --git a/Src/Data/Myc.Data.Types.pas b/Src/Data/Myc.Data.Types.pas index 7d715d4..2dd14eb 100644 --- a/Src/Data/Myc.Data.Types.pas +++ b/Src/Data/Myc.Data.Types.pas @@ -7,7 +7,7 @@ uses System.SysUtils; type - TDataKind = (dkOrdinal, dkFloat, dkText, dkTimestamp, dkRecord, dkTuple, dkArray); + TDataKind = (dkOrdinal, dkFloat, dkText, dkTimestamp, dkRecord, dkTuple, dkArray, dkEnum); IDataType = interface function GetName: String; @@ -18,7 +18,9 @@ type IDataValue = interface function GetDataType: IDataType; + function GetAsString: string; property DataType: IDataType read GetDataType; + property AsString: string read GetAsString; end; IDataOrdinalValue = interface(IDataValue) @@ -57,6 +59,21 @@ type function CreateValue(const AValue: TDateTime): IDataTimestampValue; end; + IDataEnumValue = interface(IDataValue) + function GetValue: Integer; + property Value: Integer read GetValue; + end; + + IDataEnumType = interface(IDataType) + function GetIdentifier(Idx: Integer): string; + function GetIdentifierCount: Integer; + function IndexOf(const AIdentifier: string): Integer; + function CreateValue(const AValue: Integer): IDataEnumValue; overload; + function CreateValue(const AIdentifier: string): IDataEnumValue; overload; + property IdentifierCount: Integer read GetIdentifierCount; + property Identifiers[Idx: Integer]: string read GetIdentifier; default; + end; + IDataRecordValue = interface(IDataValue) function GetItem(Idx: Integer): IDataValue; property Items[Idx: Integer]: IDataValue read GetItem; default; @@ -127,6 +144,7 @@ type class function ArrayOfType(const AElementType: IDataType): IDataArrayType; static; class function RecordOf(const Fields: TArray): IDataRecordType; overload; static; class function RecordOf(const Fields: array of TRecordField): IDataRecordType; overload; static; + class function EnumOf(const AName: string; const AIdentifiers: array of string): IDataEnumType; static; property DataType: IDataType read FDataType; property Name: String read GetName; @@ -147,6 +165,7 @@ type function AsRecord: IDataRecordValue; function AsTuple: IDataTupleValue; function AsArray: IDataArrayValue; + function AsEnum: IDataEnumValue; class operator Implicit(const A: IDataValue): TDataValue; overload; class operator Implicit(const A: TDataValue): IDataValue; overload; @@ -173,7 +192,8 @@ uses Myc.Data.Types.Timestamp, Myc.Data.Types.Arrays, Myc.Data.Types.Records, - Myc.Data.Types.Tuple; + Myc.Data.Types.Tuple, + Myc.Data.Types.Enum; { TRecordField } @@ -262,6 +282,11 @@ begin Result := RecordOf(tFields); end; +class function TDataType.EnumOf(const AName: string; const AIdentifiers: array of string): IDataEnumType; +begin + Result := TImplDataEnumType.Create(AName, AIdentifiers); +end; + class function TDataType.Text: IDataTextType; begin Result := TImplDataTextType.Singleton; @@ -343,6 +368,13 @@ begin Result := IDataTupleValue(FDataValue); end; +function TDataValue.AsEnum: IDataEnumValue; +begin + if FDataValue.DataType.Kind <> dkEnum then + raise EInvalidCast.Create('Enum expected'); + Result := IDataEnumValue(FDataValue); +end; + class function TDataValue.FromOrdinal(const AValue: Int64): IDataOrdinalValue; begin Result := TImplDataOrdinalType.Singleton.CreateValue(AValue); diff --git a/Src/Data/TestDataTypes.pas b/Src/Data/TestDataTypes.pas index baea821..f11a4a0 100644 --- a/Src/Data/TestDataTypes.pas +++ b/Src/Data/TestDataTypes.pas @@ -23,6 +23,12 @@ type [Test] procedure TestTimestamps; + + [Test] + procedure TestEnums; + + [Test] + procedure TestAsString; end; implementation @@ -253,7 +259,7 @@ begin Assert.AreEqual(dkTimestamp, tsType.Kind, 'Type kind should be dkTimestamp'); // --- 2. Test Value Creation and Access --- - now := Now; + now := System.Sysutils.Now; tsValue1 := TDataValue.FromTimestamp(now); Assert.IsNotNull(tsValue1, 'TimestampValue should be created'); @@ -277,6 +283,113 @@ begin ); end; +procedure TMyTestObject.TestEnums; +var + colorType: IDataEnumType; + red, green, blue: IDataEnumValue; +begin + // --- 1. Test Type Creation --- + colorType := TDataType.EnumOf('Color', ['Red', 'Green', 'Blue']); + + Assert.IsNotNull(colorType, 'EnumType should be created'); + Assert.AreEqual('Color', colorType.Name, 'Name should be Color'); + Assert.AreEqual(dkEnum, colorType.Kind, 'Kind should be dkEnum'); + Assert.AreEqual(3, colorType.IdentifierCount, 'IdentifierCount should be 3'); + Assert.AreEqual('Red', colorType.Identifiers[0], 'Identifier at index 0 should be Red'); + Assert.AreEqual('Green', colorType.Identifiers[1], 'Identifier at index 1 should be Green'); + Assert.AreEqual('Blue', colorType.Identifiers[2], 'Identifier at index 2 should be Blue'); + Assert.AreEqual(1, colorType.IndexOf('Green'), 'IndexOf Green should be 1'); + Assert.AreEqual(-1, colorType.IndexOf('Yellow'), 'IndexOf Yellow should be -1'); + + // --- 2. Test Value Creation and Access --- + red := colorType.CreateValue(0); + green := colorType.CreateValue('Green'); + blue := TDataValue(colorType.CreateValue(2)).AsEnum; + + Assert.IsNotNull(red, 'EnumValue from index should be created'); + Assert.AreSame(colorType, red.DataType, 'Red should have the correct data type'); + Assert.AreEqual(0, red.Value, 'Value of Red should be 0'); + Assert.AreEqual('Red', red.AsString, 'AsText of Red should be Red'); + + Assert.IsNotNull(green, 'EnumValue from identifier should be created'); + Assert.AreSame(colorType, green.DataType, 'Green should have the correct data type'); + Assert.AreEqual(1, green.Value, 'Value of Green should be 1'); + Assert.AreEqual('Green', green.AsString, 'AsText of Green should be Green'); + + Assert.AreEqual(2, blue.Value, 'Value of Blue should be 2'); + + // --- 3. Test Validation and Error Handling --- + // Test creating a type with duplicate identifiers + Assert.WillRaise( + procedure begin TDataType.EnumOf('Fails', ['A', 'B', 'A']); end, + EArgumentException, + 'Duplicate identifiers should raise an exception' + ); + + // Test creating a value with an invalid index + Assert.WillRaise(procedure begin colorType.CreateValue(3); end, EArgumentException, 'Invalid index should raise an exception'); + Assert.WillRaise(procedure begin colorType.CreateValue(-1); end, EArgumentException, 'Negative index should raise an exception'); + + // Test creating a value with an unknown identifier + Assert.WillRaise( + procedure begin colorType.CreateValue('Yellow'); end, + EArgumentException, + 'Unknown identifier should raise an exception' + ); +end; + +procedure TMyTestObject.TestAsString; +var + ordinalValue: IDataOrdinalValue; + floatValue: IDataFloatValue; + textValue: IDataTextValue; + tsValue: IDataTimestampValue; + enumValue: IDataEnumValue; + arrayValue: IDataArrayValue; + recordValue: IDataRecordValue; + tupleValue: IDataTupleValue; + colorType: IDataEnumType; + personType: IDataRecordType; + intArrayType: IDataArrayType; + now: TDateTime; +begin + // Ordinal + ordinalValue := TDataValue.FromOrdinal(123); + Assert.AreEqual('123', ordinalValue.AsString, 'Ordinal AsString incorrect'); + + // Float + floatValue := TDataValue.FromFloat(45.67); + Assert.AreEqual(FloatToStr(45.67), floatValue.AsString, 'Float AsString incorrect'); + + // Text + textValue := TDataValue.FromText('Hello'); + Assert.AreEqual('Hello', textValue.AsString, 'Text AsString incorrect'); + + // Timestamp + now := System.Sysutils.Now; + tsValue := TDataValue.FromTimestamp(now); + Assert.AreEqual(DateTimeToStr(now), tsValue.AsString, 'Timestamp AsString incorrect'); + + // Enum + colorType := TDataType.EnumOf('Color', ['Red', 'Green', 'Blue']); + enumValue := colorType.CreateValue('Green'); + Assert.AreEqual('Green', enumValue.AsString, 'Enum AsString incorrect'); + + // Array + intArrayType := TDataType.ArrayOfType(TDataType.Ordinal); + arrayValue := intArrayType.CreateValue([TDataValue.FromOrdinal(1), TDataValue.FromOrdinal(2)]); + Assert.AreEqual('[1, 2]', arrayValue.AsString, 'Array AsString incorrect'); + + // Record + personType := TDataType.RecordOf([TRecordField.Create('ID', TDataType.Ordinal), TRecordField.Create('Name', TDataType.Text)]); + recordValue := personType.CreateValue([TDataValue.FromOrdinal(1), TDataValue.FromText('Bob')]); + Assert.AreEqual('', recordValue.AsString, 'Record AsString incorrect'); + + // Tuple + tupleValue := TDataValue.FromTuple([TDataValue.FromOrdinal(10), TDataValue.FromText('Tuple')]); + Assert.AreEqual('(10, Tuple)', tupleValue.AsString, 'Tuple AsString incorrect'); +end; + initialization TDUnitX.RegisterTestFixture(TMyTestObject);