From b3e41bc551589157e292295e7d8f64f0361d4f4a Mon Sep 17 00:00:00 2001 From: omaus Date: Sun, 26 Mar 2023 18:12:41 +0200 Subject: [PATCH 1/6] Add `Seq.map4` + internal `checkNonNull` --- src/FSharpAux/Seq.fs | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/src/FSharpAux/Seq.fs b/src/FSharpAux/Seq.fs index de72694..6b5d818 100644 --- a/src/FSharpAux/Seq.fs +++ b/src/FSharpAux/Seq.fs @@ -3,13 +3,17 @@ open System.Collections.Generic [] -module Seq = - - ///Adds a value to the back of a sequence. +module Seq = + + let inline internal checkNonNull argName arg = + if isNull arg then + nullArg argName + + /// Adds a value to the back of a sequence. let appendSingleton (s : seq<'T>) (value : 'T) = Seq.append s (Seq.singleton value) - ///Adds a value to the front of a sequence. + /// Adds a value to the front of a sequence. let consSingleton (s : seq<'T>) (value : 'T) = Seq.append (Seq.singleton value) s @@ -222,17 +226,42 @@ module Seq = - - + /// Returns head of a seq as option or None if seq is empty. let tryHead s = Seq.tryPick Some s - + /// Returns head of a seq or default value if seq is empty. let headOrDefault defaultValue s = match (tryHead s) with | Some x -> x | None -> defaultValue + /// + /// Builds a new collection whose elements are the results of applying the given function + /// to the corresponding quadruples of elements from the four sequences. If one input sequence if shorter than + /// the others then the remaining elements of the longer sequences are ignored. + /// + /// The function to transform quadruples of elements from the input sequences. + /// The first input sequence. + /// The second input sequence. + /// The third input sequence. + /// The fourth input sequence. + /// The result sequence. + /// Thrown when any of the input sequences is null. + let map4 (mapping : 'T -> 'T -> 'T -> 'T -> 'U) (source1 : seq<'T>) (source2 : seq<'T>) (source3 : seq<'T>) (source4 : seq<'T>) = + checkNonNull "source1" source1 + checkNonNull "source2" source2 + checkNonNull "source3" source3 + checkNonNull "source4" source4 + let e1 = source1.GetEnumerator() + let e2 = source2.GetEnumerator() + let e3 = source3.GetEnumerator() + let e4 = source4.GetEnumerator() + seq { + while e1.MoveNext() && e2.MoveNext() && e3.MoveNext() && e4.MoveNext() do + yield mapping e1.Current e2.Current e3.Current e4.Current + } + /// Splits a sequence of pairs into two sequences. let unzip (input : seq<_>) = let (lstA, lstB) = From 00983068b3425618b30815314e90a685d42741fd Mon Sep 17 00:00:00 2001 From: omaus Date: Sun, 26 Mar 2023 18:12:49 +0200 Subject: [PATCH 2/6] Add unit tests for `Seq.map4` --- tests/FSharpAux.Tests/SeqTests.fs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/FSharpAux.Tests/SeqTests.fs b/tests/FSharpAux.Tests/SeqTests.fs index d048bd1..2f79ac6 100644 --- a/tests/FSharpAux.Tests/SeqTests.fs +++ b/tests/FSharpAux.Tests/SeqTests.fs @@ -18,6 +18,8 @@ let testSeq4_groupWhen_Equal = seq {seq {3}; seq {3; 2; 4; 2; 2}} let testSeq4_groupWhen_NotEqual = seq {seq {3}; seq {3; 2; 4; 2}; seq {2}} let testSeq5_groupWhen_Equal = seq {seq {3}; seq {3; 2; 4; 2}; seq {1}} let testSeq5_groupWhen_NotEqual = seq {seq {3}; seq {3; 2; 4; 2; 1}} +let testSeq_map4_1 = seq {(3, 3, 3, 3); (3, 3, 3, 3); (2, 2, 2, 2); (4, 4, 4, 4); (1, 1, 2, 2); (2, 1, 2, 1)} +let testSeq_map4_2 = seq {(3, 3, 3, 1337); (3, 3, 3, 14); (2, 2, 2, 23); (4, 4, 4, 23); (1, 1, 2, 69); (2, 1, 2, 1)} // helper functions let list s = Seq.toList s @@ -26,6 +28,16 @@ let list2 s = Seq.map (Seq.toList) s |> Seq.toList [] let seqTests = testList "SeqTests" [ + testList "Seq.map4" [ + testCase "throws when any seq is null" <| fun _ -> + Expect.throws (fun _ -> Seq.map4 (fun _ _ _ _ -> ()) testSeq2 testSeq2 null testSeq2 |> ignore) "Seq.map4 did not throw when any seq is null" + testCase "maps correctly" <| fun _ -> + let res = Seq.map4 (fun a b c d -> a, b, c, d) testSeq2 testSeq3 testSeq4 testSeq5 |> list + Expect.sequenceEqual res (list testSeq_map4_1) "Seq.map4 did not map seqs correctly" + testCase "maps correctly with unequal seq lengths" <| fun _ -> + let res = Seq.map4 (fun a b c d -> a, b, c, d) testSeq2 testSeq3 testSeq4 testSeq1 |> list + Expect.sequenceEqual res (list testSeq_map4_2) "Seq.map4 did not map correctly when seqs have unequal lengths" + ] let isOdd = fun n -> n % 2 <> 0 testList "Seq.groupWhen" [ testCase "returns correct jagged list, case1: [3; 3; 2; 4; 1; 2]" (fun _ -> From 85685884c59a54e3c2eb92bdbf3eec314beb3a3a Mon Sep 17 00:00:00 2001 From: omaus Date: Sun, 26 Mar 2023 18:34:34 +0200 Subject: [PATCH 3/6] Add `List.map4` --- src/FSharpAux/List.fs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/FSharpAux/List.fs b/src/FSharpAux/List.fs index 104aa36..04da638 100644 --- a/src/FSharpAux/List.fs +++ b/src/FSharpAux/List.fs @@ -4,7 +4,27 @@ open Microsoft.FSharp.Core.OptimizedClosures [] module List = - + + /// + /// Builds a new collection whose elements are the results of applying the given function + /// to the corresponding elements of the four collections simultaneously. + /// + /// The function to transform quadruples of elements from the input lists. + /// The first input list. + /// The second input list. + /// The third input list. + /// The fourth input list. + /// The list of transformed elements. + let map4 (mapping : 'T -> 'T -> 'T -> 'T -> 'U) (list1 : 'T list) (list2 : 'T list) (list3 : 'T list) (list4 : 'T list) = + if list1.Length <> list2.Length || list1.Length <> list3.Length || list1.Length <> list4.Length then + failwithf "The input lists have different lengths.\n\tlist1.Length = %i; list2.Length = %i; list3.Length = %i; list4.Length = %i" list1.Length list2.Length list3.Length list4.Length + let rec loop acc nl1 nl2 nl3 nl4 = + match nl1, nl2, nl3, nl4 with + | h1 :: t1, h2 :: t2, h3 :: t3, h4 :: t4 -> + loop (mapping h1 h2 h3 h4 :: acc) t1 t2 t3 t4 + | _ -> List.rev acc + loop [] list1 list2 list3 list4 + /// Applies a function to each element of the list, threading an accumulator argument through the computation. If the input function is f and the elements are i0...iN then computes f (... (f i0 i1)...) iN and returns the intermediary and final results. Raises ArgumentException if the list has size zero. let scanReduce f l = match l with From d031205ef8eeca6f502f98325e40a8a7a3947026 Mon Sep 17 00:00:00 2001 From: omaus Date: Sun, 26 Mar 2023 18:34:41 +0200 Subject: [PATCH 4/6] Add unit tests for `List.map4` --- tests/FSharpAux.Tests/ListTests.fs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/FSharpAux.Tests/ListTests.fs b/tests/FSharpAux.Tests/ListTests.fs index a0ed0d0..4153647 100644 --- a/tests/FSharpAux.Tests/ListTests.fs +++ b/tests/FSharpAux.Tests/ListTests.fs @@ -32,10 +32,18 @@ let testList4_groupWhen_Equal = [[3]; [3; 2; 4; 2; 2]] let testList4_groupWhen_NotEqual = [[3]; [3; 2; 4; 2]; [2]] let testList5_groupWhen_Equal = [[3]; [3; 2; 4; 2]; [1]] let testList5_groupWhen_NotEqual = [[3]; [3; 2; 4; 2; 1]] +let testList_map4 = [(3, 3, 3, 3); (3, 3, 3, 3); (2, 2, 2, 2); (4, 4, 4, 4); (1, 1, 2, 2); (2, 1, 2, 1)] [] let listTests = testList "ListTests" [ + testList "List.map4" [ + testCase "throws when lists have different lengths" <| fun _ -> + Expect.throws (fun _ -> List.map4 (fun _ _ _ _ -> ()) [1] [2] [3; 3] [4] |> ignore) "List.map4 did not throw when lists had different lengths" + testCase "maps correctly" <| fun _ -> + let res = List.map4 (fun a b c d -> a, b, c, d) testList2 testList3 testList4 testList5 + Expect.sequenceEqual res testList_map4 "List.map4 did not map lists correctly" + ] testList "List.filteri" [ testCase "returns correct list" (fun _ -> Expect.equal (testList1 |> List.filteri (fun i t -> i < 5 && t < 100)) testList1_filteri_Equal "List.filteri did return correct List" From f35e6a05ab035ede110e2dc7e14a88abc859b510 Mon Sep 17 00:00:00 2001 From: omaus Date: Sun, 26 Mar 2023 18:57:39 +0200 Subject: [PATCH 5/6] Add `Array.map4` --- src/FSharpAux/Array.fs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/FSharpAux/Array.fs b/src/FSharpAux/Array.fs index c31eed0..a506934 100644 --- a/src/FSharpAux/Array.fs +++ b/src/FSharpAux/Array.fs @@ -24,7 +24,30 @@ module Array = // else // i <- i + 1 // found - + + /// + /// Builds a new collection whose elements are the results of applying the given function + /// to the corresponding quadruples from the four collections. The four input + /// arrays must have the same length, otherwise an ArgumentException is + /// raised. + /// + /// The function to transform the quadruples of the input elements. + /// The first input array. + /// The second input array. + /// The third input array. + /// The fourth input array. + /// Thrown when the input arrays differ in length. + /// Thrown when any of the input arrays is null. + /// The array of transformed elements. + let map4 (mapping : 'T -> 'T -> 'T -> 'T -> 'U) (array1 : 'T []) (array2 : 'T []) (array3 : 'T []) (array4 : 'T []) = + checkNonNull "array1" array1 + checkNonNull "array2" array2 + checkNonNull "array3" array3 + checkNonNull "array4" array4 + if array1.Length <> array2.Length || array1.Length <> array3.Length || array1.Length <> array4.Length then + failwithf "The input lists have different lengths.\n\tarray1.Length = %i; array2.Length = %i; array3.Length = %i; array4.Length = %i" array1.Length array2.Length array3.Length array4.Length + [|for i = 0 to array1.Length - 1 do yield mapping array1[i] array2[i] array3[i] array4[i]|] + /// Builds a new array that contains every element of the input array except for that on position index. let removeIndex index (arr : 'T []) = if index < arr.Length then From 26a75374a9975f326c933d41aa143384bdc5b29b Mon Sep 17 00:00:00 2001 From: omaus Date: Sun, 26 Mar 2023 18:57:54 +0200 Subject: [PATCH 6/6] Add unit tests for `Array.map4` --- tests/FSharpAux.Tests/ArrayTests.fs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/FSharpAux.Tests/ArrayTests.fs b/tests/FSharpAux.Tests/ArrayTests.fs index 37ed397..1952d1b 100644 --- a/tests/FSharpAux.Tests/ArrayTests.fs +++ b/tests/FSharpAux.Tests/ArrayTests.fs @@ -6,6 +6,8 @@ open Expecto let testArray1 = [|1337; 14; 23; 23; 69; 1; 2; 3; 1000; 9001; 23|] let testArray2 = [|3; 3; 2; 4; 2; 1|] let testArray3 = [|6; 6; 2; 4; 2; 8|] +let testArray4 = [|6; 6; 2; 4; 2; 9|] +let testArray5 = [|6; 6; 2; 4; 2; 5|] let testArray1_filteri_Equal = [|14; 23; 23; 69|] let testArray1_filteri_NotEqual = [|1337; 14; 23;|] @@ -21,10 +23,20 @@ let testArray1_skipNth_Equal = [|1337; 14; 23; 69; 2; 3; 9001; 23|] let testArray1_skipNth_NotEqual = [|5; 6; 7; 10|] let testArray1_groupWhen_Equal = [|[|1337; 14|]; [|23|]; [|23|]; [|69|]; [|1; 2|]; [|3; 1000|]; [|9001|]; [|23|]|] let testArray1_groupWhen_NotEqual = [|[|1337; 14|]; [|23|]; [|23|]; [|69|]; [|1; 2|]; [|3; 1000|]; [|9001; 23|]|] +let testArray_map4 = [|(3, 6, 6, 6); (3, 6, 6, 6); (2, 2, 2, 2); (4, 4, 4, 4); (2, 2, 2, 2); (1, 8, 9, 5)|] [] let arrayTests = testList "ArrayTests" [ + testList "Array.map4" [ + testCase "Throws when any array is null" <| fun _ -> + Expect.throws (fun _ -> Array.map4 (fun _ _ _ _ -> ()) testArray2 testArray3 null null |> ignore) "Array.map4 did not throw when an input array was null" + testCase "Throws when arrays have unequal lengths" <| fun _ -> + Expect.throws (fun _ -> Array.map4 (fun _ _ _ _ -> ()) [|1|] [||] [|3|] [|4|] |> ignore) "Array.map4 did not throw when input arrays have unequal length" + testCase "Maps correctly" <| fun _ -> + let res = Array.map4 (fun a b c d -> a, b, c, d) testArray2 testArray3 testArray4 testArray5 + Expect.sequenceEqual res testArray_map4 "Array.map4 did not map correctly" + ] testList "Array.filteri" [ testCase "returns correct array" (fun _ -> Expect.equal (testArray1 |> Array.filteri (fun i t -> i < 5 && t < 100)) testArray1_filteri_Equal "Array.filteri did return correct array"