From 87fb75a01a955a4b83bd692d76d55a145370aefa Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:04:12 +0100 Subject: [PATCH 01/15] Add new TIntegerRange adv record to structs category A new source code file was added for TIntegerRange. Meta data for TIntegerRange was added to structs.ini. --- collection/706.dat | 261 +++++++++++++++++++++++++++++++++++++++++ collection/structs.ini | 20 +++- 2 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 collection/706.dat diff --git a/collection/706.dat b/collection/706.dat new file mode 100644 index 0000000..d5b92f7 --- /dev/null +++ b/collection/706.dat @@ -0,0 +1,261 @@ +type + TIntegerRange = record + strict private + var + fLowerBound: Integer; + fUpperBound: Integer; + function GetLowerBound: Integer; + function GetUpperBound: Integer; + function IsSubrangeOf(const ARange: TIntegerRange): Boolean; + public + // Constructs a range whose bounds are A and B. The lowest of the two + // parameters is taken as the lower bound of the range with the other + // parameter taken as the upper bound. + // Valid bounds must fall in the range -MaxInt..MaxInt. An + // EArgumentException exception is raised otherwise. + constructor Create(const A, B: Integer); + + // Constructs an empty range. + class function CreateEmpty: TIntegerRange; static; + + // Checks if the range is empty. + function IsEmpty: Boolean; + + // Returns the length of the range, i.e. the number of integers in the range. + function Length: Cardinal; + + // Constrains AValue to fall within this range. If the value lies within the + // range it is returned unchanged. If it is outside the range then either + // LowerBound or UpperBound is returned, depending on whether the value + // falls below or above the range, respectively. + // An EInvalidOpException exception is raised if the range is empty. + function Constrain(const AValue: Integer): Integer; + + // Checks if this range overlaps with ARange, i.e. the interection of the + // ranges is non empty. Empty ranges cannot overlap with any range. + function OverlapsWith(const ARange: TIntegerRange): Boolean; + + // Checks if this range is immediately adjacent to ARange, with no overlap. + // Empty ranges are never contiguous with other ranges or themselves. + function IsContiguousWith(const ARange: TIntegerRange): Boolean; + + // Checks if the set of all values in this range and ARange form a + // continuous sequence. This implies that a range is continuous with itself. + // Since adding an empty range to a non-empty range doesn't change the + // non-empty range we define empty ranges to be continuous with any range. + function IsContinuousWith(const ARange: TIntegerRange): Boolean; + + // Checks if ranges A and B are the same + class operator Equal(const A, B: TIntegerRange): Boolean; + + // Checks if ranges A and B are not the same + class operator NotEqual(const A, B: TIntegerRange): Boolean; + + // Checks if range A is contained in, or is the same as, range B. + // An empty range is deemed to be contained in any other range. + class operator LessThanOrEqual(const A, B: TIntegerRange): Boolean; + + // Checks if range A is contains, or is the same as, range B. + // A non-empty range is never contained in an empty range. + class operator GreaterThanOrEqual(const A, B: TIntegerRange): Boolean; + + // Combine two ranges, A and B. The result is the smallest range that + // contains both A and B. + // If A and B are not continuous the resulting range will contain values + // that were not in either A or B. + // Combining any range either with itself or with an empty range is a no-op. + class operator Add(const A, B: TIntegerRange): TIntegerRange; + + // Returns a range that is the intersection of ranges A and B. + // Returns an empty range if A and B do not overlap. + class operator Multiply(const A, B: TIntegerRange): TIntegerRange; + + // Checks if integer AValue is contained within range ARange. + class operator In(const AValue: Integer; const ARange: TIntegerRange): + Boolean; + + // Implicitly casts ARange to a string. If ARange is non-empty the string + // has format [X..Y], where X and Y are the lower and upper bounds of + // ARange respectively. If ARange is empty then [] is returned. + // This means that ARange can be assigned directly to a string. + class operator Implicit(const ARange: TIntegerRange): string; + + // Explicitly casts ARange to a string. If ARange is non-empty the string + // has format [X..Y], where X and Y are the lower and upper bounds of + // ARange respectively. If ARange is empty then [] is returned. + // This means that ARange can be explicitly cast to a string using + // string(ARange). + class operator Explicit(const ARange: TIntegerRange): string; + + // The lower bound of a non-empty range. + // EInvalidOpException is raised if the property is read when the range is + // empty. + property LowerBound: Integer read GetLowerBound; + + // The upper bound of a non-empty range. + // EInvalidOpException is raised if the property is read when the range is + // empty. + property UpperBound: Integer read GetUpperBound; + end; + +class operator TIntegerRange.Add(const A, B: TIntegerRange): TIntegerRange; +begin + if A.IsEmpty then + Exit(B); + if B.IsEmpty then + Exit(A); + Result := TIntegerRange.Create( + Math.Min(A.fLowerBound, B.fLowerBound), + Math.Max(A.fUpperBound, B.fUpperBound) + ); +end; + +function TIntegerRange.Constrain(const AValue: Integer): Integer; +begin + if IsEmpty then + raise Sysutils.EInvalidOpException.Create( + 'TIntegerRange.Constrain not valid for an empty range.' + ); + Result := Math.EnsureRange(AValue, fLowerBound, fUpperBound); +end; + +constructor TIntegerRange.Create(const A, B: Integer); +begin + // Normalise range so that smallest parameter is the lower bound + fLowerBound := Math.Min(A, B); + fUpperBound := Math.Max(A, B); + if fLowerBound = Low(Integer) then + // This restriction is required to prevent the Length method's Cardinal + // return value from wrapping around / overflowing + raise SysUtils.EArgumentException.CreateFmt( + 'TIntegerRange.Create: Arguments must be greater than %d', [Low(Integer)] + ); +end; + +class function TIntegerRange.CreateEmpty: TIntegerRange; +begin + Result.fLowerBound := High(Integer); + Result.fUpperBound := Low(Integer); +end; + +class operator TIntegerRange.Equal(const A, B: TIntegerRange): Boolean; +begin + if A.IsEmpty or B.IsEmpty then + Exit(A.IsEmpty and B.IsEmpty); + Result := (A.fLowerBound = B.fLowerBound) and (A.fUpperBound = B.fUpperBound); +end; + +class operator TIntegerRange.Explicit(const ARange: TIntegerRange): string; +begin + if ARange.IsEmpty then + Exit('[]'); + Result := SysUtils.Format( + '[%d..%d]', [ARange.fLowerBound, ARange.fUpperBound] + ); +end; + +function TIntegerRange.GetLowerBound: Integer; +begin + if IsEmpty then + raise Sysutils.EInvalidOpException.Create( + 'TIntegerRange.LowerBound not valid for an empty range.' + ); + Result := fLowerBound; +end; + +function TIntegerRange.GetUpperBound: Integer; +begin + if IsEmpty then + raise Sysutils.EInvalidOpException.Create( + 'TIntegerRange.LowerBound not valid for an empty range.' + ); + Result := fUpperBound; +end; + +class operator TIntegerRange.GreaterThanOrEqual(const A, B: TIntegerRange): + Boolean; +begin + Result := B.IsSubrangeOf(A); +end; + +class operator TIntegerRange.Implicit(const ARange: TIntegerRange): string; +begin + Result := string(ARange); // calls Explicit cast operator +end; + +class operator TIntegerRange.In(const AValue: Integer; + const ARange: TIntegerRange): Boolean; +begin + if ARange.IsEmpty then + Exit(False); + Result := (AValue >= ARange.fLowerBound) and (AValue <= ARange.fUpperBound); +end; + +function TIntegerRange.IsContiguousWith(const ARange: TIntegerRange): Boolean; +begin + if Self.IsEmpty or ARange.IsEmpty then + Exit(False); + Result := (Self + ARange).Length = (Self.Length + ARange.Length); +end; + +function TIntegerRange.IsContinuousWith(const ARange: TIntegerRange): Boolean; +begin + if Self.IsEmpty or ARange.IsEmpty then + // Empty ranges are only continuous with other empty ranges + Exit(True); + Result := IsContiguousWith(ARange) or OverlapsWith(ARange); +end; + +function TIntegerRange.IsEmpty: Boolean; +begin + Result := fLowerBound > fUpperBound; +end; + +function TIntegerRange.IsSubrangeOf(const ARange: TIntegerRange): Boolean; +begin + if ARange.IsEmpty then + Exit(Self.IsEmpty); + Result := (Self.fLowerBound >= ARange.fLowerBound) + and (Self.fUpperBound <= ARange.fUpperBound) + or Self.IsEmpty +end; + +function TIntegerRange.Length: Cardinal; +begin + if IsEmpty then + Exit(0); + Result := fUpperBound - fLowerBound + 1 +end; + +class operator TIntegerRange.LessThanOrEqual(const A, B: TIntegerRange): + Boolean; +begin + Result := A.IsSubrangeOf(B); +end; + +class operator TIntegerRange.Multiply(const A, B: TIntegerRange): TIntegerRange; +var + Up, Lo: Integer; +begin + if A.IsEmpty or B.IsEmpty then + Exit(TIntegerRange.CreateEmpty); + Lo := Math.Max(A.fLowerBound, B.fLowerBound); + Up := Math.Min(A.fUpperBound, B.fUpperBound); + if Lo <= Up then + Result := TIntegerRange.Create(Lo, Up) + else + Result := TIntegerRange.CreateEmpty; +end; + +class operator TIntegerRange.NotEqual(const A, B: TIntegerRange): Boolean; +begin + if A.IsEmpty or B.IsEmpty then + Exit(A.IsEmpty <> B.IsEmpty); + Result := (A.fLowerBound <> B.fLowerBound) + or (A.fUpperBound <> B.fUpperBound); +end; + +function TIntegerRange.OverlapsWith(const ARange: TIntegerRange): Boolean; +begin + Result := not (Self * ARange).IsEmpty; +end; \ No newline at end of file diff --git a/collection/structs.ini b/collection/structs.ini index f338d9d..2fb328f 100644 --- a/collection/structs.ini +++ b/collection/structs.ini @@ -57,7 +57,7 @@ FPC=Y DescEx="

Constructs and returns a TRange record with bounds A and B.

The smaller of A and B is used as the lower bound and the larger as the upper bound. If both values are equal then the range will be empty.

" Depends=TRange SeeAlso=TRange -SeeAlso=TRange,TRangeEx +SeeAlso=TRange,TRangeEx,TIntegerRange Snip=580.dat Delphi7=Y Delphi2005Win32=Y @@ -122,6 +122,20 @@ Delphi10S=Y Delphi12A=Y FPC=Y +[TIntegerRange] +DisplayName=TIntegerRange +DescEx="

An advanced record that encapsulates an integer range along with operations on it.

The range is immutable, so the constructor must be used to instantiate a non-empty range.

" +Extra="

TIntegerRange supports various operator overloads, which operate in a way similar to the Pascal set operators:

= compares two ranges for equality.

<> compares two ranges for inequality.

<= checks if the left operand is wholly contained in, or is equal to the right operand (c.f. Pascal subset operator).

>= checks if the left operand wholly contains, or is equal to, the right hand operand (c.f. Pascal proper superset operator).

+ creates the union of the two ranges specified by the operands, i.e. a new range that is the smallest range that contains both operands. For example, representing a range as [A..B], [1..3] + [7..10] = [1..10] (c.f. Pascal union operator).

* creates a new range that the largest range contained in both ranges specified by the operands, or an empty range if the operands do not overlap. For example, [1..3] * [2..7] = [2..3] while [1..3] * [6..9] is an empty range. (c.f. Pascal intersection operator).

in checks if the integer specified in the left hand operand is contained in the range specified by the right hand operand (c.f. Pascal membership operator).

" +Kind=class +Units=SysUtils,Math +SeeAlso=TRange,TRangeEx +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" +Snip=706.dat +DelphiXE=Y +Delphi12A=Y + [TPointF] Kind=type DescEx="

Encapsulates a point with double precision floating point coordinates.

" @@ -150,7 +164,7 @@ FPC=Y Kind=type DescEx="

Encapsulates the upper and lower bounds of a range of values.

" SeeAlso=Range -SeeAlso=Range,TRangeEx +SeeAlso=Range,TRangeEx,TIntegerRange Snip=579.dat Delphi7=Y Delphi2005Win32=Y @@ -195,7 +209,7 @@ FPC=Y Kind=class DescEx="

Encapsulates a range of integers with a methods to test whether a value falls within the range and to adjust the value to fit.

" Units=Math -SeeAlso=Range,TRange +SeeAlso=Range,TRange,TIntegerRange Snip=578.dat Delphi2=N Delphi3=N From 857a29a5ba4f664af2f4bc4782285b2022e6f6df Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:06:01 +0100 Subject: [PATCH 02/15] Add unit tests for new TIntegerRange adv records Created a new DUnit test project for the Structures category, for compilation with Delphi XE only. This is TestCatStructsXE.dpr/.dproj Added unit tests for TIntegerRange to the a new TestUStructCatSnippets unit. Generated a new UStructCatSnippets unit from CodeSnip that includes TIntegerRange. --- tests/Cat-Structs/TestCatStructsXE.dpr | 35 + tests/Cat-Structs/TestCatStructsXE.dproj | 118 ++++ tests/Cat-Structs/TestCatStructsXE.res | Bin 0 -> 91336 bytes tests/Cat-Structs/TestUStructCatSnippets.pas | 656 +++++++++++++++++++ tests/Cat-Structs/UStructCatSnippets.pas | 519 +++++++++++++++ 5 files changed, 1328 insertions(+) create mode 100644 tests/Cat-Structs/TestCatStructsXE.dpr create mode 100644 tests/Cat-Structs/TestCatStructsXE.dproj create mode 100644 tests/Cat-Structs/TestCatStructsXE.res create mode 100644 tests/Cat-Structs/TestUStructCatSnippets.pas create mode 100644 tests/Cat-Structs/UStructCatSnippets.pas diff --git a/tests/Cat-Structs/TestCatStructsXE.dpr b/tests/Cat-Structs/TestCatStructsXE.dpr new file mode 100644 index 0000000..cc5d08d --- /dev/null +++ b/tests/Cat-Structs/TestCatStructsXE.dpr @@ -0,0 +1,35 @@ +program TestCatStructsXE; +{ + +Delphi DUnit Test Project +------------------------- +This project contains the DUnit test framework and the GUI/Console test runners. +Add "CONSOLE_TESTRUNNER" to the conditional defines entry in the project options +to use the console test runner. Otherwise the GUI test runner will be used by +default. + +} + +{$IFDEF CONSOLE_TESTRUNNER} +{$APPTYPE CONSOLE} +{$ENDIF} + +uses + Forms, + TestFramework, + GUITestRunner, + TextTestRunner, + TestUStructCatSnippets in 'TestUStructCatSnippets.pas', + UStructCatSnippets in 'UStructCatSnippets.pas'; + +{$R *.RES} + +begin + Application.Initialize; + if IsConsole then + with TextTestRunner.RunRegisteredTests do + Free + else + GUITestRunner.RunRegisteredTests; +end. + diff --git a/tests/Cat-Structs/TestCatStructsXE.dproj b/tests/Cat-Structs/TestCatStructsXE.dproj new file mode 100644 index 0000000..49810fe --- /dev/null +++ b/tests/Cat-Structs/TestCatStructsXE.dproj @@ -0,0 +1,118 @@ + + + {CEAD8332-8CBB-45F7-BC9C-CF3ED093133E} + TestCatStructsXE.dpr + 12.3 + True + Debug + Win32 + Application + VCL + DCC32 + + + true + + + true + Base + true + + + true + Base + true + + + $(BDS)\Source\DUnit\src;$(DCC_UnitSearchPath) + 00400000 + _build\Exe + WinTypes=Windows;WinProcs=Windows;DbiTypes=BDE;DbiProcs=BDE;$(DCC_UnitAlias) + _CONSOLE_TESTRUNNER;$(DCC_Define) + _build\Bin\Cat-Structs + false + false + false + false + false + + + DEBUG;$(DCC_Define) + false + true + + + false + RELEASE;$(DCC_Define) + 0 + false + + + + MainSource + + + + + Cfg_2 + Base + + + Base + + + Cfg_1 + Base + + + + + + Delphi.Personality.12 + + + + + False + False + 1 + 0 + 0 + 0 + False + False + False + False + False + 2057 + 1252 + + + + + 1.0.0.0 + + + + + + 1.0.0.0 + + + + TestCatStructsXE.dpr + + + + True + + + DUnit / Delphi Win32 + GUI + C:\Code\Repos\delphidabbler\code-snippets\tests\Cat-Hex\TestHexCat.dproj + + + + 12 + + diff --git a/tests/Cat-Structs/TestCatStructsXE.res b/tests/Cat-Structs/TestCatStructsXE.res new file mode 100644 index 0000000000000000000000000000000000000000..2c52f2037bee971d7ea57132d1748031a60cd6f4 GIT binary patch literal 91336 zcmV)uK$gD%0000W00030{{R60{{R300000000000000000000?9{~U$00030{{sO3 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ⅇBW= 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⁢dP zY!#-As+8EqMK1T=x`XnSN+qnxKm9M8l?PC*1<0yNsFUPuD4oD$GXAMkCrf@o$t5DtG`U8My2Kar72T)0&z{O{k468L90bsBQK)m6qiCF|N84B?JJ;R8lrihvh);+H(N!y|)j3pC_CDn{`(GOM%&Ku(~xEM91Vw z6KK<&fCPmSI)&GDCz|lyp;M@DY{QHRRfoTJ-!tH>)=ru-Hm%R1={kKnDB}e`4gD-42eWrae3kOUeUx^!+ z%vJ#4cH`8k2JGCm7pG63fy?PqFVv?5XE$eOvi|8M`UaeljG>~k3^QgsTj!=`bTmDj_rov z8g2l*X>Ay)VZ})sTqA`T z9Cn|mKSd#cJAE4GZgbA40i}C@n4m-eP*p|tHe(8^t16J9^W=8pmo~hI-A7IeK`1X6 z*!bOV+`B}M@UzEX+i>JW)8gZ&+Ax_6>$5j6LL#Mf{yW+`uye;=95`?UZcEcoYVg9t zC!=K13(*ifdvcJCRIESFT!2|KXOLSm9uWcr-RAZmK8WtO-$u`&{iunygI~cE55T3$ zg#H^Wq2HY<4xXDG|xnmESn_JS{oAWQL>(D+|i+95t z?3mFbFmv`al$Mo9KEUJf;@2;(L*22Hs4Ol(RY~E-zxnd53ILvZb<2jG`;IN%d+;PC zjv)ZJb*U%=yIHp^62Wdh4BM>e04w1KM_M$C%D0xO6omK>GN3k}!Qa;wfe}(D4Wa=zFaw zAC3vtB3-6A+)Am{`XJBh^19p&_@S)9+Wk59Z>HV0b8^R+dc698<_z_et;IrcY? zHb4u7Brg@C9lJN7n13-I3PJ{!uo7_wPu23}qrIpcCu^o6=?&1mt~N*0DqFkB!@Yjl zOiZe+LO7aKz@R7L^?0!D-96Z`eYYqEGbiApD?J)ajoj17&LWX8rcIlKIdf(p5DdWJ z@+8^Suby6my@yX?N^K3MjvKl0Q#UPE0I+laiG8oG-!XChwgUtJ)wt)Tg(xg2A^>ob z+wa7t&D-$W>NnYKK*r6_BKv4a_c>>s88TslA@s)Sv!`O^4Ob&ZAGXd7N37V>+<>la z+t9sj3(Ak|SE^MkS;brd77=x7(BnoL0NDM~3Uc8ZQ%TPgKzKb;onYpmba3^TM!F0D zL;74Vb?k31TRTR>*`0ryVWx>SVpv@i_t!UwEEqHDxTmito{}$~1=yX6OL!`RWpv+$ z0)=R(s6|up7$nKq9oD_6Wa3OzR~F;iMKdv~rVLTu(B##5EO?IC+4k+b;Nft405`76 zWmw}z1V|jxNqF@&S7834+2oj~M6xW@+l${m^E&DdpTdF}cF$d#DSHndffZykp-T$SRUWMD~R_?!5cg&_Ro)wE*a&+on zB~Cb=-2ZEDxC%az)uK~o1MloY{O}<}w{3!Z?dxhfknWK7NR1_tG{p+k7yvA9LMI?{ zR+qr^x{(U{rElRf0Q6lSCIB$=ujJET`M|sXN6Gsa**iOI%znZWkdfG&?>)Njw6-oJ z&$LSX&%ORp`U+9pAL41iNfy~i3NSbsEHo$Da!YY+

xUN|6$sOVze+x2u!!Vae>t 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 R, '#1a'); +// CheckFalse(R > L, '#1b'); +// L := TIntegerRange.Create(0, 1); +// R := TIntegerRange.Create(0, 0); +// CheckTrue(L > R, '#2a'); +// CheckFalse(R > L, '#2b'); +// L := TIntegerRange.Create(-3, 12); +// R := TIntegerRange.Create(-5, 18); +// CheckFalse(L > R, '#3a'); +// CheckTrue(R > L, '#3b'); +// L := TIntegerRange.Create(-5, 17); +// R := TIntegerRange.Create(-5, 18); +// CheckFalse(L > R, '#4a'); +// CheckTrue(R > L, '#4b'); +// L := TIntegerRange.Create(-4, 18); +// R := TIntegerRange.Create(-5, 18); +// CheckFalse(L > R, '#5a'); +// CheckTrue(R > L, '#5b'); +// L := TIntegerRange.Create(-5, 18); +// R := TIntegerRange.Create(-5, 18); +// CheckFalse(L > R, '#6a'); +// CheckFalse(R > L, '#6b'); +// L := TIntegerRange.Create(-5, 2); +// R := TIntegerRange.Create(4, 18); +// CheckFalse(L > R, '#7a'); +// CheckFalse(R > L, '#7b'); +// L := TIntegerRange.Create(1, 1); +// R := TIntegerRange.Create(4, 18); +// CheckFalse(L > R, '#8a'); +// CheckFalse(R > L, '#8b'); +// L := TIntegerRange.CreateEmpty; +// R := TIntegerRange.CreateEmpty; +// CheckFalse(L > R, '#9'); +// L := TIntegerRange.Create(10, 10); +// R := TIntegerRange.CreateEmpty; +// CheckTrue(L > R, '#10a'); +// CheckFalse(R > L, '#10b'); +//end; + +procedure TestTIntegerRange.TestGreaterThanOrEqualOp; +var + L, R: TIntegerRange; +begin + L := TIntegerRange.Create(0, 0); + R := TIntegerRange.Create(0, 0); + CheckTrue(L >= R, '#1a'); + CheckTrue(R >= L, '#1b'); + L := TIntegerRange.Create(0, 1); + R := TIntegerRange.Create(0, 0); + CheckTrue(L >= R, '#2a'); + CheckFalse(R >= L, '#2b'); + L := TIntegerRange.Create(-3, 12); + R := TIntegerRange.Create(-5, 18); + CheckFalse(L >= R, '#3a'); + CheckTrue(R >= L, '#3b'); + L := TIntegerRange.Create(-5, 17); + R := TIntegerRange.Create(-5, 18); + CheckFalse(L >= R, '#4a'); + CheckTrue(R >= L, '#4b'); + L := TIntegerRange.Create(-4, 18); + R := TIntegerRange.Create(-5, 18); + CheckFalse(L >= R, '#5a'); + CheckTrue(R >= L, '#5b'); + L := TIntegerRange.Create(-5, 18); + R := TIntegerRange.Create(-5, 18); + CheckTrue(L >= R, '#6a'); + CheckTrue(R >= L, '#6b'); + L := TIntegerRange.Create(-5, 2); + R := TIntegerRange.Create(4, 18); + CheckFalse(L >= R, '#7a'); + CheckFalse(R >= L, '#7b'); + L := TIntegerRange.Create(1, 1); + R := TIntegerRange.Create(4, 18); + CheckFalse(L >= R, '#8a'); + CheckFalse(R >= L, '#8b'); + L := TIntegerRange.CreateEmpty; + R := TIntegerRange.CreateEmpty; + Checktrue(L >= R, '#9'); + L := TIntegerRange.Create(10, 10); + R := TIntegerRange.CreateEmpty; + CheckTrue(L >= R, '#10a'); + CheckFalse(R >= L, '#10b'); +end; + +procedure TestTIntegerRange.TestImplicitToStringOp; +var + R: TIntegerRange; +begin + R := TIntegerRange.CreateEmpty; + CheckEquals('[]', R); + R := TIntegerRange.Create(0, 0); + CheckEquals('[0..0]', R); + R := TIntegerRange.Create(-42, 56); + CheckEquals('[-42..56]', R); +end; + +procedure TestTIntegerRange.TestInOp; +var + R: TIntegerRange; +begin + R := TIntegerRange.Create(0, 0); + CheckTrue(0 in R, '#1'); + CheckFalse(-2 in R, '#2'); + R := TIntegerRange.Create(-42, 56); + CheckTrue(0 in R, '#3'); + CheckTrue(-2 in R, '#4'); + CheckTrue(-42 in R, '#5'); + CheckTrue(56 in R, '#6'); + CheckFalse(-43 in R, '#7'); + CheckFalse(57 in R, '#8'); + R := TIntegerRange.Create(999, 999); + CheckTrue(999 in R, '#9'); + CheckFalse(998 in R, '#10'); + R := TIntegerRange.CreateEmpty; + CheckFalse(0 in R, '#11'); + CheckFalse(High(Integer) in R, '#12'); + CheckFalse(Low(Integer) in R, '#13'); +end; + +procedure TestTIntegerRange.TestIsContiguousWith; +var + R, S: TIntegerRange; +begin + R := TIntegerRange.Create(4, 7); + S := TIntegerRange.Create(8, 12); + CheckTrue(R.IsContiguousWith(S), '#1a'); + CheckTrue(S.IsContiguousWith(R), '#1b'); + R := TIntegerRange.Create(4, 7); + S := TIntegerRange.Create(7, 12); + CheckFalse(R.IsContiguousWith(S), '#2a'); + CheckFalse(S.IsContiguousWith(R), '#2b'); + R := TIntegerRange.Create(1, 3); + S := TIntegerRange.Create(7, 12); + CheckFalse(R.IsContiguousWith(S), '#3a'); + CheckFalse(S.IsContiguousWith(R), '#3b'); + R := TIntegerRange.CreateEmpty; + S := TIntegerRange.Create(7, 12); + CheckFalse(R.IsContiguousWith(S), '#4a'); + CheckFalse(S.IsContiguousWith(R), '#4b'); + R := TIntegerRange.CreateEmpty; + S := TIntegerRange.CreateEmpty; + CheckFalse(R.IsContiguousWith(S), '#5a'); + CheckFalse(S.IsContiguousWith(R), '#5b'); + R := TIntegerRange.Create(0, 0); + S := TIntegerRange.Create(-1, -1); + CheckTrue(R.IsContiguousWith(S), '#6a'); + CheckTrue(S.IsContiguousWith(R), '#6b'); + R := TIntegerRange.Create(1, 3); + S := TIntegerRange.Create(-3, 5); + CheckFalse(R.IsContiguousWith(S), '#7a'); + CheckFalse(S.IsContiguousWith(R), '#7b'); +end; + +procedure TestTIntegerRange.TestIsContinuousWith; +var + R, S: TIntegerRange; +begin + R := TIntegerRange.Create(4, 7); + S := TIntegerRange.Create(8, 12); + CheckTrue(R.IsContinuousWith(S), '#1a'); + CheckTrue(S.IsContinuousWith(R), '#1b'); + R := TIntegerRange.Create(4, 7); + S := TIntegerRange.Create(7, 12); + CheckTrue(R.IsContinuousWith(S), '#2a'); + CheckTrue(S.IsContinuousWith(R), '#2b'); + R := TIntegerRange.Create(1, 3); + S := TIntegerRange.Create(7, 12); + CheckFalse(R.IsContinuousWith(S), '#3a'); + CheckFalse(S.IsContinuousWith(R), '#3b'); + R := TIntegerRange.CreateEmpty; + S := TIntegerRange.Create(7, 12); + CheckTrue(R.IsContinuousWith(S), '#4a'); + CheckTrue(S.IsContinuousWith(R), '#4b'); + R := TIntegerRange.CreateEmpty; + S := TIntegerRange.CreateEmpty; + CheckTrue(R.IsContinuousWith(S), '#5a'); + CheckTrue(S.IsContinuousWith(R), '#5b'); + R := TIntegerRange.Create(0, 0); + S := TIntegerRange.Create(-1, -1); + CheckTrue(R.IsContinuousWith(S), '#6a'); + CheckTrue(S.IsContinuousWith(R), '#6b'); + R := TIntegerRange.Create(1, 3); + S := TIntegerRange.Create(-3, 5); + CheckTrue(R.IsContinuousWith(S), '#7a'); + CheckTrue(S.IsContinuousWith(R), '#7b'); +end; + +procedure TestTIntegerRange.TestIsEmpty; +var + R: TIntegerRange; +begin + R := TIntegerRange.Create(0, 0); + CheckFalse(R.IsEmpty, '#1'); + R := TIntegerRange.Create(-3, 10); + CheckFalse(R.IsEmpty, '#2'); + R := TIntegerRange.Create(10, -42); + CheckFalse(R.IsEmpty, '#3'); + R := TIntegerRange.Create(10, 10); + CheckFalse(R.IsEmpty, '#4'); + R := TIntegerRange.CreateEmpty; + CheckTrue(R.IsEmpty, '#5'); +end; + +procedure TestTIntegerRange.TestLength; +var + R: TIntegerRange; +begin + R := TIntegerRange.Create(0, 0); + CheckEquals(1, R.Length, '#1'); + R := TIntegerRange.Create(-3, 10); + CheckEquals(14, R.Length, '#2'); + R := TIntegerRange.Create(10, -42); + CheckEquals(53, R.Length, '#3'); + R := TIntegerRange.Create(10, 10); + CheckEquals(1, R.Length, '#4'); + R := TIntegerRange.Create(12, 11); + CheckEquals(2, R.Length, '#5'); + R := TIntegerRange.CreateEmpty; + CheckEquals(0, R.Length, '#6'); + R := TIntegerRange.Create(-MaxInt, MaxInt); + CheckEquals(High(Cardinal), R.Length, '#7'); +end; + +//procedure TestTIntegerRange.TestLessThanOp; +//var +// L, R: TIntegerRange; +//begin +// L := TIntegerRange.Create(0, 0); +// R := TIntegerRange.Create(0, 0); +// CheckFalse(L < R, '#1a'); +// CheckFalse(R < L, '#1b'); +// L := TIntegerRange.Create(0, 0); +// R := TIntegerRange.Create(0, 1); +// CheckTrue(L < R, '#2a'); +// CheckFalse(R < L, '#2b'); +// L := TIntegerRange.Create(-3, 12); +// R := TIntegerRange.Create(-5, 18); +// CheckTrue(L < R, '#3a'); +// CheckFalse(R < L, '#3b'); +// L := TIntegerRange.Create(-5, 17); +// R := TIntegerRange.Create(-5, 18); +// CheckTrue(L < R, '#4a'); +// CheckFalse(R < L, '#4b'); +// L := TIntegerRange.Create(-4, 18); +// R := TIntegerRange.Create(-5, 18); +// CheckTrue(L < R, '#5a'); +// CheckFalse(R < L, '#5b'); +// L := TIntegerRange.Create(-5, 18); +// R := TIntegerRange.Create(-5, 18); +// CheckFalse(L < R, '#6a'); +// CheckFalse(R < L, '#6b'); +// L := TIntegerRange.Create(-5, 2); +// R := TIntegerRange.Create(4, 18); +// CheckFalse(L < R, '#7a'); +// CheckFalse(R < L, '#7b'); +// L := TIntegerRange.Create(1, 1); +// R := TIntegerRange.Create(4, 18); +// CheckFalse(L < R, '#8a'); +// CheckFalse(R < L, '#8b'); +// L := TIntegerRange.CreateEmpty; +// R := TIntegerRange.CreateEmpty; +// CheckFalse(L < R, '#9'); +// L := TIntegerRange.Create(10, 10); +// R := TIntegerRange.CreateEmpty; +// CheckFalse(L < R, '#10a'); +// CheckTrue(R < L, '#10b'); +//end; + +procedure TestTIntegerRange.TestLessThanOrEqualOp; +var + L, R: TIntegerRange; +begin + L := TIntegerRange.Create(0, 0); + R := TIntegerRange.Create(0, 0); + CheckTrue(L <= R, '#1a'); + CheckTrue(R <= L, '#1b'); + L := TIntegerRange.Create(0, 0); + R := TIntegerRange.Create(0, 1); + CheckTrue(L <= R, '#2a'); + CheckFalse(R <= L, '#2b'); + L := TIntegerRange.Create(-3, 12); + R := TIntegerRange.Create(-5, 18); + CheckTrue(L <= R, '#3a'); + CheckFalse(R <= L, '#3b'); + L := TIntegerRange.Create(-5, 17); + R := TIntegerRange.Create(-5, 18); + CheckTrue(L <= R, '#4a'); + CheckFalse(R <= L, '#4b'); + L := TIntegerRange.Create(-4, 18); + R := TIntegerRange.Create(-5, 18); + CheckTrue(L <= R, '#5a'); + CheckFalse(R <= L, '#5b'); + L := TIntegerRange.Create(-5, 18); + R := TIntegerRange.Create(-5, 18); + CheckTrue(L <= R, '#6a'); + CheckTrue(R <= L, '#6b'); + L := TIntegerRange.Create(-5, 2); + R := TIntegerRange.Create(4, 18); + CheckFalse(L <= R, '#7a'); + CheckFalse(R <= L, '#7b'); + L := TIntegerRange.Create(1, 1); + R := TIntegerRange.Create(4, 18); + CheckFalse(L <= R, '#8a'); + CheckFalse(R <= L, '#8b'); + L := TIntegerRange.CreateEmpty; + R := TIntegerRange.CreateEmpty; + CheckTrue(L <= R, '#9'); + L := TIntegerRange.Create(10, 10); + R := TIntegerRange.CreateEmpty; + CheckFalse(L <= R, '#10a'); + CheckTrue(R <= L, '#10b'); +end; + +procedure TestTIntegerRange.TestMultiplyOp; +var + L, R, E: TIntegerRange; +begin + L := TIntegerRange.Create(0, 0); + R := TIntegerRange.Create(0, 0); + E := TIntegerRange.Create(0, 0); + CheckTrue(E = L * R, '#1a'); + CheckTrue(E = R * L, '#1b'); + L := TIntegerRange.Create(4, 4); + R := TIntegerRange.Create(4, 4); + E := TIntegerRange.Create(4, 4); + CheckTrue(E = L * R, '#2a'); + CheckTrue(E = R * L, '#2b'); + L := TIntegerRange.Create(3, 3); + R := TIntegerRange.Create(4, 4); + E := TIntegerRange.CreateEmpty; + CheckTrue(E = L * R, '#3a'); + CheckTrue(E = R * L, '#3b'); + L := TIntegerRange.Create(-9, 0); + R := TIntegerRange.Create(1, 9); + E := TIntegerRange.CreateEmpty; + CheckTrue(E = L * R, '#4a'); + CheckTrue(E = R * L, '#4b'); + L := TIntegerRange.Create(1, 10); + R := TIntegerRange.Create(5, 12); + E := TIntegerRange.Create(5, 10); + CheckTrue(E = L * R, '#5a'); + CheckTrue(E = R * L, '#5b'); + L := TIntegerRange.Create(1, 10); + R := TIntegerRange.Create(4, 4); + E := TIntegerRange.Create(4, 4); + CheckTrue(E = L * R, '#6a'); + CheckTrue(E = R * L, '#6b'); + L := TIntegerRange.Create(1, 10); + R := TIntegerRange.Create(1, 9); + E := TIntegerRange.Create(1, 9); + CheckTrue(E = L * R, '#7a'); + CheckTrue(E = R * L, '#7b'); + L := TIntegerRange.Create(1, 10); + R := TIntegerRange.Create(2, 10); + E := TIntegerRange.Create(2, 10); + CheckTrue(E = L * R, '#8a'); + CheckTrue(E = R * L, '#8b'); + L := TIntegerRange.Create(1, 10); + R := TIntegerRange.Create(4, 6); + E := TIntegerRange.Create(4, 6); + CheckTrue(E = L * R, '#9a'); + CheckTrue(E = R * L, '#9b'); + L := TIntegerRange.Create(1, 10); + R := TIntegerRange.Create(1, 10); + E := TIntegerRange.Create(1, 10); + CheckTrue(E = L * R, '#10a'); + CheckTrue(E = R * L, '#10b'); + L := TIntegerRange.Create(0, 10); + R := TIntegerRange.Create(10, 20); + E := TIntegerRange.Create(10, 10); + CheckTrue(E = L * R, '#11a'); + CheckTrue(E = R * L, '#11b'); + L := TIntegerRange.CreateEmpty; + R := TIntegerRange.CreateEmpty; + E := TIntegerRange.CreateEmpty; + CheckTrue(E = L * R, '#12'); + L := TIntegerRange.CreateEmpty; + R := TIntegerRange.Create(10, 20); + E := TIntegerRange.CreateEmpty; + CheckTrue(E = L * R, '#13a'); + CheckTrue(E = R * L, '#13b'); +end; + +procedure TestTIntegerRange.TestNotEqualOp; +var + L, R: TIntegerRange; +begin + L := TIntegerRange.Create(0, 0); + R := TIntegerRange.Create(0, 0); + CheckFalse(L <> R, '#1a'); + CheckFalse(R <> L, '#1b'); + L := TIntegerRange.Create(-3, 11); + R := TIntegerRange.Create(-3, 10); + CheckTrue(L <> R, '#2a'); + CheckTrue(R <> L, '#2b'); + L := TIntegerRange.Create(-43, 9); + R := TIntegerRange.Create(10, -42); + CheckTrue(L <> R, '#3a'); + CheckTrue(R <> L, '#3b'); + L := TIntegerRange.Create(10, 10); + R := TIntegerRange.Create(10, 10); + CheckFalse(L <> R, '#4a'); + CheckFalse(R <> L, '#4b'); + CheckFalse(R <> R, '#5'); + L := TIntegerRange.CreateEmpty; + R := TIntegerRange.CreateEmpty; + CheckFalse(L <> R, '#6'); + L := TIntegerRange.Create(10, 10); + R := TIntegerRange.CreateEmpty; + CheckTrue(L <> R, '#6'); +end; + +procedure TestTIntegerRange.TestOverlapsWith; +var + A, B: TIntegerRange; +begin + A := TIntegerRange.Create(1, 4); + B := TIntegerRange.Create(3, 10); + CheckTrue(A.OverlapsWith(B), '#1a'); + CheckTrue(B.OverlapsWith(A), '#1b'); + A := TIntegerRange.Create(1, 4); + B := TIntegerRange.Create(4, 10); + CheckTrue(A.OverlapsWith(B), '#2a'); + CheckTrue(B.OverlapsWith(A), '#2b'); + A := TIntegerRange.CreateEmpty; + B := TIntegerRange.CreateEmpty; + CheckFalse(A.OverlapsWith(B), '#3'); + A := TIntegerRange.CreateEmpty; + B := TIntegerRange.Create(4, 10); + CheckFalse(A.OverlapsWith(B), '#4a'); + CheckFalse(B.OverlapsWith(A), '#4b'); + A := TIntegerRange.Create(-MaxInt, MaxInt); + B := TIntegerRange.Create(4, 10); + CheckTrue(A.OverlapsWith(B), '#5a'); + CheckTrue(B.OverlapsWith(A), '#5b'); + A := TIntegerRange.Create(1, 3); + B := TIntegerRange.Create(4, 10); + CheckFalse(A.OverlapsWith(B), '#6a'); + CheckFalse(B.OverlapsWith(A), '#6b'); + A := TIntegerRange.Create(-MaxInt, MaxInt); + B := TIntegerRange.Create(-MaxInt, MaxInt); + CheckTrue(A.OverlapsWith(B), '#7'); +end; + +initialization + // Register any test cases with the test runner + RegisterTest(TestTIntegerRange.Suite); + +end. diff --git a/tests/Cat-Structs/UStructCatSnippets.pas b/tests/Cat-Structs/UStructCatSnippets.pas new file mode 100644 index 0000000..2b21608 --- /dev/null +++ b/tests/Cat-Structs/UStructCatSnippets.pas @@ -0,0 +1,519 @@ +{ + * This unit was generated automatically. It incorporates a selection of source + * code taken from the Code Snippets Database at + * https://github.com/delphidabbler/code-snippets. + * + * The unit is copyright 2005-2025 by Peter Johnson & Contributors and is + * licensed under the MIT License (https://opensource.org/licenses/MIT). + * + * Generated on : Fri, 04 Apr 2025 08:38:43 GMT. + * Generated by : DelphiDabbler CodeSnip Release 4.24.0. + * + * The latest version of CodeSnip is available from the CodeSnip GitHub project + * at https://github.com/delphidabbler/codesnip. +} + +unit UStructCatSnippets; + +interface + +uses + Types, SysUtils, Math; + +{ + Encapsulates a point with double precision floating point coordinates. +} +type + TPointF = record + X, Y: Double; // x and y coordinates + end; + +{ + Encapsulates a rectangle with double precision floating point size and + position. +} +type + TRectF = record + case Integer of + 0: (Left, Top, Right, Bottom: Double); + 1: (TopLeft, BottomRight: TPointF); + end; + +{ + Encapsulates the upper and lower bounds of a range of values. +} +type + TRange = record + Lower: Integer; // lower bound of range + Upper: Integer; // upper bound of range + end; + +{ + An advanced record that encapsulates an integer range along with operations on + it. + The range is immutable, so the constructor must be used to instantiate a + non-empty range. +} +type + TIntegerRange = record + strict private + var + fLowerBound: Integer; + fUpperBound: Integer; + function GetLowerBound: Integer; + function GetUpperBound: Integer; + function IsSubrangeOf(const ARange: TIntegerRange): Boolean; + public + // Constructs a range whose bounds are A and B. The lowest of the two + // parameters is taken as the lower bound of the range with the other + // parameter taken as the upper bound. + // Valid bounds must fall in the range -MaxInt..MaxInt. An + // EArgumentException exception is raised otherwise. + constructor Create(const A, B: Integer); + + // Constructs an empty range. + class function CreateEmpty: TIntegerRange; static; + + // Checks if the range is empty. + function IsEmpty: Boolean; + + // Returns the length of the range, i.e. the number of integers in the range. + function Length: Cardinal; + + // Constrains AValue to fall within this range. If the value lies within the + // range it is returned unchanged. If it is outside the range then either + // LowerBound or UpperBound is returned, depending on whether the value + // falls below or above the range, respectively. + // An EInvalidOpException exception is raised if the range is empty. + function Constrain(const AValue: Integer): Integer; + + // Checks if this range overlaps with ARange, i.e. the interection of the + // ranges is non empty. Empty ranges cannot overlap with any range. + function OverlapsWith(const ARange: TIntegerRange): Boolean; + + // Checks if this range is immediately adjacent to ARange, with no overlap. + // Empty ranges are never contiguous with other ranges or themselves. + function IsContiguousWith(const ARange: TIntegerRange): Boolean; + + // Checks if the set of all values in this range and ARange form a + // continuous sequence. This implies that a range is continuous with itself. + // Since adding an empty range to a non-empty range doesn't change the + // non-empty range we define empty ranges to be continuous with any range. + function IsContinuousWith(const ARange: TIntegerRange): Boolean; + + // Checks if ranges A and B are the same + class operator Equal(const A, B: TIntegerRange): Boolean; + + // Checks if ranges A and B are not the same + class operator NotEqual(const A, B: TIntegerRange): Boolean; + + // Checks if range A is contained in, or is the same as, range B. + // An empty range is deemed to be contained in any other range. + class operator LessThanOrEqual(const A, B: TIntegerRange): Boolean; + + // Checks if range A is contains, or is the same as, range B. + // A non-empty range is never contained in an empty range. + class operator GreaterThanOrEqual(const A, B: TIntegerRange): Boolean; + + // Combine two ranges, A and B. The result is the smallest range that + // contains both A and B. + // If A and B are not continuous the resulting range will contain values + // that were not in either A or B. + // Combining any range either with itself or with an empty range is a no-op. + class operator Add(const A, B: TIntegerRange): TIntegerRange; + + // Returns a range that is the intersection of ranges A and B. + // Returns an empty range if A and B do not overlap. + class operator Multiply(const A, B: TIntegerRange): TIntegerRange; + + // Checks if integer AValue is contained within range ARange. + class operator In(const AValue: Integer; const ARange: TIntegerRange): + Boolean; + + // Implicitly casts ARange to a string. If ARange is non-empty the string + // has format [X..Y], where X and Y are the lower and upper bounds of + // ARange respectively. If ARange is empty then [] is returned. + // This means that ARange can be assigned directly to a string. + class operator Implicit(const ARange: TIntegerRange): string; + + // Explicitly casts ARange to a string. If ARange is non-empty the string + // has format [X..Y], where X and Y are the lower and upper bounds of + // ARange respectively. If ARange is empty then [] is returned. + // This means that ARange can be explicitly cast to a string using + // string(ARange). + class operator Explicit(const ARange: TIntegerRange): string; + + // The lower bound of a non-empty range. + // EInvalidOpException is raised if the property is read when the range is + // empty. + property LowerBound: Integer read GetLowerBound; + + // The upper bound of a non-empty range. + // EInvalidOpException is raised if the property is read when the range is + // empty. + property UpperBound: Integer read GetUpperBound; + end; + +{ + Encapsulates a range of integers with a methods to test whether a value falls + within the range and to adjust the value to fit. +} +type + TRangeEx = record + // Minimum and maximum bounds of range. + Min, Max: Integer; + // Constructs record with given minimum and maximum bounds. + constructor Create(AMin, AMax: Integer); + // Checks if the given value is contained within the range. + function Contains(const Value: Integer): Boolean; + // Adjusts the given value to lie within the range, and returns it. If the + // value is less than Min, Min is returned. If the value is greater than Max + // then max is returned. If the value is in the range it is returned + // unchanged. + function Constrain(const Value: Integer): Integer; + end; + +{ + Record that represents the size, i.e. the width and height, of something. + This is an extended version of the TSize record that features equality and + inequality operator overloading, a test for zero size and a constructor. + TSizeEx is assignment compatible and comparible with the Delphi RTL's TSize + record. +} +type + TSizeEx = record + public + // Width + CX: Integer; + // Height + CY: Integer; + // Constructs record with two given CX and CY field values + constructor Create(ACX, ACY: Integer); + // Enables TSize to be assigned to and compared with TSizeEx + class operator Implicit(S: Types.TSize): TSizeEx; + // Enables TSizeEx to be assigned to and compared with TSize + class operator Implicit(S: TSizeEx): Types.TSize; + // Tests for equality of TSizeEx records. Also works if one record is TSize. + class operator Equal(S1, S2: TSizeEx): Boolean; + // Tests for inequality of TSizeEx records. Also works if one record is + // TSize. + class operator NotEqual(S1, S2: TSizeEx): Boolean; + // Tests if a TSizeEx instance is zero (i.e. one of fields is zero) + function IsZero: Boolean; + end; + +{ + Constructs and returns a TRectF record with the given top-left coordinate, + width and height. +} +function BoundsF(ALeft, ATop, AWidth, AHeight: Double): TRectF; + +{ + Constructs and returns a TPointF record with the given x & y coordinates. +} +function PointF(const AX, AY: Double): TPointF; + +{ + Constructs and returns a TRange record with bounds A and B. + The smaller of A and B is used as the lower bound and the larger as the upper + bound. If both values are equal then the range will be empty. +} +function Range(const A, B: Integer): TRange; + +{ + Constructs and returns a TRectF record with the given left, top, right & + bottom coordinates. +} +function RectF(const ALeft, ATop, ARight, ABottom: Double): TRectF; + +{ + Constructs and returns a TSize record with the given dimensions. +} +function Size(const ACX, ACY: Integer): Types.TSize; + +implementation + +{ + Constructs and returns a TRectF record with the given top-left coordinate, + width and height. +} +function BoundsF(ALeft, ATop, AWidth, AHeight: Double): TRectF; +begin + Result.Left := ALeft; + Result.Top := ATop; + Result.Right := ALeft + AWidth; + Result.Bottom := ATop + AHeight; +end; + +{ + Constructs and returns a TPointF record with the given x & y coordinates. +} +function PointF(const AX, AY: Double): TPointF; +begin + Result.X := AX; + Result.Y := AY; +end; + +{ + Constructs and returns a TRange record with bounds A and B. + The smaller of A and B is used as the lower bound and the larger as the upper + bound. If both values are equal then the range will be empty. +} +function Range(const A, B: Integer): TRange; +begin + if A <= B then + begin + Result.Lower := A; + Result.Upper := B; + end + else + begin + Result.Lower := B; + Result.Upper := A; + end; +end; + +{ + Constructs and returns a TRectF record with the given left, top, right & + bottom coordinates. +} +function RectF(const ALeft, ATop, ARight, ABottom: Double): TRectF; +begin + Result.Left := ALeft; + Result.Top := ATop; + Result.Right := ARight; + Result.Bottom := ABottom; +end; + +{ + Constructs and returns a TSize record with the given dimensions. +} +function Size(const ACX, ACY: Integer): Types.TSize; +begin + Result.cx := ACX; + Result.cy := ACY; +end; + +class operator TIntegerRange.Add(const A, B: TIntegerRange): TIntegerRange; +begin + if A.IsEmpty then + Exit(B); + if B.IsEmpty then + Exit(A); + Result := TIntegerRange.Create( + Math.Min(A.fLowerBound, B.fLowerBound), + Math.Max(A.fUpperBound, B.fUpperBound) + ); +end; + +function TIntegerRange.Constrain(const AValue: Integer): Integer; +begin + if IsEmpty then + raise Sysutils.EInvalidOpException.Create( + 'TIntegerRange.Constrain not valid for an empty range.' + ); + Result := Math.EnsureRange(AValue, fLowerBound, fUpperBound); +end; + +constructor TIntegerRange.Create(const A, B: Integer); +begin + // Normalise range so that smallest parameter is the lower bound + fLowerBound := Math.Min(A, B); + fUpperBound := Math.Max(A, B); + if fLowerBound = Low(Integer) then + // This restriction is required to prevent the Length method's Cardinal + // return value from wrapping around / overflowing + raise SysUtils.EArgumentException.CreateFmt( + 'TIntegerRange.Create: Arguments must be greater than %d', [Low(Integer)] + ); +end; + +class function TIntegerRange.CreateEmpty: TIntegerRange; +begin + Result.fLowerBound := High(Integer); + Result.fUpperBound := Low(Integer); +end; + +class operator TIntegerRange.Equal(const A, B: TIntegerRange): Boolean; +begin + if A.IsEmpty or B.IsEmpty then + Exit(A.IsEmpty and B.IsEmpty); + Result := (A.fLowerBound = B.fLowerBound) and (A.fUpperBound = B.fUpperBound); +end; + +class operator TIntegerRange.Explicit(const ARange: TIntegerRange): string; +begin + if ARange.IsEmpty then + Exit('[]'); + Result := SysUtils.Format( + '[%d..%d]', [ARange.fLowerBound, ARange.fUpperBound] + ); +end; + +function TIntegerRange.GetLowerBound: Integer; +begin + if IsEmpty then + raise Sysutils.EInvalidOpException.Create( + 'TIntegerRange.LowerBound not valid for an empty range.' + ); + Result := fLowerBound; +end; + +function TIntegerRange.GetUpperBound: Integer; +begin + if IsEmpty then + raise Sysutils.EInvalidOpException.Create( + 'TIntegerRange.LowerBound not valid for an empty range.' + ); + Result := fUpperBound; +end; + +class operator TIntegerRange.GreaterThanOrEqual(const A, B: TIntegerRange): + Boolean; +begin + Result := B.IsSubrangeOf(A); +end; + +class operator TIntegerRange.Implicit(const ARange: TIntegerRange): string; +begin + Result := string(ARange); // calls Explicit cast operator +end; + +class operator TIntegerRange.In(const AValue: Integer; + const ARange: TIntegerRange): Boolean; +begin + if ARange.IsEmpty then + Exit(False); + Result := (AValue >= ARange.fLowerBound) and (AValue <= ARange.fUpperBound); +end; + +function TIntegerRange.IsContiguousWith(const ARange: TIntegerRange): Boolean; +begin + if Self.IsEmpty or ARange.IsEmpty then + Exit(False); + Result := (Self + ARange).Length = (Self.Length + ARange.Length); +end; + +function TIntegerRange.IsContinuousWith(const ARange: TIntegerRange): Boolean; +begin + if Self.IsEmpty or ARange.IsEmpty then + // Empty ranges are only continuous with other empty ranges + Exit(True); + Result := IsContiguousWith(ARange) or OverlapsWith(ARange); +end; + +function TIntegerRange.IsEmpty: Boolean; +begin + Result := fLowerBound > fUpperBound; +end; + +function TIntegerRange.IsSubrangeOf(const ARange: TIntegerRange): Boolean; +begin + if ARange.IsEmpty then + Exit(Self.IsEmpty); + Result := (Self.fLowerBound >= ARange.fLowerBound) + and (Self.fUpperBound <= ARange.fUpperBound) + or Self.IsEmpty +end; + +function TIntegerRange.Length: Cardinal; +begin + if IsEmpty then + Exit(0); + Result := fUpperBound - fLowerBound + 1 +end; + +class operator TIntegerRange.LessThanOrEqual(const A, B: TIntegerRange): + Boolean; +begin + Result := A.IsSubrangeOf(B); +end; + +class operator TIntegerRange.Multiply(const A, B: TIntegerRange): TIntegerRange; +var + Up, Lo: Integer; +begin + if A.IsEmpty or B.IsEmpty then + Exit(TIntegerRange.CreateEmpty); + Lo := Math.Max(A.fLowerBound, B.fLowerBound); + Up := Math.Min(A.fUpperBound, B.fUpperBound); + if Lo <= Up then + Result := TIntegerRange.Create(Lo, Up) + else + Result := TIntegerRange.CreateEmpty; +end; + +class operator TIntegerRange.NotEqual(const A, B: TIntegerRange): Boolean; +begin + if A.IsEmpty or B.IsEmpty then + Exit(A.IsEmpty <> B.IsEmpty); + Result := (A.fLowerBound <> B.fLowerBound) + or (A.fUpperBound <> B.fUpperBound); +end; + +function TIntegerRange.OverlapsWith(const ARange: TIntegerRange): Boolean; +begin + Result := not (Self * ARange).IsEmpty; +end; + +function TRangeEx.Constrain(const Value: Integer): Integer; +begin + if Value < Min then + Result := Min + else if Value > Max then + Result := Max + else + Result := Value; +end; + +function TRangeEx.Contains(const Value: Integer): Boolean; +begin + Result := Math.InRange(Value, Min, Max); +end; + +constructor TRangeEx.Create(AMin, AMax: Integer); +begin + Min := AMin; + Max := AMax; +end; + +constructor TSizeEx.Create(ACX, ACY: Integer); +begin + CX := ACX; + CY := ACY; +end; + +class operator TSizeEx.Equal(S1, S2: TSizeEx): Boolean; +begin + // zero records are special: can be zero when only one of CX or CY is zero + if S1.IsZero and S2.IsZero then + begin + Result := True; + Exit; + end; + Result := (S1.CX = S1.CX) and (S1.CY = S2.CY); +end; + +class operator TSizeEx.Implicit(S: Types.TSize): TSizeEx; +begin + Result.CX := S.cx; + Result.CY := S.cy; +end; + +class operator TSizeEx.Implicit(S: TSizeEx): Types.TSize; +begin + Result.cx := S.CX; + Result.cy := S.CY; +end; + +function TSizeEx.IsZero: Boolean; +begin + Result := (CX = 0) or (CY = 0); +end; + +class operator TSizeEx.NotEqual(S1, S2: TSizeEx): Boolean; +begin + Result := not (S1 = S2); +end; + +end. From d089b7f86bf02403a0719c712f9b9c21095474c9 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:10:51 +0100 Subject: [PATCH 03/15] Add structs unit test to CodeSnippetsTestsXE group The TestCatStructsXE DUnit test project was added to CodeSnippetsTestsXE.groupproj. --- tests/CodeSnippetsTestsXE.groupproj | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/CodeSnippetsTestsXE.groupproj b/tests/CodeSnippetsTestsXE.groupproj index 5c76171..86d2da7 100644 --- a/tests/CodeSnippetsTestsXE.groupproj +++ b/tests/CodeSnippetsTestsXE.groupproj @@ -24,6 +24,9 @@ + + + Default.Personality.12 @@ -95,14 +98,23 @@ + + + + + + + + + - + - + - + From 36f7128a32ade0685a1e3797bed06c1f6e59f027 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 10:13:02 +0100 Subject: [PATCH 04/15] Update tests/README.md re new Structures unit tests --- tests/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/README.md b/tests/README.md index ee26a9e..e28d032 100644 --- a/tests/README.md +++ b/tests/README.md @@ -35,6 +35,10 @@ Contains the _TestCatMaths_[^2] project that provides _DUnit_ tests for selected ### `./Cat-String` +Contains the _TestCatStructsXE_[^3] project that provides _DUnit_ tests for selected snippets from the database's _Structures_ category. + +### `./Cat-Structs` + Contains the _TestCatString_[^2] project that provides _DUnit_ tests for selected snippets from the database's _String Management_ category. ### `./Cat-WinSys` @@ -47,6 +51,8 @@ These test files originated as a [zip file on Google Drive](https://drive.google All subsequent changes to the files were made on GitHub. At present the GitHub repository differs from the original code on Google Drive. -[^1]: There are different versions of the group project file for different Delphi versions: `CodeSnippetsTestsbdsgroup` for Delphi 2006, `CodeSnippetsTests.groupproj` for Delphi 2007-2010 and `CodeSnippetsTestsXE.groupproj` for Delphi XE. +[^1]: There are different versions of the group project file for different Delphi versions: `CodeSnippetsTests.bdsgroup` for Delphi 2006, `CodeSnippetsTests.groupproj` for Delphi 2007-2010 and `CodeSnippetsTestsXE.groupproj` for Delphi XE. + +[^2]: There are different versions of project files for different Delphi versions. They are one or more of `.bdsproj` for Delphi 2006, `.dproj` for Delphi 2007 to 2010 and `XE.dproj` for Delphi XE, where `` is the base name of the project, for example `TestDateCat` or `TestCatMaths`. There are also different `.dpr` files: `.dpr` for compilers earlier than Delphi XE and `XE.dpr` for Delphi XE. -[^2]: There are different versions of project files for different Delphi versions. They are one or more of `.bdsproj` for Delphi 2006, `.dproj` for Delphi 2007 to 2010 and `XE.dproj` for Delphi XE, where `` is the base name of the project, for example `TestDateCat` or `TestCatMaths`. +[^3]: There is only one project file for the _Structures_ category tests, `TesttCatStructsXE.dpr`, which is targetted at Delphi XE. There is no test project targetted at earlier compilers. This project is only included in the `CodeSnippetsTestsXE.groupproj` group and no others. From 77e27c206cb9cf349fc07248d75ea562105a27ef Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 12:58:05 +0100 Subject: [PATCH 05/15] Add more unit tests for structs category snippets Added tests to TestUStructCatSnippets unit for all snippets in the Structures category except for TIntegerRange, which was the only snippet in the category to have tests. Advanced records had dedicated unit test classes. The remaining simple record types and functions had tests grouped together in a single new unit test class. --- tests/Cat-Structs/TestUStructCatSnippets.pas | 349 +++++++++++++++++++ 1 file changed, 349 insertions(+) diff --git a/tests/Cat-Structs/TestUStructCatSnippets.pas b/tests/Cat-Structs/TestUStructCatSnippets.pas index 602bc84..791cd76 100644 --- a/tests/Cat-Structs/TestUStructCatSnippets.pas +++ b/tests/Cat-Structs/TestUStructCatSnippets.pas @@ -7,7 +7,42 @@ interface implementation +uses + Types; + type + + TestMiscStructsCatSnippets = class(TTestCase) + published + procedure TestType_TPointF; + procedure TestType_TRange; + procedure TestType_TRectF; // requires TPointF + procedure TestFunction_PointF; // requires TPointF + procedure TestFunction_Range; // requires TRange + procedure TestFunction_RectF; // requires TPointF & TRectF + procedure TestFunction_Size; + procedure TestFunction_BoundsF; // requires TRectF & RectF + end; + + TestTSizeEx = class(TTestCase) + published + // Order of tests is important Ctor then implicit ops then equality ops and + // IsZero method + procedure TestCtorAndFields; + procedure TestImplicitOp_TSizeToTSizeEx; + procedure TestImplicitOp_TSizeExToTSize; + procedure TestEqualOp; + procedure TestNotEqualOp; + procedure TestIsZero; + end; + + TestTRangeEx = class(TTestCase) + published + procedure TestCtorAndFields; + procedure TestContains; + procedure TestConstrain; + end; + TestTIntegerRange = class(TTestCase) private procedure TestCtorAndPropsException; @@ -35,6 +70,316 @@ TestTIntegerRange = class(TTestCase) procedure TestIsContinuousWith; end; +{ TestMiscStructsCatSnippets } + +procedure TestMiscStructsCatSnippets.TestFunction_BoundsF; +var + R, Expected: TRectF; +const + Delta = 0.00000001; +begin + R := BoundsF(10.3, 20.4, 10.5, 20.6); + Expected := RectF(10.3, 20.4, 10.3+10.5, 20.4+20.6); + CheckEquals(Expected.Left, R.Left, Delta, 'left'); + CheckEquals(Expected.Top, R.Top, Delta, 'top'); + CheckEquals(Expected.Right, R.Right, Delta, 'right'); + CheckEquals(Expected.Bottom, R.Bottom, Delta, 'bottom'); +end; + +procedure TestMiscStructsCatSnippets.TestFunction_PointF; +var + P: TPointF; +const + Delta = 0.00000001; +begin + P := PointF(0.0, 0.0); + CheckEquals(0.0, P.X, Delta, '#1X'); + CheckEquals(0.0, P.Y, Delta, '#1Y'); + P := PointF(42.56, -12.345); + CheckEquals(42.56, P.X, Delta, '#2X'); + CheckEquals(-12.345, P.Y, Delta, '#2Y'); +end; + +procedure TestMiscStructsCatSnippets.TestFunction_Range; +var + R: TRange; +begin + // Range function orders parameters + R := Range(42, 56); + CheckEquals(42, R.Lower, '#1 lower'); + CheckEquals(56, R.Upper, '#1 upper'); + R := Range(56, 42); + CheckEquals(42, R.Lower, '#2 lower'); + CheckEquals(56, R.Upper, '#2 upper'); +end; + +procedure TestMiscStructsCatSnippets.TestFunction_RectF; +var + R: TRectF; +const + Delta = 0.00000001; +begin + R := RectF(0.0, -10.8, 34.56, 20.3); + CheckEquals(0.0, R.Left, Delta, 'left'); + CheckEquals(-10.8, R.Top, Delta, 'top'); + CheckEquals(34.56, R.Right, Delta, 'right'); + CheckEquals(20.3, R.Bottom, Delta, 'bottom'); + CheckEquals(0.0, R.TopLeft.X, Delta, 'topleft.x'); + CheckEquals(-10.8, R.TopLeft.Y, Delta, 'topleft.y'); + CheckEquals(34.56, R.BottomRight.X, Delta, 'bottomright.x'); + CheckEquals(20.3, R.BottomRight.Y, Delta, 'bottomright.y'); +end; + +procedure TestMiscStructsCatSnippets.TestFunction_Size; +var + S: TSize; +begin + S.cx := 42; + S.cy := 56; + CheckEquals(42, S.cx, 'cx'); + CheckEquals(56, S.cy, 'cy'); +end; + +procedure TestMiscStructsCatSnippets.TestType_TPointF; +var + P: TPointF; +const + Delta = 0.00000001; +begin + P.X := 0.0; + P.Y := 0.0; + CheckEquals(0.0, P.X, Delta, '#1X'); + CheckEquals(0.0, P.Y, Delta, '#1Y'); + P.X := 42.56; + P.Y := -12.345; + CheckEquals(42.56, P.X, Delta, '#2X'); + CheckEquals(-12.345, P.Y, Delta, '#2Y'); +end; + +procedure TestMiscStructsCatSnippets.TestType_TRange; +var + R: TRange; +begin + // Test direct field setting + R.Lower := 42; + R.Upper := 56; + CheckEquals(42, R.Lower, '#1 lower'); + CheckEquals(56, R.Upper, '#1 upper'); + R.Lower := 56; + R.Upper := 42; + CheckEquals(56, R.Lower, '#2 lower'); + CheckEquals(42, R.Upper, '#2 upper'); +end; + +procedure TestMiscStructsCatSnippets.TestType_TRectF; +var + R: TRectF; + TL, BR: TPointF; +const + Delta = 0.00000001; +begin + // Set Left, Right, Top & Bottom fields + R.Left := 2.2; + R.Right := 6.6; + R.Top := 8.8; + R.Bottom := 16.16; + TL.X := 2.2; + TL.Y := 8.8; + BR.X := 6.6; + BR.Y := 16.16; + CheckEquals(2.2, R.Left, Delta, '#1 left'); + CheckEquals(6.6, R.Right, Delta, '#1 right'); + CheckEquals(8.8, R.Top, Delta, '#1 top'); + CheckEquals(16.16, R.Bottom, Delta, '#1 bottom'); + CheckEquals(2.2, R.TopLeft.X, Delta, '#1 topleft.x'); + CheckEquals(8.8, R.TopLeft.Y, Delta, '#1 topleft.y'); + CheckEquals(6.6, R.BottomRight.X, Delta, '#1 bottomright.x'); + CheckEquals(16.16, R.BottomRight.Y, Delta, '#1 bottomright.y'); + // Set TopLeft & BottomRight TPointF properties + TL.X := 10.11; + TL.Y := 12.13; + BR.X := 11.12; + BR.Y := 13.14; + R.TopLeft := TL; + R.BottomRight := BR; + CheckEquals(10.11, R.Left, Delta, '#2 left'); + CheckEquals(12.13, R.Top, Delta, '#2 top'); + CheckEquals(11.12, R.Right, Delta, '#2 right'); + CheckEquals(13.14, R.Bottom, Delta, '#2 bottom'); +end; + +{ TestTSizeEx } + +procedure TestTSizeEx.TestCtorAndFields; +var + S: TSizeEx; +begin + // Test direct field access + S.CX := 42; + S.CY := -56; + CheckEquals(42, S.CX, '#1a'); + CheckEquals(-56, S.CY, '#1b'); + // Text Ctor + S := TSizeEx.Create(42, -56); + CheckEquals(42, S.CX, '#2a'); + CheckEquals(-56, S.CY, '#2b'); +end; + +procedure TestTSizeEx.TestEqualOp; +var + Sx0, Sx1a, Sx1b, Sx2: TSizeEx; + S0, S1, S2: TSize; +begin + // Test with both operands TSizeEx + Sx0 := TSizeEx.Create(0, 0); + Sx1a := TSizeEx.Create(42, 56); + Sx1b := TSizeEx.Create(42, 56); + Sx2 := TSizeEx.Create(99, 99); + CheckTrue(Sx1a = Sx1b, '#1a'); + CheckFalse(Sx0 = Sx2, '#1b'); + CheckFalse(Sx1a = Sx2, '#1c'); + // Test with one TSizeEx and one TSize operanc + S0 := Sx0; + S1 := Sx1a; + S2 := Sx2; + CheckTrue(Sx1a = S1, '#2a'); + CheckFalse(S0 = Sx2, '#2b'); + CheckTrue(S2 = Sx2, '#2c'); +end; + +procedure TestTSizeEx.TestImplicitOp_TSizeExToTSize; +var + Src: TSizeEx; + Dest: TSize; +begin + Src := TSizeEx.Create(23, -99); + Dest := Src; + CheckEquals(23, Dest.cx, 'cx'); + CheckEquals(-99, Dest.cy, 'cy'); +end; + +procedure TestTSizeEx.TestImplicitOp_TSizeToTSizeEx; +var + Src: TSize; + Dest: TSizeEx; +begin + Src := TSizeEx.Create(23, 423); + Dest := Src; + CheckEquals(23, Dest.CX, 'CX'); + CheckEquals(423, Dest.CY, 'CY'); +end; + +procedure TestTSizeEx.TestIsZero; +var + S: TSizeEx; +begin + S := TSizeEx.Create(12, 23); + CheckFalse(S.IsZero, '#1'); + S := TSizeEx.Create(0, 0); + CheckTrue(S.IsZero, '#2'); + S := TSizeEx.Create(0, 1); + CheckTrue(S.IsZero, '#3'); + S := TSizeEx.Create(-1, 0); + CheckTrue(S.IsZero, '#4'); +end; + +procedure TestTSizeEx.TestNotEqualOp; +var + Sx0, Sx1a, Sx1b, Sx2: TSizeEx; + S0, S1, S2: TSize; +begin + // Test with both operands TSizeEx + Sx0 := TSizeEx.Create(0, 0); + Sx1a := TSizeEx.Create(42, 56); + Sx1b := TSizeEx.Create(42, 56); + Sx2 := TSizeEx.Create(99, 99); + CheckFalse(Sx1a <> Sx1b, '#1a'); + CheckTrue(Sx0 <> Sx2, '#1b'); + CheckTrue(Sx1a <> Sx2, '#1c'); + // Test with one TSizeEx and one TSize operanc + S0 := Sx0; + S1 := Sx1a; + S2 := Sx2; + CheckFalse(Sx1a <> S1, '#2a'); + CheckTrue(S0 <> Sx2, '#2b'); + CheckFalse(S2 <> Sx2, '#2c'); +end; + +{ TestTRangeEx } + +procedure TestTRangeEx.TestConstrain; +var + R: TRangeEx; +begin + // Min < Max => expected results + R := TRangeEx.Create(-42, 56); + CheckEquals(2, R.Constrain(2), '#1a'); + CheckEquals(-42, R.Constrain(-42), '#1b'); + CheckEquals(56, R.Constrain(56), '#1c'); + CheckEquals(-42, R.Constrain(-99), '#1d'); + CheckEquals(56, R.Constrain(99), '#1e'); + // Min > Max => bonkers results !!! + R := TRangeEx.Create(56, 42); + CheckEquals(56, R.Constrain(2), '#2a'); // !!! should be 42 + CheckEquals(56, R.Constrain(42), '#2b'); // !!! should be 42 + CheckEquals(42, R.Constrain(56), '#2c'); // !!! should be 56 + CheckEquals(56, R.Constrain(48), '#2d'); // !!! should be 48 + CheckEquals(56, R.Constrain(40), '#2e'); // !!! should be 42 + CheckEquals(42, R.Constrain(99), '#2f'); // !!! should be 56 + // Min = Max => expected results + R := TRangeEx.Create(3, 3); + CheckEquals(3, R.Constrain(2), '#1a'); + CheckEquals(3, R.Constrain(3), '#1b'); + CheckEquals(3, R.Constrain(4), '#1c'); +end; + +procedure TestTRangeEx.TestContains; +var + R: TRangeEx; +begin + // Min < Max => expected results + R := TRangeEx.Create(-42, 56); + CheckTrue(R.Contains(2), '#1a'); + CheckTrue(R.Contains(-42), '#1b'); + CheckTrue(R.Contains(56), '#1c'); + CheckFalse(R.Contains(-99), '#1d'); + CheckFalse(R.Contains(57), '#1e'); + // Max > Min => bonkers results !!! + R := TRangeEx.Create(56, 42); + CheckFalse(R.Contains(48), '#2a'); // !!! Should be True + CheckFalse(R.Contains(2), '#2b'); + CheckFalse(R.Contains(99), '#2c'); + CheckFalse(R.Contains(42), '#2b'); // !!! Should be True + CheckFalse(R.Contains(56), '#2b'); // !!! Should be True + // Min = Max => expected results + R := TRangeEx.Create(3, 3); + CheckFalse(R.Contains(2), '#3a'); + CheckFalse(R.Contains(4), '#3b'); + CheckTrue(R.Contains(3), '#3c'); +end; + +procedure TestTRangeEx.TestCtorAndFields; +var + R: TRangeEx; +begin + // Direct field access: no ordering of range + R.Min := 42; + R.Max := 56; + CheckEquals(42, R.Min, '#1 min'); + CheckEquals(56, R.Max, '#1 max'); + R.Min := 56; + R.Max := 42; + CheckEquals(56, R.Min, '#2 min'); + CheckEquals(42, R.Max, '#2 max'); + // Ctor: also no ordering of range + R := TRangeEx.Create(42, 56); + CheckEquals(42, R.Min, '#3 min'); + CheckEquals(56, R.Max, '#3 max'); + R := TRangeEx.Create(56, 42); + CheckEquals(56, R.Min, '#3 min'); + CheckEquals(42, R.Max, '#3 max'); +end; + { TestTIntegerRange } procedure TestTIntegerRange.SetUp; @@ -650,7 +995,11 @@ procedure TestTIntegerRange.TestOverlapsWith; end; initialization + // Register any test cases with the test runner RegisterTest(TestTIntegerRange.Suite); + RegisterTest(TestTRangeEx.Suite); + RegisterTest(TestTSizeEx.Suite); + RegisterTest(TestMiscStructsCatSnippets.Suite); end. From f7004477a686b1678d40774f743ae9adc74bbbc3 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:06:02 +0100 Subject: [PATCH 06/15] Update structs.ini with advanced testing info All but one snippets in structs.ini had the same advanced testing information added to it that sets TestInfo=advanced, AdvancedTest.Level=unit-tests and AdvancedTest.URL to the Cat-Structs unit tests on Github. The only snippet not so updated was TIntegerRange, which already has the same test info. --- collection/structs.ini | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/collection/structs.ini b/collection/structs.ini index 2fb328f..5d5e835 100644 --- a/collection/structs.ini +++ b/collection/structs.ini @@ -9,6 +9,9 @@ DescEx="

Constructs and returns a TRectF record with the given top-left coordinate, width and height.

" SeeAlso=RectF,TRectF Depends=TRectF +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=493.dat Delphi2=Y Delphi3=Y @@ -31,6 +34,9 @@ FPC=Y [PointF] DescEx="

Constructs and returns a TPointF record with the given x & y coordinates.

" +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" SeeAlso=TPointF,RectF Depends=TPointF Snip=491.dat @@ -58,6 +64,9 @@ DescEx="

Constructs and returns a TRange record with bounds A< Depends=TRange SeeAlso=TRange SeeAlso=TRange,TRangeEx,TIntegerRange +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=580.dat Delphi7=Y Delphi2005Win32=Y @@ -77,6 +86,9 @@ FPC=Y DescEx="

Constructs and returns a TRectF record with the given left, top, right & bottom coordinates.

" SeeAlso=BoundsF,PointF,TRectF Depends=TRectF +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=492.dat Delphi2=Y Delphi3=Y @@ -102,6 +114,9 @@ DescEx="

Constructs and returns a TSize record with the given dimen Extra="

This routine complements the Rect and Point routines defined in the Delphi RTL.

For versions of Delphi before Delphi 6 replace the Types unit with Windows to get the routine to compile.

" Units=Types SeeAlso=TSizeEx +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=488.dat Delphi2=N Delphi3=N @@ -140,6 +155,9 @@ Delphi12A=Y Kind=type DescEx="

Encapsulates a point with double precision floating point coordinates.

" SeeAlso=PointF,TRectF +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=489.dat Delphi2=Y Delphi3=Y @@ -165,6 +183,9 @@ Kind=type DescEx="

Encapsulates the upper and lower bounds of a range of values.

" SeeAlso=Range SeeAlso=Range,TRangeEx,TIntegerRange +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=579.dat Delphi7=Y Delphi2005Win32=Y @@ -185,6 +206,9 @@ Kind=type DescEx="

Encapsulates a rectangle with double precision floating point size and position.

" SeeAlso=RectF,BoundsF,TPointF Depends=TPointF +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=490.dat Delphi2=Y Delphi3=Y @@ -210,6 +234,9 @@ Kind=class DescEx="

Encapsulates a range of integers with a methods to test whether a value falls within the range and to adjust the value to fit.

" Units=Math SeeAlso=Range,TRange,TIntegerRange +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=578.dat Delphi2=N Delphi3=N @@ -235,6 +262,9 @@ Kind=class DescEx="

Record that represents the size, i.e. the width and height, of something.

This is an extended version of the TSize record that features equality and inequality operator overloading, a test for zero size and a constructor.

TSizeEx is assignment compatible and comparible with the Delphi RTL's TSize record.

" Units=Types SeeAlso=Size +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Structs" Snip=510.dat Delphi2=N Delphi3=N From 278652a431cebcd5fa910ec9d3e241b28fee8463 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:10:14 +0100 Subject: [PATCH 07/15] Correct erroneous snippet description in structs cat The snippet description for the `Range` snippet had its description fixed not to claim that a range of length 1 was empty. --- collection/structs.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/collection/structs.ini b/collection/structs.ini index 5d5e835..ed275a3 100644 --- a/collection/structs.ini +++ b/collection/structs.ini @@ -60,7 +60,7 @@ Delphi12A=Y FPC=Y [Range] -DescEx="

Constructs and returns a TRange record with bounds A and B.

The smaller of A and B is used as the lower bound and the larger as the upper bound. If both values are equal then the range will be empty.

" +DescEx="

Constructs and returns a TRange record with bounds A and B.

The smaller of A and B is used as the lower bound and the other parameter is used as the upper bound.

" Depends=TRange SeeAlso=TRange SeeAlso=TRange,TRangeEx,TIntegerRange From 53d88d481e98d89f2da25062f8db6a11475dcf16 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:12:55 +0100 Subject: [PATCH 08/15] Add warning to potentially buggy snippet meta data The `TRangeEx` advanced record had a warning added to its extra information re buggy behaviour if the range's lower bound was set greater than the upper bound. --- collection/structs.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/collection/structs.ini b/collection/structs.ini index ed275a3..3091f08 100644 --- a/collection/structs.ini +++ b/collection/structs.ini @@ -232,6 +232,7 @@ FPC=Y [TRangeEx] Kind=class DescEx="

Encapsulates a range of integers with a methods to test whether a value falls within the range and to adjust the value to fit.

" +Extra="

Warning: It is up to the caller to ensure that the Min field is always less than or equal to the Max field otherwise the Constrain method will return crazy values and Contains will always return False .

" Units=Math SeeAlso=Range,TRange,TIntegerRange TestInfo=advanced From a4f911eaeec2c2198c7882863afd8796344e74ff Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Fri, 4 Apr 2025 13:16:20 +0100 Subject: [PATCH 09/15] Tidy up TestTIntegerRange code Removed the unused Setup and TearDown methods from the test class. Removed commented out test code from the class implementation. --- tests/Cat-Structs/TestUStructCatSnippets.pas | 105 ------------------- 1 file changed, 105 deletions(-) diff --git a/tests/Cat-Structs/TestUStructCatSnippets.pas b/tests/Cat-Structs/TestUStructCatSnippets.pas index 791cd76..48f0bb8 100644 --- a/tests/Cat-Structs/TestUStructCatSnippets.pas +++ b/tests/Cat-Structs/TestUStructCatSnippets.pas @@ -47,9 +47,6 @@ TestTIntegerRange = class(TTestCase) private procedure TestCtorAndPropsException; procedure TestConstrainException; - public - procedure SetUp; override; - procedure TearDown; override; published procedure TestCtorAndProps; procedure TestCreateEmpty; @@ -382,18 +379,6 @@ procedure TestTRangeEx.TestCtorAndFields; { TestTIntegerRange } -procedure TestTIntegerRange.SetUp; -begin - inherited; - -end; - -procedure TestTIntegerRange.TearDown; -begin - inherited; - -end; - procedure TestTIntegerRange.TestAddOp; var L, R, E: TIntegerRange; @@ -544,51 +529,6 @@ procedure TestTIntegerRange.TestExplicitToStringOp; CheckEquals('[-42..56]', string(R)); end; -//procedure TestTIntegerRange.TestGreaterThanOp; -//var -// L, R: TIntegerRange; -//begin -// L := TIntegerRange.Create(0, 0); -// R := TIntegerRange.Create(0, 0); -// CheckFalse(L > R, '#1a'); -// CheckFalse(R > L, '#1b'); -// L := TIntegerRange.Create(0, 1); -// R := TIntegerRange.Create(0, 0); -// CheckTrue(L > R, '#2a'); -// CheckFalse(R > L, '#2b'); -// L := TIntegerRange.Create(-3, 12); -// R := TIntegerRange.Create(-5, 18); -// CheckFalse(L > R, '#3a'); -// CheckTrue(R > L, '#3b'); -// L := TIntegerRange.Create(-5, 17); -// R := TIntegerRange.Create(-5, 18); -// CheckFalse(L > R, '#4a'); -// CheckTrue(R > L, '#4b'); -// L := TIntegerRange.Create(-4, 18); -// R := TIntegerRange.Create(-5, 18); -// CheckFalse(L > R, '#5a'); -// CheckTrue(R > L, '#5b'); -// L := TIntegerRange.Create(-5, 18); -// R := TIntegerRange.Create(-5, 18); -// CheckFalse(L > R, '#6a'); -// CheckFalse(R > L, '#6b'); -// L := TIntegerRange.Create(-5, 2); -// R := TIntegerRange.Create(4, 18); -// CheckFalse(L > R, '#7a'); -// CheckFalse(R > L, '#7b'); -// L := TIntegerRange.Create(1, 1); -// R := TIntegerRange.Create(4, 18); -// CheckFalse(L > R, '#8a'); -// CheckFalse(R > L, '#8b'); -// L := TIntegerRange.CreateEmpty; -// R := TIntegerRange.CreateEmpty; -// CheckFalse(L > R, '#9'); -// L := TIntegerRange.Create(10, 10); -// R := TIntegerRange.CreateEmpty; -// CheckTrue(L > R, '#10a'); -// CheckFalse(R > L, '#10b'); -//end; - procedure TestTIntegerRange.TestGreaterThanOrEqualOp; var L, R: TIntegerRange; @@ -773,51 +713,6 @@ procedure TestTIntegerRange.TestLength; CheckEquals(High(Cardinal), R.Length, '#7'); end; -//procedure TestTIntegerRange.TestLessThanOp; -//var -// L, R: TIntegerRange; -//begin -// L := TIntegerRange.Create(0, 0); -// R := TIntegerRange.Create(0, 0); -// CheckFalse(L < R, '#1a'); -// CheckFalse(R < L, '#1b'); -// L := TIntegerRange.Create(0, 0); -// R := TIntegerRange.Create(0, 1); -// CheckTrue(L < R, '#2a'); -// CheckFalse(R < L, '#2b'); -// L := TIntegerRange.Create(-3, 12); -// R := TIntegerRange.Create(-5, 18); -// CheckTrue(L < R, '#3a'); -// CheckFalse(R < L, '#3b'); -// L := TIntegerRange.Create(-5, 17); -// R := TIntegerRange.Create(-5, 18); -// CheckTrue(L < R, '#4a'); -// CheckFalse(R < L, '#4b'); -// L := TIntegerRange.Create(-4, 18); -// R := TIntegerRange.Create(-5, 18); -// CheckTrue(L < R, '#5a'); -// CheckFalse(R < L, '#5b'); -// L := TIntegerRange.Create(-5, 18); -// R := TIntegerRange.Create(-5, 18); -// CheckFalse(L < R, '#6a'); -// CheckFalse(R < L, '#6b'); -// L := TIntegerRange.Create(-5, 2); -// R := TIntegerRange.Create(4, 18); -// CheckFalse(L < R, '#7a'); -// CheckFalse(R < L, '#7b'); -// L := TIntegerRange.Create(1, 1); -// R := TIntegerRange.Create(4, 18); -// CheckFalse(L < R, '#8a'); -// CheckFalse(R < L, '#8b'); -// L := TIntegerRange.CreateEmpty; -// R := TIntegerRange.CreateEmpty; -// CheckFalse(L < R, '#9'); -// L := TIntegerRange.Create(10, 10); -// R := TIntegerRange.CreateEmpty; -// CheckFalse(L < R, '#10a'); -// CheckTrue(R < L, '#10b'); -//end; - procedure TestTIntegerRange.TestLessThanOrEqualOp; var L, R: TIntegerRange; From b1f724851d624b8510ac2c1e2b33c094ba971f7d Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Sat, 5 Apr 2025 11:38:35 +0100 Subject: [PATCH 10/15] Add more unit tests for arrays category snippets Added tests to TestUArraysCatSnippets unit for all snippets in the Arrays category that didn't already have them. --- tests/Cat-Arrays/TestUArraysCatSnippets.pas | 471 +++++++++++++++++++- 1 file changed, 467 insertions(+), 4 deletions(-) diff --git a/tests/Cat-Arrays/TestUArraysCatSnippets.pas b/tests/Cat-Arrays/TestUArraysCatSnippets.pas index baae0e5..4628a3e 100644 --- a/tests/Cat-Arrays/TestUArraysCatSnippets.pas +++ b/tests/Cat-Arrays/TestUArraysCatSnippets.pas @@ -3,7 +3,7 @@ interface uses - TestFramework, UArraysCatSnippets; + TestFramework, UArraysCatSnippets, Types; type @@ -44,16 +44,34 @@ TestTArrayUtils = class(TTestCase) end; TestArraysCatSnippets = class(TTestCase) + private + function StringArraysEqual(const L, R: TStringDynArray): Boolean; + procedure ByteArraysSameStart_AssertionFailure; + procedure PopByteArray_AssertionFailure; + procedure ShiftByteArray_AssertionFailure; published procedure TestByteArraysEqual; - // The following test must come after TestByteArraysEqual since the test calls it - procedure TestReverseByteArray; + procedure TestAppendByteArray; // test requires ByteArraysEqual + procedure TestArrayToStringList; + procedure TestByteArraysSameStart; + procedure TestChopByteArray; // test requires ByteArraysEqual + procedure TestCloneByteArray; // test requires ByteArraysEqual + procedure TestConcatByteArrays; // test requires ByteArraysEqual + procedure TestIndexOfByte; + procedure TestLastIndexOfByte; + procedure TestPopByteArray; // test requires ByteArraysEqual + procedure TestPushByteArray; // test requires ByteArraysEqual + procedure TestReverseByteArray; // test requires ByteArraysEqual + procedure TestShiftByteArray; // test requires ByteArraysEqual + procedure TestSliceByteArray; // test requires ByteArraysEqual + procedure TestStringListToArray; + procedure TestUnShiftByteArray; // test requires ByteArraysEqual end; implementation uses - SysUtils, Generics.Defaults; + SysUtils, Generics.Defaults, Classes; var IntegerCompareFn: TEqualityComparison; @@ -305,6 +323,118 @@ procedure TestTArrayUtils.TestSameStart; { TestArraysCatSnippets } +procedure TestArraysCatSnippets.ByteArraysSameStart_AssertionFailure; +const + A1: array[1..3] of Byte = (1, 2, 3); + A2: array[11..15] of Byte = (1, 2, 3, 4, 5); +begin + ByteArraysSameStart(A1, A2, 0); +end; + +procedure TestArraysCatSnippets.PopByteArray_AssertionFailure; +var + A: TBytes; +begin + // Attempt to pop empty array + SetLength(A, 0); + PopByteArray(A); +end; + +procedure TestArraysCatSnippets.ShiftByteArray_AssertionFailure; +var + A: TBytes; +begin + // Attempt to shift empty array + SetLength(A, 0); + ShiftByteArray(A); +end; + +function TestArraysCatSnippets.StringArraysEqual(const L, R: TStringDynArray): + Boolean; +var + I: Integer; +begin + Result := Length(L) = Length(R); + if Result then + begin + for I := 0 to High(L) do + begin + if L[I] <> R[I] then + begin + Result := False; + Exit; + end; + end; + end; +end; + +procedure TestArraysCatSnippets.TestAppendByteArray; +var + B1, B2, E: TBytes; +begin + B1 := TBytes.Create(42, 56); + B2 := TBytes.Create(99, 199, 201); + E := TBytes.Create(42, 56, 99, 199, 201); + AppendByteArray(B1, B2); + CheckTrue(ByteArraysEqual(E, B1), '#1'); + + SetLength(B1, 0); + B2 := TBytes.Create(1, 2, 3, 4, 5); + E := TBytes.Create(1, 2, 3, 4, 5); + AppendByteArray(B1, B2); + CheckTrue(ByteArraysEqual(E, B1), '#2'); + + SetLength(B1, 0); + SetLength(B2, 0); + SetLength(E, 0); + AppendByteArray(B1, B2); + CheckTrue(ByteArraysEqual(E, B1), '#3'); + + SetLength(B1, 0); + B2 := TBytes.Create(12, 89); + E := TBytes.Create(12, 89); + AppendByteArray(B1, B2); + CheckTrue(ByteArraysEqual(E, B1), '#4'); + + B1 := TBytes.Create(1, 2, 3); + SetLength(B2, 0); + E := TBytes.Create(1, 2, 3); + AppendByteArray(B1, B2); + CheckTrue(ByteArraysEqual(E, B1), '#5'); +end; + +procedure TestArraysCatSnippets.TestArrayToStringList; +const + S1: array[0..0] of string = ('single elem'); + S3: array[1..3] of string = ('one', 'two', 'three'); +var + S0: array of string; + SL: TStringList; + Expected: string; +begin + SetLength(S0, 0); + SL := TStringList.Create; + try + SL.LineBreak := sLineBreak; + ArrayToStringList(S1, SL); + Expected := 'single elem'; + CheckEquals(Expected, Trim(SL.Text), '#1a'); + CheckEquals(1, SL.Count, '#1b'); + + ArrayToStringList(S3, SL); + Expected := 'one' + sLineBreak + 'two' + sLineBreak + 'three'; + CheckEquals(Expected, Trim(SL.Text), '#2a'); + CheckEquals(3, SL.Count, '#2b'); + + ArrayToStringList(S0, SL); + Expected := ''; + CheckEquals(Expected, Trim(SL.Text), '#3a'); + CheckEquals(0, SL.Count, '#3b'); + finally + SL.Free; + end; +end; + procedure TestArraysCatSnippets.TestByteArraysEqual; var A0L, A0R: TBytes; @@ -337,6 +467,215 @@ procedure TestArraysCatSnippets.TestByteArraysEqual; CheckFalse(ByteArraysEqual(A0L, A1L), '#10'); end; +procedure TestArraysCatSnippets.TestByteArraysSameStart; +const + A1: array[1..3] of Byte = (1, 2, 3); + A2: array[11..15] of Byte = (1, 2, 3, 4, 5); + A3: array[0..3] of Byte = (2, 3, 4, 5); + A4: array[0..0] of Byte = (2); +var + A0: array of Byte; +begin + SetLength(A0, 0); + CheckTrue(ByteArraysSameStart(A1, A2, 2), '#1a'); + CheckTrue(ByteArraysSameStart(A1, A2, 3), '#1b'); + CheckFalse(ByteArraysSameStart(A1, A2, 4), '#1c'); + CheckFalse(ByteArraysSameStart(A1, A2, 12), '#1d'); + CheckTrue(ByteArraysSameStart(A3, A4, 1), '#2a'); + CheckFalse(ByteArraysSameStart(A3, A4, 2), '#2b'); + CheckFalse(ByteArraysSameStart(A1, A3, 1), '#3a'); + CheckFalse(ByteArraysSameStart(A1, A3, 2), '#3b'); + CheckFalse(ByteArraysSameStart(A1, A3, 12), '#3c'); + CheckTrue(ByteArraysSameStart(A2, A2, 1), '#4a'); + CheckTrue(ByteArraysSameStart(A2, A2, 5), '#4b'); + CheckFalse(ByteArraysSameStart(A2, A2, 6), '#4c'); + CheckFalse(ByteArraysSameStart(A0, A0, 1), '#5'); + CheckException(ByteArraysSameStart_AssertionFailure, EAssertionFailed, 'Assert failed'); +end; + +procedure TestArraysCatSnippets.TestChopByteArray; +var + A, R, E: TBytes; +begin + // test zero length array => always returns empty array + SetLength(A, 0); + SetLength(E, 0); + R := ChopByteArray(A, 0, 0); + CheckTrue(ByteArraysEqual(E, R), '#1a'); + R := ChopByteArray(A, -3, -3); + CheckTrue(ByteArraysEqual(E, R), '#1b'); + R := ChopByteArray(A, 2, 29); + CheckTrue(ByteArraysEqual(E, R), '#1c'); + + // test normal cases + A := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + R := ChopByteArray(A, 2, 4); + E := TBytes.Create(1, 2, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#2a'); + R := ChopByteArray(A, 0, 2); + E := TBytes.Create(3, 4, 5, 6, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#2b'); + R := ChopByteArray(A, 9, 1); + E := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9); + CheckTrue(ByteArraysEqual(E, R), '#2c'); + R := ChopByteArray(A, 0, 10); + SetLength(E, 0); + CheckTrue(ByteArraysEqual(E, R), '#2d'); + R := ChopByteArray(A, 0, 1); + E := TBytes.Create(2, 3, 4, 5, 6, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#2e'); + + // test parameter out of bounds cases + // length = 0 => return whole array + A := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + R := ChopByteArray(A, 3, 0); + E := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#3a'); + // start < 0 => start = 0 + R := ChopByteArray(A, -12, 2); + E := TBytes.Create(3, 4, 5, 6, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#3b'); + // length < 0 => length = 0 => return whole array + R := ChopByteArray(A, 3, -12); + E := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#3c'); + // start > length of array => return whole array + R := ChopByteArray(A, 11, 4); + E := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#3d'); + // length goes beyond end of array => chop from start to end + R := ChopByteArray(A, 7, 100); + E := TBytes.Create(1, 2, 3, 4, 5, 6, 7); + CheckTrue(ByteArraysEqual(E, R), '#3e'); +end; + +procedure TestArraysCatSnippets.TestCloneByteArray; +var + B0, B1, B6, R: TBytes; +begin + SetLength(B0, 0); + B1 := TBytes.Create(42); + B6 := TBytes.Create(1, 1, 2, 3, 5, 8); + R := CloneByteArray(B0); + CheckTrue(ByteArraysEqual(B0, R), '#1'); + R := CloneByteArray(B1); + CheckTrue(ByteArraysEqual(B1, R), '#2'); + R := CloneByteArray(B6); + CheckTrue(ByteArraysEqual(B6, R), '#3'); +end; + +procedure TestArraysCatSnippets.TestConcatByteArrays; +var + B1, B2, R, E: TBytes; +begin + B1 := TBytes.Create(42, 56); + B2 := TBytes.Create(99, 199, 201); + E := TBytes.Create(42, 56, 99, 199, 201); + R := ConcatByteArrays(B1, B2); + CheckTrue(ByteArraysEqual(E, R), '#1'); + + SetLength(B1, 0); + B2 := TBytes.Create(1, 2, 3, 4, 5); + E := TBytes.Create(1, 2, 3, 4, 5); + R := ConcatByteArrays(B1, B2); + CheckTrue(ByteArraysEqual(E, R), '#2'); + + SetLength(B1, 0); + SetLength(B2, 0); + SetLength(E, 0); + R := ConcatByteArrays(B1, B2); + CheckTrue(ByteArraysEqual(E, R), '#3'); + + SetLength(B1, 0); + B2 := TBytes.Create(12, 89); + E := TBytes.Create(12, 89); + R := ConcatByteArrays(B1, B2); + CheckTrue(ByteArraysEqual(E, R), '#4'); + + B1 := TBytes.Create(1, 2, 3); + SetLength(B2, 0); + E := TBytes.Create(1, 2, 3); + R := ConcatByteArrays(B1, B2); + CheckTrue(ByteArraysEqual(E, R), '#5'); +end; + +procedure TestArraysCatSnippets.TestIndexOfByte; +var + B0, B1, B6: TBytes; +begin + SetLength(B0, 0); + B1 := TBytes.Create(3); + B6 := TBytes.Create(1, 1, 2, 3, 5, 8); + CheckEquals(-1, IndexOfByte(3, B0), '#3a'); + CheckEquals(0, IndexOfByte(3, B1), '#3b'); + CheckEquals(3, IndexOfByte(3, B6), '#3c'); + CheckEquals(-1, IndexOfByte(1, B0), '#1a'); + CheckEquals(-1, IndexOfByte(1, B1), '#1b'); + CheckEquals(0, IndexOfByte(1, B6), '#1c'); + CheckEquals(-1, IndexOfByte(8, B0), '#8a'); + CheckEquals(-1, IndexOfByte(8, B1), '#8b'); + CheckEquals(5, IndexOfByte(8, B6), '#8c'); +end; + +procedure TestArraysCatSnippets.TestLastIndexOfByte; +var + B0, B2, B6: TBytes; +begin + SetLength(B0, 0); + B2 := TBytes.Create(3, 3); + B6 := TBytes.Create(1, 1, 2, 3, 5, 8); + CheckEquals(-1, LastIndexOfByte(3, B0), '#3a'); + CheckEquals(1, LastIndexOfByte(3, B2), '#3b'); + CheckEquals(3, LastIndexOfByte(3, B6), '#3c'); + CheckEquals(-1, LastIndexOfByte(1, B0), '#1a'); + CheckEquals(-1, LastIndexOfByte(1, B2), '#1b'); + CheckEquals(1, LastIndexOfByte(1, B6), '#1c'); + CheckEquals(-1, LastIndexOfByte(8, B0), '#8a'); + CheckEquals(-1, LastIndexOfByte(8, B2), '#8b'); + CheckEquals(5, LastIndexOfByte(8, B6), '#8c'); +end; + +procedure TestArraysCatSnippets.TestPopByteArray; +var + A, E: TBytes; + R: Byte; +begin + // pop value from end of multi-value array + A := TBytes.Create(1, 2, 3); + E := TBytes.Create(1, 2); + R := PopByteArray(A); + CheckTrue(ByteArraysEqual(E, A), '#1a'); + CheckEquals(2, Length(A), '#1b'); + CheckEquals(3, R, '#1c'); + // pop value from end of single value array + A := TBytes.Create(1); + SetLength(E, 0); + R := PopByteArray(A); + CheckTrue(ByteArraysEqual(E, A), '#2a'); + CheckEquals(0, Length(A), '#2b'); + CheckEquals(1, R, '#2c'); + // check assertion failure for empty array + CheckException(PopByteArray_AssertionFailure, EAssertionFailed, 'Assert'); +end; + +procedure TestArraysCatSnippets.TestPushByteArray; +var + A, E: TBytes; +begin + // push value to end of non-empty array + A := TBytes.Create(1, 2, 3); + E := TBytes.Create(1, 2, 3, 42); + PushByteArray(42, A); + CheckTrue(ByteArraysEqual(E, A), '#1a'); + CheckEquals(4, Length(A), '#1b'); + // push value to end of empty array + SetLength(A, 0); + E := TBytes.Create(56); + PushByteArray(56, A); + CheckTrue(ByteArraysEqual(E, A), '#2a'); + CheckEquals(1, Length(A), '#2b'); +end; + procedure TestArraysCatSnippets.TestReverseByteArray; var A0, A1, A2, A6, A7, A4Sym, A5Sym: TBytes; @@ -363,6 +702,130 @@ procedure TestArraysCatSnippets.TestReverseByteArray; CheckTrue(ByteArraysEqual(A5Sym, ReverseByteArray(A5Sym)), '#5 sym'); end; +procedure TestArraysCatSnippets.TestShiftByteArray; +var + A, E: TBytes; + R: Byte; +begin + // pop value from start of multi-value array + A := TBytes.Create(1, 2, 3); + E := TBytes.Create(2, 3); + R := ShiftByteArray(A); + CheckTrue(ByteArraysEqual(E, A), '#1a'); + CheckEquals(2, Length(A), '#1b'); + CheckEquals(1, R, '#1c'); + // pop value from start of single value array + A := TBytes.Create(42); + SetLength(E, 0); + R := ShiftByteArray(A); + CheckTrue(ByteArraysEqual(E, A), '#2a'); + CheckEquals(0, Length(A), '#2b'); + CheckEquals(42, R, '#2c'); + // check assertion failure for empty array + CheckException(ShiftByteArray_AssertionFailure, EAssertionFailed, 'Assert'); +end; + +procedure TestArraysCatSnippets.TestSliceByteArray; +var + A, R, E: TBytes; +begin + // test zero length array => always returns empty array + SetLength(A, 0); + SetLength(E, 0); + R := SliceByteArray(A, 0, 0); + CheckTrue(ByteArraysEqual(E, R), '#1a'); + R := SliceByteArray(A, -3, -3); + CheckTrue(ByteArraysEqual(E, R), '#1b'); + R := SliceByteArray(A, 2, 29); + CheckTrue(ByteArraysEqual(E, R), '#1c'); + + // test normal cases + A := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + R := SliceByteArray(A, 3, 5); + E := TBytes.Create(4, 5, 6, 7, 8); + CheckTrue(ByteArraysEqual(E, R), '#2a'); + R := SliceByteArray(A, 0, 1); + E := TBytes.Create(1); + CheckTrue(ByteArraysEqual(E, R), '#2b'); + R := SliceByteArray(A, 7, 3); + E := TBytes.Create(8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#2c'); + R := SliceByteArray(A, 0, 10); + E := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + CheckTrue(ByteArraysEqual(E, R), '#2d'); + + // test parameter out of bounds cases + // length = 0 => return empty array + A := TBytes.Create(1, 2, 3, 4, 5, 6, 7, 8, 9, 10); + R := SliceByteArray(A, 3, 0); + SetLength(E, 0); + CheckTrue(ByteArraysEqual(E, R), '#3a'); + // start < 0 => start = 0 + R := SliceByteArray(A, -12, 2); + E := TBytes.Create(1, 2); + CheckTrue(ByteArraysEqual(E, R), '#3b'); + // length < 0 => length = 0 => return empty array + R := SliceByteArray(A, 3, -12); + SetLength(E, 0); + CheckTrue(ByteArraysEqual(E, R), '#3c'); + // start > length of array => return empty array + R := SliceByteArray(A, 11, 4); + SetLength(E, 0); + CheckTrue(ByteArraysEqual(E, R), '#3d'); + // length goes beyond end of array => return from start to end of array + R := SliceByteArray(A, 8, 100); + E := TBytes.Create(9, 10); + CheckTrue(ByteArraysEqual(E, R), '#3e'); +end; + +procedure TestArraysCatSnippets.TestStringListToArray; +var + SL: TStringList; + A, E: TStringDynArray; +begin + SL := TStringList.Create; + try + SL.Clear; + SetLength(E, 0); + A := StringListToArray(SL); + CheckTrue(StringArraysEqual(E, A), '#1'); + + SL.Clear; + SL.Add('one'); + A := StringListToArray(SL); + E := TStringDynArray.Create('one'); + CheckTrue(StringArraysEqual(E, A), '#2'); + + SL.Clear; + SL.Add('one'); + SL.Add('two'); + SL.Add('three'); + A := StringListToArray(SL); + E := TStringDynArray.Create('one', 'two', 'three'); + CheckTrue(StringArraysEqual(E, A), '#3'); + finally + SL.Free; + end; +end; + +procedure TestArraysCatSnippets.TestUnShiftByteArray; +var + A, E: TBytes; +begin + // push value to start of non-empty array + A := TBytes.Create(1, 2, 3); + E := TBytes.Create(42, 1, 2, 3); + UnShiftByteArray(42, A); + CheckTrue(ByteArraysEqual(E, A), '#1a'); + CheckEquals(4, Length(A), '#1b'); + // push value to start of empty array + SetLength(A, 0); + E := TBytes.Create(56); + UnShiftByteArray(56, A); + CheckTrue(ByteArraysEqual(E, A), '#2a'); + CheckEquals(1, Length(A), '#2b'); +end; + initialization IntegerCompareFn := function (const Left, Right: Integer): Boolean From a7ca4491727e9d120e243d43aa117ef9d9c13c56 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Sat, 5 Apr 2025 11:39:41 +0100 Subject: [PATCH 11/15] Update arrays.ini with advanced testing info Added advanced testing information for all snippets in arrays.ini that didn't already have it: all snippets in this category now have unit tests in the same DUnit tests project. Set TestInfo=advanced, AdvancedTest.Level=unit-tests and AdvancedTest.URL to the Cat-Arrays unit test project on Github. --- collection/arrays.ini | 45 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/collection/arrays.ini b/collection/arrays.ini index 19ac33c..11ffc5a 100644 --- a/collection/arrays.ini +++ b/collection/arrays.ini @@ -9,6 +9,9 @@ DescEx="

Copies the elements of string array Strings to string list SL, replacing any existing contents of SL.

" SeeAlso=StringListToArray Units=Classes +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=533.dat Delphi2=N Delphi3=N @@ -32,6 +35,9 @@ FPC=Y [ByteArraysEqual] DescEx="

Checks if two byte arrays are equal.

The arrays are equal if they have the same number of elements and elements at the same position in the array are equal.

" SeeAlso=ByteArraysSameStart +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=365.dat Delphi2=N Delphi3=N @@ -55,6 +61,9 @@ FPC=Y [ByteArraysSameStart] DescEx="

Checks if two byte arrays B1 and B2 are equal for the first Count elements.

False is returned if any array has less than Count elements.

Count must be >= 1.

" SeeAlso=ByteArraysEqual +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=481.dat Delphi2=N Delphi3=N @@ -78,6 +87,9 @@ FPC=Y [IndexOfByte] DescEx="

Returns the index of the first occurrence of byte B in byte array A, or -1 if B is not in A.

" SeeAlso=LastIndexOfByte +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=482.dat Delphi2=N Delphi3=N @@ -101,6 +113,9 @@ FPC=Y [LastIndexOfByte] DescEx="

Returns the index of the last occurrence of byte B in byte array A, or -1 if B is not in A.

" SeeAlso=IndexOfByte +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=483.dat Delphi2=N Delphi3=N @@ -125,6 +140,9 @@ FPC=Y DescEx="

Creates and returns a dynamic string array containing all the strings from the given string list.

" SeeAlso=ArrayToStringList Units=Classes,Types +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=347.dat Delphi2=N Delphi3=N @@ -149,6 +167,9 @@ FPC=Y DescEx="

Appends array of bytes B2 to the end of byte array B1.

" Depends=TBytes SeeAlso=ConcatByteArrays +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=364.dat Delphi2=N Delphi3=N @@ -173,6 +194,9 @@ FPC=Y DescEx="

Deletes a sequence of bytes from byte array B starting at index Start with length Len.

If either Start or Len are less than 0 they are taken as zero. If Start is beyond the end of the array or if Len is 0 then the whole array is returned unchanged. If the sequence of bytes to be chopped extends beyond the end of the array it is truncated from Start.

" Depends=TBytes,ConcatByteArrays,SliceByteArray SeeAlso=SliceByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=366.dat Delphi2=N Delphi3=N @@ -198,6 +222,9 @@ DescEx="

Makes a copy of an array of bytes.

" Extra="

Useful for creating a TBytes array from a constant array or for cloning an existing byte array.

" Depends=TBytes SeeAlso=AppendByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=367.dat Delphi2=N Delphi3=N @@ -222,6 +249,9 @@ FPC=Y DescEx="

Concatenates two byte arrays B1 and B2 and returns the resulting array.

The result is the contents of B1 followed by the contents of B2.

" Depends=TBytes,AppendByteArray,CloneByteArray SeeAlso=AppendByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=368.dat Delphi2=N Delphi3=N @@ -246,6 +276,9 @@ FPC=Y DescEx="

Removes the last element of byte array A and returns the element. The length of A shrinks by one.

A must not be empty.

" Depends=TBytes SeeAlso=PushByteArray,ShiftByteArray,UnShiftByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=484.dat Delphi2=N Delphi3=N @@ -270,6 +303,9 @@ FPC=Y DescEx="

Pushes byte B onto the end of byte array A. The length of A grows by one.

" Depends=TBytes SeeAlso=PopByteArray,ShiftByteArray,UnShiftByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=485.dat Delphi2=N Delphi3=N @@ -294,6 +330,9 @@ FPC=Y DescEx="

Removes the first element of byte array A and returns the element. The length of A shrinks by one.

A must not be empty.

" Depends=TBytes SeeAlso=PopByteArray,PushByteArray,UnShiftByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=486.dat Delphi2=N Delphi3=N @@ -318,6 +357,9 @@ FPC=Y DescEx="

Slices a range of bytes from byte array B, starting at index Start with length Len, and returns the result.

If either Start or Len are less than 0, they are taken as 0. If Start is beyond the end of the array or if Len is 0 then an empty array is returned. If the sequence of bytes to be sliced extends beyond the end of the array it is truncated from Start.

" Depends=TBytes SeeAlso=ChopByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=369.dat Delphi2=N Delphi3=N @@ -342,6 +384,9 @@ FPC=Y DescEx="

Inserts byte B at the beginning of byte array A. The length of A grows by one.

" Depends=TBytes SeeAlso=PopByteArray,PushByteArray,ShiftByteArray +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-Arrays" Snip=487.dat Delphi2=N Delphi3=N From 58104604212f4900eaab961a76bcec8ed7f6e097 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Sat, 5 Apr 2025 11:42:28 +0100 Subject: [PATCH 12/15] Update and correct tests/README.md Noted that all snippets in Arrays category now have unit tests. Added warning that newer tests are only checked for compiling with Delphi XE and later. Corrected some errors and inconsistencies. --- tests/README.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/README.md b/tests/README.md index e28d032..36fedfa 100644 --- a/tests/README.md +++ b/tests/README.md @@ -2,6 +2,8 @@ This project provides tests and demos of snippets from the [DelphiDabbler Code Snippets Database](https://github.com/delphidabbler/code-snippets). +> ⚠️ Recent changes have only been compiled and tested with Delphi XE and later. While project files for earlier versions of Delphi remain available it is not guaranteed that any of the projects will compile with versions of Delphi prior to Delphi XE. + ## Directory Structure The project has the following directories: @@ -15,7 +17,7 @@ Contains: ### `./Cat-Arrays` -Contains the _TestArraysCat_[^2] project that provides _DUnit_ tests for snippets from the database's _Arrays_ category. Currently tests are only available for the _TArrayUtils_ advanced record. +Contains the _TestArraysCat_[^2] project that provides _DUnit_ tests for all the snippets in the database's _Arrays_ category. ### `./Cat-Date` @@ -27,7 +29,7 @@ Contains the _TestDriveCat_[^2] project that implements a GUI application that e ### `./Cat-Hex` -Contains the _TestHexCat_[^2] project that provides _DUnit_ tests for all the snippets from the database's _Hex Utilities_ category. +Contains the _TestHexCat_[^2] project that provides _DUnit_ tests for all the snippets in the database's _Hex Utilities_ category. ### `./Cat-Maths` @@ -35,11 +37,11 @@ Contains the _TestCatMaths_[^2] project that provides _DUnit_ tests for selected ### `./Cat-String` -Contains the _TestCatStructsXE_[^3] project that provides _DUnit_ tests for selected snippets from the database's _Structures_ category. +Contains the _TestCatString_[^2] project that provides _DUnit_ tests for selected snippets from the database's _String Management_ category. ### `./Cat-Structs` -Contains the _TestCatString_[^2] project that provides _DUnit_ tests for selected snippets from the database's _String Management_ category. +Contains the _TestCatStructsXE_[^3] project that provides _DUnit_ tests for all snippets in the database's _Structures_ category. ### `./Cat-WinSys` From 3a69083735fa882749fa8f1ca985f57a700d48e3 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Sat, 5 Apr 2025 11:52:37 +0100 Subject: [PATCH 13/15] Fix minor type in tests/README.md --- tests/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/README.md b/tests/README.md index 36fedfa..203d090 100644 --- a/tests/README.md +++ b/tests/README.md @@ -45,7 +45,7 @@ Contains the _TestCatStructsXE_[^3] project that provides _DUnit_ tests for all ### `./Cat-WinSys` -Contains the _TextWinSysCat_[^2] project that provides a GUI application that exercises and demonstrates all the code from the database's _Windows System_ category. +Contains the _TestWinSysCat_[^2] project that provides a GUI application that exercises and demonstrates all the code from the database's _Windows System_ category. ### History From e314b6e56f033443e387c514584652a660159086 Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Sat, 5 Apr 2025 13:05:24 +0100 Subject: [PATCH 14/15] Add new RandomString function to string category A new source code file was added for RandomString Meta data for RandomString was added to string.ini. --- collection/707.dat | 8 ++++++++ collection/string.ini | 11 +++++++++++ 2 files changed, 19 insertions(+) create mode 100644 collection/707.dat diff --git a/collection/707.dat b/collection/707.dat new file mode 100644 index 0000000..5bf0e60 --- /dev/null +++ b/collection/707.dat @@ -0,0 +1,8 @@ +function RandomString(const SL: Classes.TStrings): string; overload; +begin + if SL.Count = 0 then + raise SysUtils.EArgumentException.Create( + 'RandomString called with empty string list' + ); + Result := SL[Random(SL.Count)]; +end; \ No newline at end of file diff --git a/collection/string.ini b/collection/string.ini index a7ee191..6d65508 100644 --- a/collection/string.ini +++ b/collection/string.ini @@ -1807,3 +1807,14 @@ DelphiXE4=Y Delphi10S=Y Delphi12A=Y FPC=Y + +[RandomString] +DescEx="

Returns a random string from the given non-empty string list.

An EArgumentException exception is raised if the string list is empty.

" +Extra="

The Delphi RTL has a similar RandomFrom function in StrUtils that returns a random string from a string array.

" +Units=SysUtils,Classes +TestInfo=advanced +AdvancedTest.Level=unit-tests +AdvancedTest.URL="https://github.com/delphidabbler/code-snippets/tree/master/tests/Cat-String" +Snip=707.dat +DelphiXE=Y +Delphi12A=Y From 5e0cf738721bdc8576c63ef648a2649597519c0d Mon Sep 17 00:00:00 2001 From: delphidabbler <5164283+delphidabbler@users.noreply.github.com> Date: Sat, 5 Apr 2025 13:19:26 +0100 Subject: [PATCH 15/15] Added tests for new RandomString strings function Added unit test for the RandomString to TestUStringCatSnippets unit. Regenerated UStringCatSnippets using CodeSnip to contain the RandomString function. --- tests/Cat-String/TestUStringCatSnippets.pas | 51 +++++- tests/Cat-String/UStringCatSnippets.pas | 173 ++++++++++++++------ 2 files changed, 177 insertions(+), 47 deletions(-) diff --git a/tests/Cat-String/TestUStringCatSnippets.pas b/tests/Cat-String/TestUStringCatSnippets.pas index b814a84..4deb37e 100644 --- a/tests/Cat-String/TestUStringCatSnippets.pas +++ b/tests/Cat-String/TestUStringCatSnippets.pas @@ -8,6 +8,7 @@ interface type TestStringCatSnippets = class(TTestCase) private + procedure RandomString_Exception; published procedure TestStripAccelChars; procedure TestReverseStr; @@ -15,12 +16,13 @@ TestStringCatSnippets = class(TTestCase) procedure TestIsNumeric; procedure TestSplitString; procedure TestParseStr; + procedure TestRandomString; end; implementation uses - Classes; + SysUtils, Classes; function IsEqualStringList(const Expected, Actual: TStrings): Boolean; var @@ -37,6 +39,18 @@ function IsEqualStringList(const Expected, Actual: TStrings): Boolean; { TestStringCatSnippets } +procedure TestStringCatSnippets.RandomString_Exception; +var + SL: TStrings; +begin + SL := TStringList.Create; + try + RandomString(SL); + finally + SL.Free; + end; +end; + procedure TestStringCatSnippets.TestIsNumeric; begin CheckTrue(IsNumeric('123', False, False), 'Test 1'); @@ -133,6 +147,41 @@ procedure TestStringCatSnippets.TestParseStr; end; end; +procedure TestStringCatSnippets.TestRandomString; +var + SL5, SL1: TStrings; + S: string; + Idx, I: Integer; +begin + SL1 := nil; + SL5 := nil; + try + SL5 := TStringList.Create; + SL5.Add('one'); + SL5.Add('two'); + SL5.Add('three'); + SL5.Add('four'); + SL5.Add('five'); + for I := 1 to 5 do + begin + S := RandomString(SL5); + Idx := SL5.IndexOf(S); + CheckTrue(Idx >= 0, Format('SL5.%d', [I])); + SL5.Delete(Idx); + end; + + SL1 := TStringList.Create; + SL1.Add('only'); + S := RandomString(SL1); + CheckEquals('only', S, '#2'); + finally + SL5.Free; + SL1.Free; + end; + + CheckException(RandomString_Exception, EArgumentException, 'Exception'); +end; + procedure TestStringCatSnippets.TestReverseStr; begin CheckEquals('derf', ReverseStr('fred'), 'Test 1'); diff --git a/tests/Cat-String/UStringCatSnippets.pas b/tests/Cat-String/UStringCatSnippets.pas index f4de3eb..7afd6c0 100644 --- a/tests/Cat-String/UStringCatSnippets.pas +++ b/tests/Cat-String/UStringCatSnippets.pas @@ -1,14 +1,25 @@ { - * This file was generated from the DelphiDabbler Code Snippets collection. - * - * See https://github.com/delphidabsbler/code-snippets/tree/master/LICENSE.md for - * full license & copyright information. + * This unit was generated automatically. It incorporates a selection of source + * code taken from the Code Snippets Database at + * https://github.com/delphidabbler/code-snippets. + * + * The unit is copyright 2005-2025 by Peter Johnson & Contributors and is + * licensed under the MIT License (https://opensource.org/licenses/MIT). + * + * Generated on : Sat, 05 Apr 2025 12:14:56 GMT. + * Generated by : DelphiDabbler CodeSnip Release 4.24.0. + * + * The latest version of CodeSnip is available from the CodeSnip GitHub project + * at https://github.com/delphidabbler/codesnip. } unit UStringCatSnippets; {$IFNDEF FPC} {$IFDEF CONDITIONALEXPRESSIONS} + {$IF CompilerVersion >= 24.00} + {$LEGACYIFEND ON} + {$IFEND} {$IF CompilerVersion >= 14.00} {$WARN SYMBOL_PLATFORM OFF} {$WARN SYMBOL_DEPRECATED OFF} @@ -29,31 +40,116 @@ interface uses - SysUtils, StrUtils, Classes, Windows; + SysUtils, Classes, StrUtils; -function StripAccelChars(const S: string): string; +{ + Checks if the string Value contains a valid numeric value and returns True if + so or False if not. + If AllowFloat is true then Value may contain a floating point number, + otherwise it must be an integer. If TrimWhiteSpace is True any white space + surrounding Value is trimmed before testing. +} +function IsNumeric(Value: string; const AllowFloat: Boolean; + const TrimWhiteSpace: Boolean = True): Boolean; + +{ + Splits the string StrToParse into segments separated by Delimiter and stores + each segment in turn in string list Words, replacing any existing content. +} +procedure ParseStr(const StrToParse: string; const Delimiter: Char; + const Words: Classes.TStringList); +{ + Returns a random string from the given non-empty string list. + An EArgumentException exception is raised if the string list is empty. +} +function RandomString(const SL: Classes.TStrings): string; overload; + +{ + Returns the reverse of the given string. +} function ReverseStr(S: string): string; +{ + Returns the reverse of the given string. +} function ReverseStrR(const S: string): string; -function IsNumeric(Value: string; const AllowFloat: Boolean; - const TrimWhiteSpace: Boolean = True): Boolean; - +{ + Splits the string AText into segments separated by Delimiter and creates and + returns a string list containing the segments. + The caller is responsible for freeing the returnd string list object. +} function SplitString(const AText, ADelimiter: string): Classes.TStringList; -procedure ParseStr(const StrToParse: string; const Delimiter: Char; - const Words: Classes.TStringList); +{ + Strips all accelerator ('&') characters from the given string and returns the + resulting string. +} +function StripAccelChars(const S: string): string; implementation -function StripAccelChars(const S: string): string; +{ + Checks if the string Value contains a valid numeric value and returns True if + so or False if not. + If AllowFloat is true then Value may contain a floating point number, + otherwise it must be an integer. If TrimWhiteSpace is True any white space + surrounding Value is trimmed before testing. +} +function IsNumeric(Value: string; const AllowFloat: Boolean; + const TrimWhiteSpace: Boolean = True): Boolean; +var + ValueInt: Int64; // dummy integer value + ValueFloat: Extended; // dummy float value begin - Result := SysUtils.StringReplace( - S, '&', SysUtils.EmptyStr, [SysUtils.rfReplaceAll] - ); + if TrimWhiteSpace then + Value := SysUtils.Trim(Value); + // Check for valid integer + Result := SysUtils.TryStrToInt64(Value, ValueInt); + if not Result and AllowFloat then + // Wasn't valid as integer, try float + Result := SysUtils.TryStrToFloat(Value, ValueFloat); +end; + +{ + Splits the string StrToParse into segments separated by Delimiter and stores + each segment in turn in string list Words, replacing any existing content. +} +procedure ParseStr(const StrToParse: string; const Delimiter: Char; + const Words: Classes.TStringList); +var + TmpInStr: string; +begin + TmpInStr := StrToParse; + Words.Clear; + if Length(TmpInStr) > 0 then + begin + while Pos(Delimiter, TmpInStr) > 0 do + begin + Words.Append(Copy(TmpInStr, 1, Pos(Delimiter, TmpInStr) - 1)); + Delete(TmpInStr, 1, Pos(Delimiter, TmpInStr)); + end; + Words.Append(TmpInStr); + end; end; +{ + Returns a random string from the given non-empty string list. + An EArgumentException exception is raised if the string list is empty. +} +function RandomString(const SL: Classes.TStrings): string; overload; +begin + if SL.Count = 0 then + raise SysUtils.EArgumentException.Create( + 'RandomString called with empty string list' + ); + Result := SL[Random(SL.Count)]; +end; + +{ + Returns the reverse of the given string. +} function ReverseStr(S: string): string; begin Result := SysUtils.EmptyStr; @@ -64,6 +160,9 @@ function ReverseStr(S: string): string; end; end; +{ + Returns the reverse of the given string. +} function ReverseStrR(const S: string): string; begin if SysUtils.AnsiSameText(S, SysUtils.EmptyStr) or (System.Length(S) = 1) then @@ -73,21 +172,11 @@ function ReverseStrR(const S: string): string; + ReverseStrR(StrUtils.LeftStr(S, System.Length(S) - 1)) end; -function IsNumeric(Value: string; const AllowFloat: Boolean; - const TrimWhiteSpace: Boolean = True): Boolean; -var - ValueInt: Int64; // dummy integer value - ValueFloat: Extended; // dummy float value -begin - if TrimWhiteSpace then - Value := SysUtils.Trim(Value); - // Check for valid integer - Result := SysUtils.TryStrToInt64(Value, ValueInt); - if not Result and AllowFloat then - // Wasn't valid as integer, try float - Result := SysUtils.TryStrToFloat(Value, ValueFloat); -end; - +{ + Splits the string AText into segments separated by Delimiter and creates and + returns a string list containing the segments. + The caller is responsible for freeing the returnd string list object. +} function SplitString(const AText, ADelimiter: string): Classes.TStringList; var LTxt, LTmp: string; @@ -104,23 +193,15 @@ function SplitString(const AText, ADelimiter: string): Classes.TStringList; Result.Add(LTxt); end; -procedure ParseStr(const StrToParse: string; const Delimiter: Char; - const Words: Classes.TStringList); -var - TmpInStr: string; +{ + Strips all accelerator ('&') characters from the given string and returns the + resulting string. +} +function StripAccelChars(const S: string): string; begin - TmpInStr := StrToParse; - Words.Clear; - if Length(TmpInStr) > 0 then - begin - while Pos(Delimiter, TmpInStr) > 0 do - begin - Words.Append(Copy(TmpInStr, 1, Pos(Delimiter, TmpInStr) - 1)); - Delete(TmpInStr, 1, Pos(Delimiter, TmpInStr)); - end; - Words.Append(TmpInStr); - end; + Result := SysUtils.StringReplace( + S, '&', SysUtils.EmptyStr, [SysUtils.rfReplaceAll] + ); end; end. -