diff --git a/src/FSharpPlus/Control/Collection.fs b/src/FSharpPlus/Control/Collection.fs index 580e9b69f..00d4dfd7b 100644 --- a/src/FSharpPlus/Control/Collection.fs +++ b/src/FSharpPlus/Control/Collection.fs @@ -289,7 +289,11 @@ type GroupBy = type ChunkBy = static member ChunkBy (x: Id<'T> , f: 'T->'Key, _: Id<'Key*Id<'T>> , []_impl: ChunkBy) = let a = Id.run x in Id.create (f a, x) static member ChunkBy (x: seq<'T> , f: 'T->'Key, _: seq<'Key*seq<'T>> , []_impl: ChunkBy) = Seq.chunkBy f x |> Seq.map (fun (x,y) -> x, y :> _ seq) - static member ChunkBy (x: list<'T>, f: 'T->'Key, _: list<'Key*list<'T>>, []_impl: ChunkBy) = Seq.chunkBy f x |> Seq.map (fun (x,y) -> x, Seq.toList y) |> Seq.toList + static member ChunkBy (x: list<'T>, f: 'T->'Key, _: list<'Key*list<'T>>, []_impl: ChunkBy) = + #if TEST_TRACE + Traces.add "ChunkBy, list<'T>" + #endif + List.chunkBy f x static member ChunkBy (x: 'T [] , f: 'T->'Key, _: ('Key*('T [])) [] , []_impl: ChunkBy) = Seq.chunkBy f x |> Seq.map (fun (x,y) -> x, Seq.toArray y) |> Seq.toArray static member inline Invoke (projection: 'T->'Key) (source: '``Collection<'T>``) : '``Collection<'Key * 'Collection<'T>>`` = diff --git a/src/FSharpPlus/Extensions/List.fs b/src/FSharpPlus/Extensions/List.fs index b11cf15d6..e7feff616 100644 --- a/src/FSharpPlus/Extensions/List.fs +++ b/src/FSharpPlus/Extensions/List.fs @@ -340,6 +340,48 @@ module List = loop (ls,rs) loop (list1, list2) #endif + + /// + /// Chunks the list up into groups with the same projected key by applying + /// the key-generating projection function to each element and yielding a list of + /// keys tupled with values. + /// + /// + /// + /// Each key is tupled with an array of all adjacent elements that match + /// to the key, therefore keys are not unique but can't be adjacent + /// as each time the key changes a new group is yield. + /// + /// The ordering of the original list is respected. + /// + /// + /// A function that transforms an element of the list into a comparable key. + /// The input list. + /// + /// The resulting list of keys tupled with a list of matching values + let chunkBy (projection: 'T -> 'Key) (source: _ list) = + #if FABLE_COMPILER + Seq.chunkBy projection source |> Seq.map (fun (x, y) -> x, Seq.toList y) |> Seq.toList + #else + match source with + | [] -> [] + | x::xs -> + let mutable acc = new ListCollector<_> () + let mutable members = new ListCollector<_> () + let rec loop source g = + match source with + | [] -> acc.Add (g, members.Close ()) + | x::xs -> + let key = projection x + if g <> key then + acc.Add (g, members.Close ()) + members <- new ListCollector<_> () + members.Add x + loop xs key + members.Add x + loop xs (projection x) + acc.Close () + #endif /// Same as choose but with access to the index. /// The mapping function, taking index and element as parameters. diff --git a/tests/FSharpPlus.Tests/Collections.fs b/tests/FSharpPlus.Tests/Collections.fs new file mode 100644 index 000000000..1e5b3abcb --- /dev/null +++ b/tests/FSharpPlus.Tests/Collections.fs @@ -0,0 +1,24 @@ +namespace FSharpPlus.Tests + +open FSharpPlus +open FSharpPlus.Data +open NUnit.Framework + +#if TEST_TRACE +open FSharpPlus.Internals +#endif + +module Collections = + + [] + let chunkBy () = + #if TEST_TRACE + Traces.reset() + #endif + let source = [1; 2; 3; 5; 7; 9] + let expected = [(1, [1]); (0, [2]); (1, [3; 5; 7; 9])] + let actual = chunkBy (flip (%) 2) source + CollectionAssert.AreEqual(expected, actual) + #if TEST_TRACE + CollectionAssert.AreEqual (["ChunkBy, list<'T>"], Traces.get()) + #endif diff --git a/tests/FSharpPlus.Tests/FSharpPlus.Tests.fsproj b/tests/FSharpPlus.Tests/FSharpPlus.Tests.fsproj index 767e2ee10..4e1743d02 100644 --- a/tests/FSharpPlus.Tests/FSharpPlus.Tests.fsproj +++ b/tests/FSharpPlus.Tests/FSharpPlus.Tests.fsproj @@ -23,6 +23,7 @@ + diff --git a/tests/FSharpPlus.Tests/General.fs b/tests/FSharpPlus.Tests/General.fs index 7f26ffd7b..ee15954da 100644 --- a/tests/FSharpPlus.Tests/General.fs +++ b/tests/FSharpPlus.Tests/General.fs @@ -472,7 +472,7 @@ module Functor = let nel = zip (NonEmptyList.ofList [1; 2]) (NonEmptyList.ofList ["a"; "b"; "c"]) CollectionAssert.AreEqual (NonEmptyList.ofList [1,"a"; 2,"b"], nel) -module Collections = +module Collections2 = open System.Collections.Concurrent