diff --git a/.github/workflows/test-fpc.yml b/.github/workflows/test-fpc.yml new file mode 100644 index 0000000..5daedd5 --- /dev/null +++ b/.github/workflows/test-fpc.yml @@ -0,0 +1,24 @@ +name: Test FPC + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-22.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install prerequisites + run: | + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update + sudo apt-get install -y fpc-3.2.2 + - name: Build testsuite + run: | + cd Tests + fpc -B -MDelphi "Underscore.Delphi.Springless.Tests.dpr" + - name: Test + run: Tests/Underscore.Delphi.Springless.Tests -a --format=plain diff --git a/Tests/DUnitX.Stub.pas b/Tests/DUnitX.Stub.pas new file mode 100644 index 0000000..9d1981b --- /dev/null +++ b/Tests/DUnitX.Stub.pas @@ -0,0 +1,55 @@ +unit DUnitX.Stub; + +interface + +uses + fpcunit; + +type + Assert = class + public + class procedure IsTrue(const Value: Boolean); + class procedure IsFalse(const Value: Boolean); + class procedure AreEqual(const Expected, Value: Integer); overload; + class procedure AreEqual(const Expected, Value: string); overload; + class procedure Pass; + class procedure Fail; + end; + +implementation + +uses + testutils, + SysUtils; + +class procedure Assert.IsTrue(const Value: Boolean); +begin + TAssert.AssertTrue(Value); +end; + +class procedure Assert.IsFalse(const Value: Boolean); +begin + TAssert.AssertFalse(Value); +end; + +class procedure Assert.AreEqual(const Expected, Value: Integer); overload; +begin + TAssert.AssertEquals(Expected, Value); +end; + +class procedure Assert.AreEqual(const Expected, Value: string); overload; +begin + TAssert.AssertEquals(Expected, Value); +end; + +class procedure Assert.Pass; +begin + // do nothing +end; + +class procedure Assert.Fail; +begin + TAssert.Fail('Failed'); +end; + +end. diff --git a/Tests/Underscore.Delphi.Springless.Test.pas b/Tests/Underscore.Delphi.Springless.Test.pas new file mode 100644 index 0000000..036ffbb --- /dev/null +++ b/Tests/Underscore.Delphi.Springless.Test.pas @@ -0,0 +1,595 @@ +unit Underscore.Delphi.Springless.Test; + +interface + +{$ifdef FPC} +uses + fpcunit; +{$else} +uses + DUnitX.TestFramework; +{$endif} + +type +{$ifndef FPC} + [TestFixture] + TUnderscoreDelphiTest = class + public + [Test] + procedure MapEmptyList; + + [Test] + procedure Filter; + + [Test] + procedure MapIntToStringList; + + [Test] + procedure MapIntEnumerable; + + [Test] + procedure ReduceEmptyList; + + [Test] + procedure ReduceIntList; + + [Test] + procedure ReduceIntListToString; + + [Test] + procedure ReduceIntDictionary; + + [Test] + procedure Find; + + [Test] + procedure FindNothing; + + [Test] + procedure FindOrDefault; + + [Test] + procedure Join; + + [Test] + procedure Intersection; + + [Test] + procedure Difference; + + [Test] + procedure Union; + + [Test] + procedure EveryFalse; + + [Test] + procedure EveryTrue; + + [Test] + procedure EveryFalseTList; + + [Test] + procedure Min; + + [Test] + procedure Max; + + [Test] + procedure Uniq; + + [Test] + procedure MapDictionary; + end; +{$else} + TUnderscoreDelphiTest = class(TTestCase) + published + procedure MapEmptyList; + procedure Filter; + procedure MapIntToStringList; + procedure MapIntEnumerable; + procedure ReduceEmptyList; + procedure ReduceIntList; + procedure ReduceIntListToString; + procedure ReduceIntDictionary; + procedure Find; + procedure FindNothing; + procedure FindOrDefault; + procedure Join; + procedure Intersection; + procedure Difference; + procedure Union; + procedure EveryFalse; + procedure EveryTrue; + procedure EveryFalseTList; + procedure Min; + procedure Max; + procedure Uniq; + procedure MapDictionary; + end; + +type +{$endif} + + TMyRec = record + Id: Integer; + SomeValue: Integer; + + class function New(const A, B: Integer): TMyRec; static; + end; + +implementation + +{$ifdef FPC} +uses + Underscore.Delphi.Springless, + DUnitX.Stub, + testregistry, + Variants, + Generics.Collections, + SysUtils; +{$else} +uses + Underscore.Delphi.Springless, + Variants, + System.Generics.Collections, + System.SysUtils; +{$endif} + +type + TIntPair = TPair; + +{ Functions used as callbacks } + +function IsEven(const Value: Integer): Boolean; +begin + Result := Value mod 2 = 0; +end; + +function ConstIntToStr(const Value: Integer): string; +begin + Result := IntToStr(Value); +end; + +function FormatPair(const Item: TPair): string; +begin + Result := Format('%s = (%2d,%2d)', [Item.Key, Item.Value.Id, Item.Value.SomeValue]); +end; + +function AddAB(const A, B: Integer): Integer; +begin + Result := A + B; +end; + +function AddIntToCsvStr(const Current: string; const Item: Integer): string; +begin + if Current.IsEmpty then + Result := Item.ToString + else + Result := Current + ';' + Item.ToString; +end; + +function IntPairAdd(const Current, Item: TIntPair): TIntPair; +begin + Result.Value := Current.Value + Item.Value; +end; + +function IsThree(const Value: Integer): Boolean; +begin + Result := Value = 3; +end; + +function SomeValueOfRec(const Item: TMyRec): Integer; +begin + Result := Item.SomeValue; +end; + +{ TUnderscoreDelphiTest } + +procedure TUnderscoreDelphiTest.EveryFalse; +var + List: TList; +begin + List := TList.Create; + List.Add(2); + List.Add(4); + List.Add(5); + + Assert.IsFalse( + _.Every(List, IsEven) + ); +end; + +procedure TUnderscoreDelphiTest.EveryFalseTList; +var + List: TList; +begin + List := TList.Create; + List.Add(2); + List.Add(4); + List.Add(5); + + Assert.IsFalse( + _.Every(List, IsEven) + ); +end; + +procedure TUnderscoreDelphiTest.EveryTrue; +var + List: TList; +begin + List := TList.Create; + List.Add(2); + List.Add(4); + List.Add(6); + + Assert.IsTrue( + _.Every(List, IsEven) + ); +end; + +procedure TUnderscoreDelphiTest.Intersection; +var + ListOne: TList; + ListTwo: TList; + Intersected: TList; +begin + ListOne := TList.Create; + ListOne.Add(101); + ListOne.Add(2); + ListOne.Add(1); + ListOne.Add(10); + + ListTwo := TList.Create; + ListTwo.Add(2); + ListTwo.Add(1); + + Intersected := _.Intersection(ListOne, ListTwo); + + Assert.AreEqual(2, Intersected.Count); + Assert.IsTrue(Intersected.Contains(1)); + Assert.IsTrue(Intersected.Contains(2)); +end; + +procedure TUnderscoreDelphiTest.MapDictionary; +var + Dict: TDictionary; + Mapped: TList; +begin + Dict := TDictionary.Create; + Dict.AddOrSetValue('hello', TMyRec.New(1, 2)); + Dict.AddOrSetValue('world', TMyRec.New(2, 3)); + Dict.AddOrSetValue('etc 123', TMyRec.New(4, 5)); + + Mapped := _.Map(Dict, FormatPair); + + // the order is not guaranteed, so we sort before testing + Mapped.Sort; + + Assert.AreEqual('etc 123 = ( 4, 5)', Mapped[0]); + Assert.AreEqual('hello = ( 1, 2)', Mapped[1]); + Assert.AreEqual('world = ( 2, 3)', Mapped[2]); +end; + +procedure TUnderscoreDelphiTest.MapEmptyList; +var + ListOne: TList; + ListTwo: TList; +begin + ListOne := TList.Create; + + ListTwo := _.Map(ListOne, ConstIntToStr); + + Assert.AreEqual(ListTwo.Count, 0); +end; + +procedure TUnderscoreDelphiTest.Filter; +var + ListIn: TList; + ListOut: TList; +begin + ListIn := TList.Create; + ListIn.Add(101); + ListIn.Add(2); + ListIn.Add(1); + ListIn.Add(10); + + ListOut := _.Filter(ListIn, IsEven); + + Assert.AreEqual(2, ListOut.Count); + Assert.IsTrue(ListOut.Contains(2)); + Assert.IsTrue(ListOut.Contains(10)); +end; + +procedure TUnderscoreDelphiTest.MapIntToStringList; +var + ListOne: TList; + ListTwo: TList; +begin + ListOne := TList.Create; + ListOne.Add(1); + ListOne.Add(2); + + ListTwo := _.Map(ListOne, ConstIntToStr); + + Assert.AreEqual(ListTwo.Count, 2); + Assert.AreEqual(ListTwo[0], '1'); + Assert.AreEqual(ListTwo[1], '2'); +end; + +procedure TUnderscoreDelphiTest.MapIntEnumerable; +var + ListOne: TStack; + ListTwo: TList; +begin + ListOne := TStack.Create; + ListOne.Push(1); + ListOne.Push(2); + + ListTwo := _.Map(ListOne, ConstIntToStr); + + Assert.AreEqual(ListTwo.Count, 2); + Assert.AreEqual(ListTwo[0], '1'); + Assert.AreEqual(ListTwo[1], '2'); +end; + +procedure TUnderscoreDelphiTest.ReduceEmptyList; +var + ListOne: TList; + Value: Integer; +begin + ListOne := TList.Create; + + Value := _.Reduce(ListOne, AddAB, 0); + + Assert.AreEqual(Value, 0); +end; + +procedure TUnderscoreDelphiTest.ReduceIntList; +var + ListOne: TList; + Value: Integer; +begin + ListOne := TList.Create; + ListOne.Add(1); + ListOne.Add(3); + ListOne.Add(9); + + Value := _.Reduce(ListOne, AddAB, 0); + + Assert.AreEqual(Value, 13); +end; + +procedure TUnderscoreDelphiTest.ReduceIntListToString; +var + ListOne: TList; + Value: string; +begin + ListOne := TList.Create; + ListOne.Add(1); + ListOne.Add(3); + ListOne.Add(9); + + Value := _.Reduce(ListOne, + AddIntToCsvStr, + String.Empty); + + Assert.AreEqual(Value, '1;3;9'); +end; + +procedure TUnderscoreDelphiTest.ReduceIntDictionary; +var + ListOne: TDictionary; + Value: TPair; +begin + ListOne := TDictionary.Create; + ListOne.AddOrSetValue(0, 4); + ListOne.AddOrSetValue(1, 5); + ListOne.AddOrSetValue(2, 2); + + Value := _.Reduce(ListOne, + IntPairAdd, + TIntPair.Create(0, 0)); + + Assert.AreEqual(Value.Value, 11); +end; + +procedure TUnderscoreDelphiTest.Difference; +var + ListOne: TList; + ListTwo: TList; + ResultSet: TList; +begin + ListOne := TList.Create; + ListOne.Add(101); + ListOne.Add(2); + ListOne.Add(1); + ListOne.Add(10); + + ListTwo := TList.Create; + ListTwo.Add(3); + ListTwo.Add(2); + ListTwo.Add(1); + + ResultSet := _.Difference(ListOne, ListTwo); + + Assert.AreEqual(2, ResultSet.Count); + Assert.IsTrue(ResultSet.Contains(101)); + Assert.IsTrue(ResultSet.Contains(10)); +end; + +procedure TUnderscoreDelphiTest.Union; +var + ListOne: TList; + ListTwo: TList; + ResultSet: TList; +begin + ListOne := TList.Create; + ListOne.Add(101); + ListOne.Add(2); + ListOne.Add(1); + ListOne.Add(10); + + ListTwo := TList.Create; + ListTwo.Add(3); + ListTwo.Add(2); + ListTwo.Add(1); + + ResultSet := _.Union(ListOne, ListTwo); + + Assert.AreEqual(5, ResultSet.Count); + Assert.IsTrue(ResultSet.Contains(101)); + Assert.IsTrue(ResultSet.Contains(2)); + Assert.IsTrue(ResultSet.Contains(1)); + Assert.IsTrue(ResultSet.Contains(10)); + Assert.IsTrue(ResultSet.Contains(3)); +end; + +procedure TUnderscoreDelphiTest.Uniq; +var + List: TList; + OutList: TList; +begin + List := TList.Create; + List.Add(1); + List.Add(4); + List.Add(4); + List.Add(5); + + OutList := _.Uniq(List); + + Assert.AreEqual(3, OutList.Count); + Assert.AreEqual(1, OutList[0]); + Assert.AreEqual(4, OutList[1]); + Assert.AreEqual(5, OutList[2]); +end; + +procedure TUnderscoreDelphiTest.Find; +var + List: TList; + OutValue: Integer; +begin + List := TList.Create; + List.Add(1); + List.Add(4); + List.Add(5); + + OutValue := _.Find(List, IsEven); + + Assert.AreEqual(4, OutValue); +end; + +procedure TUnderscoreDelphiTest.FindNothing; +var + List: TList; +begin + List := TList.Create; + List.Add(1); + List.Add(3); + List.Add(5); + + try + _.Find(List, IsEven); + Assert.Fail; + except + on E: Exception do + begin + Assert.Pass; + end + end; +end; + +procedure TUnderscoreDelphiTest.FindOrDefault; +var + List: TList; +begin + List := TList.Create; + List.Add(1); + List.Add(3); + List.Add(5); + + Assert.AreEqual(3, + _.FindOrDefault(List, IsThree, -1)); + + Assert.AreEqual(-1, + _.FindOrDefault(List, IsEven, -1)); +end; + +procedure TUnderscoreDelphiTest.Join; +var + InList: TList; + OutValue: string; +begin + InList := TList.Create; + InList.Add(2); + InList.Add(6); + InList.Add(1); + + OutValue := _.Join(InList, ConstIntToStr, ';'); + + Assert.AreEqual('2;6;1', OutValue); +end; + +procedure TUnderscoreDelphiTest.Max; +var + List: TList; + OutValue: TMyRec; + A, B, C: TMyRec; +begin + A.Id := 1; + A.SomeValue := 2; + B.Id := 2; + B.SomeValue := 6; + C.Id := 3; + C.SomeValue := 1; + + List := TList.Create; + List.Add(A); + List.Add(B); + List.Add(C); + + OutValue := _.Max(List, SomeValueOfRec); + + Assert.AreEqual(B.Id, OutValue.Id); + Assert.AreEqual(B.SomeValue, OutValue.SomeValue); +end; + +procedure TUnderscoreDelphiTest.Min; +var + List: TList; + OutValue: TMyRec; + A, B, C: TMyRec; +begin + A.Id := 1; + A.SomeValue := 2; + B.Id := 2; + B.SomeValue := 6; + C.Id := 3; + C.SomeValue := 1; + + List := TList.Create; + List.Add(A); + List.Add(B); + List.Add(C); + + OutValue := _.Min(List, SomeValueOfRec); + + Assert.AreEqual(C.Id, OutValue.Id); + Assert.AreEqual(C.SomeValue, OutValue.SomeValue); +end; + +{ TMyRec } + +class function TMyRec.New(const A, B: Integer): TMyRec; +begin + Result.Id := A; + Result.SomeValue := B; +end; + +initialization +{$ifdef FPC} + RegisterTest(TUnderscoreDelphiTest); +{$else} + TDUnitX.RegisterTestFixture(TUnderscoreDelphiTest); +{$endif} +end. diff --git a/Tests/Underscore.Delphi.Springless.Tests.dpr b/Tests/Underscore.Delphi.Springless.Tests.dpr new file mode 100644 index 0000000..7e9c573 --- /dev/null +++ b/Tests/Underscore.Delphi.Springless.Tests.dpr @@ -0,0 +1,57 @@ +program Underscore.Delphi.Springless.Tests; + +{$APPTYPE CONSOLE} + +uses + SysUtils, +{$ifdef FPC} + consoletestrunner, + DUnitX.Stub in './DUnitX.Stub.pas', +{$else} + DUnitX.TestRunner, + DUnitX.TestFramework, + DUnitX.Loggers.Console, + DUnitX.Loggers.XML.NUnit, +{$endif} + Underscore.Delphi.Springless in '../Underscore.Delphi.Springless.pas', + Underscore.Delphi.Springless.Test; + +var +{$ifdef FPC} + App: TTestRunner; +{$else} + runner: ITestRunner; + results: IRunResults; + logger: ITestLogger; + xmlLogger: ITestLogger; +{$endif} + +begin +{$ifdef FPC} + App := TTestRunner.Create(nil); + App.Initialize; + App.Title := 'FPCUnit Console runner.'; + App.Run; + App.Free; +{$else} + try + runner := TDUnitX.CreateRunner; + logger := TDUnitXConsoleLogger.Create(true); + runner.AddLogger(logger); + + xmlLogger := TDUnitXXMLNUnitFileLogger.Create; + runner.AddLogger(xmlLogger); + + results := runner.Execute; + + if not results.AllPassed then + System.ExitCode := 1; + except + on E: Exception do + begin + System.Writeln(E.ClassName, ': ', E.Message); + System.ExitCode := 2; + end; + end; +{$endif} +end. diff --git a/Underscore.Delphi.Springless.pas b/Underscore.Delphi.Springless.pas index 387588a..9d6f6e2 100644 --- a/Underscore.Delphi.Springless.pas +++ b/Underscore.Delphi.Springless.pas @@ -3,28 +3,49 @@ interface uses +{$ifdef FPC} + Classes, + SysUtils, + Generics.Collections; +{$else} System.Classes, System.Generics.Collections, System.SysUtils; +{$endif} type +{$ifdef FPC} + _Func = function(const arg: T): TResult; + _Func = function(const arga: A; const argb: B): TResult; + _Predicate = function(const arg: T): Boolean; +{$else} _Func = reference to function(const arg: T): TResult; _Func = reference to function(const arga: A; const argb: B): TResult; _Predicate = reference to function(const arg: T): Boolean; +{$endif} _ = class public class function Map(const List: TList; const MapFunc: _Func): TList; overload; class function Map(const List: TEnumerable; const MapFunc: _Func): TList; overload; class function Map(const List: TDictionary; const MapFunc: _Func, S>): TList; overload; +{$ifndef FPC} class function Map(const List: TCollection; const MapFunc: _Func): TList; overload; +{$endif} class function Reduce(const List: TEnumerable; const ReduceFunc: _Func; const InitialValue: T): T; overload; class function Reduce(const List: TEnumerable; const ReduceFunc: _Func; const InitialValue: S): S; overload; class function Reduce(const List: IEnumerable; const ReduceFunc: _Func; const InitialValue: T): T; overload; class function Reduce(const List: IEnumerable; const ReduceFunc: _Func; const InitialValue: S): S; overload; + class function Reduce(const Dic: TDictionary; const ReduceFunc: _Func, T>; const InitialValue: T): T; overload; +{$ifndef FPC} class function Zip(const Lists: TList>): TList>; overload; +{$endif} + + class function Intersection(const A, B: TList): TList; + class function Difference(const A, B: TList): TList; + class function Union(const A, B: TList): TList; class function Every(const List: TList; const Predicate: _Predicate): Boolean; @@ -44,8 +65,10 @@ _ = class implementation +{$ifndef FPC} uses System.Threading; +{$endif} class function _.Map(const List: TList; const MapFunc: _Func): TList; var @@ -66,6 +89,7 @@ class function _.Map(const List: TEnumerable; const MapFunc: _Func(const List: TCollection; const MapFunc: _Func): TList; var Item: TCollectionItem; @@ -75,6 +99,7 @@ class function _.Map(const List: TCollection; const MapFunc: _Func): for Idx := 0 to List.Count - 1 do Result.Add(MapFunc(List.Items[Idx] as T)); end; +{$endif} class function _.Map(const List: TDictionary; const MapFunc: _Func, S>): TList; var @@ -181,6 +206,34 @@ class function _.Filter(const List: TList; const Predicate: _Predicate) end; end; +class function _.Intersection(const A, B: TList): TList; +var + Item: T; +begin + Result := TList.Create; + Result.Capacity := A.Count + B.Count; + + for Item in A do + begin + if B.Contains(Item) then + Result.Add(Item); + end; +end; + +class function _.Difference(const A, B: TList): TList; +var + Item: T; +begin + Result := TList.Create; + Result.Capacity := A.Count + B.Count; + + for Item in A do + begin + if not B.Contains(Item) then + Result.Add(Item); + end; +end; + class function _.Reduce(const List: TEnumerable; const ReduceFunc: _Func; const InitialValue: S): S; var Item: T; @@ -209,27 +262,60 @@ class function _.Reduce(const List: IEnumerable; const ReduceFunc: _Func(const List: TList): TList; +class function _.Reduce(const List: IEnumerable; const ReduceFunc: _Func; const InitialValue: S): S; +var + Item: T; +begin + Result := InitialValue; + + for Item in List do + Result := ReduceFunc(Result, Item); +end; + +class function _.Reduce(const Dic: TDictionary; const ReduceFunc: _Func, T>; const InitialValue: T): T; +var + Item: TPair; +begin + Result := InitialValue; + + for Item in Dic do + Result := ReduceFunc(Result, Item); +end; + +class function _.Union(const A, B: TList): TList; +var + Item: T; begin Result := TList.Create; - Result.Capacity := List.Count; - for var Item in List do + Result.Capacity := A.Count + B.Count; + + for Item in A do + begin + if not Result.Contains(Item) then + Result.Add(Item); + end; + + for Item in B do begin if not Result.Contains(Item) then Result.Add(Item); end; end; -class function _.Reduce(const List: IEnumerable; const ReduceFunc: _Func; const InitialValue: S): S; +class function _.Uniq(const List: TList): TList; var Item: T; begin - Result := InitialValue; - + Result := TList.Create; + Result.Capacity := List.Count; for Item in List do - Result := ReduceFunc(Result, Item); + begin + if not Result.Contains(Item) then + Result.Add(Item); + end; end; +{$ifndef FPC} class function _.Zip(const Lists: TList>): TList>; var List: TList; @@ -253,6 +339,7 @@ class function _.Zip(const Lists: TList>): TList>; end; end; end; +{$endif} class function _.Min(const List: TList; const ValueFunc: _Func): T; var