Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src/FSharpx.Collections/NonEmptyList.fs
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,83 @@ module NonEmptyList =
[<CompiledName("MinBy")>]
let minBy projection list =
List.minBy projection list.List

/// O(n). Returns the largest element of the non-empty list.
[<CompiledName("Max")>]
let max list =
List.max list.List

/// O(n). Returns the smallest element of the non-empty list.
[<CompiledName("Min")>]
let min list =
List.min list.List

/// O(n). Applies a function to each element of the collection, threading an accumulator argument.
[<CompiledName("Fold")>]
let fold (folder: 'State -> 'T -> 'State) (state: 'State) (list: NonEmptyList<'T>) =
List.fold folder state list.List

/// O(n). Applies a function to each element of the collection from right to left, threading an accumulator argument.
[<CompiledName("FoldBack")>]
let foldBack (folder: 'T -> 'State -> 'State) (list: NonEmptyList<'T>) (state: 'State) =
List.foldBack folder list.List state

/// O(n), worst case. Returns the first element for which the given function returns <c>Some</c>.
[<CompiledName("TryFind")>]
let tryFind (predicate: 'T -> bool) (list: NonEmptyList<'T>) =
List.tryFind predicate list.List

/// O(n), worst case. Returns the first element for which the given function returns <c>true</c>.
/// Raises <c>KeyNotFoundException</c> if no such element exists.
[<CompiledName("Find")>]
let find (predicate: 'T -> bool) (list: NonEmptyList<'T>) =
List.find predicate list.List

/// O(n). Returns a new list containing only the elements for which the given predicate returns <c>true</c>.
/// The result may be empty, so a plain <c>'T list</c> is returned.
[<CompiledName("Filter")>]
let filter (predicate: 'T -> bool) (list: NonEmptyList<'T>) : 'T list =
List.filter predicate list.List

/// O(n). Applies the given function to each element and returns a list of the values returned by
/// the function where the function returned <c>Some</c>. The result may be empty.
[<CompiledName("Choose")>]
let choose (mapping: 'T -> 'U option) (list: NonEmptyList<'T>) : 'U list =
List.choose mapping list.List

/// O(n). Splits the collection into two lists; the first containing elements for which the given
/// predicate returns <c>true</c>, the second for which it returns <c>false</c>. Both parts may be empty.
[<CompiledName("Partition")>]
let partition (predicate: 'T -> bool) (list: NonEmptyList<'T>) : 'T list * 'T list =
List.partition predicate list.List

/// O(n). Returns a NonEmptyList of each element paired with its index.
[<CompiledName("Indexed")>]
let indexed(list: NonEmptyList<'T>) : NonEmptyList<int * 'T> =
{ List = List.indexed list.List }

/// O(n). Splits a NonEmptyList of pairs into a pair of NonEmptyLists.
[<CompiledName("Unzip")>]
let unzip(list: NonEmptyList<'T1 * 'T2>) : NonEmptyList<'T1> * NonEmptyList<'T2> =
let a, b = List.unzip list.List
{ List = a }, { List = b }

/// O(n). Returns a list of each element and its successor. The result may be empty for a singleton list.
[<CompiledName("Pairwise")>]
let pairwise(list: NonEmptyList<'T>) : ('T * 'T) list =
List.pairwise list.List

/// O(n). Returns a NonEmptyList of states by threading an accumulator through the list.
/// The result always contains at least the initial state followed by the intermediate states.
[<CompiledName("Scan")>]
let scan (folder: 'State -> 'T -> 'State) (state: 'State) (list: NonEmptyList<'T>) : NonEmptyList<'State> =
{ List = List.scan folder state list.List }

/// O(n). Applies a key-generating function to each element and yields a NonEmptyList of
/// unique keys together with a NonEmptyList of all elements that match each key.
[<CompiledName("GroupBy")>]
let groupBy (projection: 'T -> 'Key) (list: NonEmptyList<'T>) : NonEmptyList<'Key * NonEmptyList<'T>> =
{ List =
list.List
|> List.groupBy projection
|> List.map(fun (k, vs) -> k, { List = vs }) }
173 changes: 172 additions & 1 deletion tests/FSharpx.Collections.Tests/NonEmptyListTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -402,4 +402,175 @@ module NonEmptyListTests =
config10k
"minBy with negation projection returns maximum element"
(Prop.forAll(neListOfInt())
<| fun nel -> NonEmptyList.minBy (fun x -> -x) nel = (nel |> NonEmptyList.toList |> List.max)) ]
<| fun nel -> NonEmptyList.minBy (fun x -> -x) nel = (nel |> NonEmptyList.toList |> List.max))

testPropertyWithConfig
config10k
"max returns maximum element"
(Prop.forAll(neListOfInt())
<| fun nel -> NonEmptyList.max nel = (nel |> NonEmptyList.toList |> List.max))

testPropertyWithConfig
config10k
"min returns minimum element"
(Prop.forAll(neListOfInt())
<| fun nel -> NonEmptyList.min nel = (nel |> NonEmptyList.toList |> List.min))

testPropertyWithConfig
config10k
"fold behaves like List.fold"
(Prop.forAll(neListOfInt())
<| fun nel ->
let actual = NonEmptyList.fold (+) 0 nel
let expected = nel |> NonEmptyList.toList |> List.fold (+) 0
actual = expected)

testPropertyWithConfig
config10k
"foldBack behaves like List.foldBack"
(Prop.forAll(neListOfInt())
<| fun nel ->
let actual = NonEmptyList.foldBack (fun x acc -> x - acc) nel 0

let expected =
nel |> NonEmptyList.toList |> List.foldBack(fun x acc -> x - acc) <| 0

actual = expected)

testPropertyWithConfig
config10k
"tryFind returns Some when element exists"
(Prop.forAll(neListOfInt())
<| fun nel ->
let list = NonEmptyList.toList nel
let predicate x = x % 2 = 0
NonEmptyList.tryFind predicate nel = List.tryFind predicate list)

testPropertyWithConfig config10k "find returns element when it exists"
<| fun (xs: int list) ->
if xs.IsEmpty then
true
else
let nel = NonEmptyList.create xs.Head xs.Tail
let target = xs.Head

NonEmptyList.find (fun x -> x = target) nel = List.find (fun x -> x = target) xs

testPropertyWithConfig config10k "find raises KeyNotFoundException when element not found"
<| fun (xs: int list) ->
if xs.IsEmpty then
true
else
let nel = NonEmptyList.create xs.Head xs.Tail
let sentinel = System.Int32.MinValue

if List.contains sentinel xs then
true
else
Expect.throwsT<System.Collections.Generic.KeyNotFoundException> "should raise" (fun () ->
NonEmptyList.find (fun x -> x = sentinel) nel |> ignore)

true

testPropertyWithConfig
config10k
"filter returns subset as plain list"
(Prop.forAll(neListOfInt())
<| fun nel ->
let predicate x = x % 2 = 0
let actual = NonEmptyList.filter predicate nel
let expected = nel |> NonEmptyList.toList |> List.filter predicate
actual = expected)

testPropertyWithConfig
config10k
"choose returns mapped subset as plain list"
(Prop.forAll(neListOfInt())
<| fun nel ->
let mapping x =
if x % 2 = 0 then Some(x * 2) else None

let actual = NonEmptyList.choose mapping nel
let expected = nel |> NonEmptyList.toList |> List.choose mapping
actual = expected)

testPropertyWithConfig
config10k
"partition splits into two plain lists"
(Prop.forAll(neListOfInt())
<| fun nel ->
let predicate x = x % 2 = 0
let trueList, falseList = NonEmptyList.partition predicate nel

let expectedTrue, expectedFalse =
nel |> NonEmptyList.toList |> List.partition predicate

trueList = expectedTrue && falseList = expectedFalse)

testPropertyWithConfig
config10k
"indexed pairs each element with its index"
(Prop.forAll(NonEmptyListGen.NonEmptyList())
<| fun nel ->
let actual = NonEmptyList.indexed nel |> NonEmptyList.toList
let expected = nel |> NonEmptyList.toList |> List.indexed
actual = expected)

testPropertyWithConfig
config10k
"unzip splits into two NonEmptyLists"
(Prop.forAll(NonEmptyListGen.NonEmptyList())
<| fun nel ->
let pairs = NonEmptyList.map (fun x -> x, x) nel
let a, b = NonEmptyList.unzip pairs

NonEmptyList.toList a = NonEmptyList.toList nel
&& NonEmptyList.toList b = NonEmptyList.toList nel)

testPropertyWithConfig
config10k
"pairwise returns adjacent pairs as plain list"
(Prop.forAll(neListOfInt())
<| fun nel ->
let actual = NonEmptyList.pairwise nel
let expected = nel |> NonEmptyList.toList |> List.pairwise
actual = expected)

testPropertyWithConfig
config10k
"scan produces intermediate accumulator states"
(Prop.forAll(neListOfInt())
<| fun nel ->
let actual = NonEmptyList.scan (+) 0 nel |> NonEmptyList.toList
let expected = nel |> NonEmptyList.toList |> List.scan (+) 0
actual = expected)

testPropertyWithConfig
config10k
"scan result is always non-empty"
(Prop.forAll(NonEmptyListGen.NonEmptyList())
<| fun nel -> NonEmptyList.scan (fun _ _ -> 0) 0 nel |> NonEmptyList.length > 0)

testPropertyWithConfig
config10k
"groupBy groups elements by key"
(Prop.forAll(neListOfInt())
<| fun nel ->
let projection x = x % 3
let groups = NonEmptyList.groupBy projection nel

// verify all original elements appear in exactly one group
let flattened =
groups
|> NonEmptyList.toList
|> List.collect(fun (_, vs) -> NonEmptyList.toList vs)
|> List.sort

let expected = nel |> NonEmptyList.toList |> List.sort
flattened = expected)

testPropertyWithConfig
config10k
"groupBy result is always non-empty"
(Prop.forAll(NonEmptyListGen.NonEmptyList())
<| fun nel -> NonEmptyList.groupBy id nel |> NonEmptyList.length > 0) ]
Loading