Skip to content
Merged
94 changes: 94 additions & 0 deletions src/FSharpx.Collections/NonEmptyList.fs
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,97 @@ 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 predicate returns <c>true</c>.
/// Returns <c>Some x</c> if such an element exists, otherwise <c>None</c>.
[<CompiledName("TryFind")>]
let tryFind (predicate: 'T -> bool) (list: NonEmptyList<'T>) =
List.tryFind predicate list.List

/// O(n), worst case. Applies the given chooser to each element and returns the first result where
/// the chooser returns <c>Some</c>. Returns <c>None</c> if no element matches.
[<CompiledName("TryPick")>]
let tryPick (chooser: 'T -> 'U option) (list: NonEmptyList<'T>) : 'U option =
List.tryPick chooser list.List

/// O(n), worst case. Applies the given chooser to each element and returns the first result where
/// the chooser returns <c>Some</c>.
/// Raises <c>KeyNotFoundException</c> if no such element exists.
[<CompiledName("Pick")>]
let pick (chooser: 'T -> 'U option) (list: NonEmptyList<'T>) : 'U =
List.pick chooser list.List
Comment thread
gdziadkiewicz marked this conversation as resolved.

/// 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 }) }
206 changes: 205 additions & 1 deletion tests/FSharpx.Collections.Tests/NonEmptyListTests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -402,4 +402,208 @@ 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 behaves like List.tryFind"
(Prop.forAll(neListOfInt())
<| fun nel ->
let list = NonEmptyList.toList nel
let predicate x = x % 2 = 0
NonEmptyList.tryFind predicate nel = List.tryFind predicate list)
Comment thread
gdziadkiewicz marked this conversation as resolved.

testPropertyWithConfig
config10k
"tryPick behaves like List.tryPick"
(Prop.forAll(neListOfInt())
<| fun nel ->
let list = NonEmptyList.toList nel

let chooser x =
if x % 2 = 0 then Some(x * 2) else None

NonEmptyList.tryPick chooser nel = List.tryPick chooser list)

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

NonEmptyList.pick Some nel = List.pick Some xs

testPropertyWithConfig config10k "pick raises KeyNotFoundException when no element is picked"
<| fun (xs: int list) ->
if xs.IsEmpty then
true
else
let nel = NonEmptyList.create xs.Head xs.Tail

Expect.throwsT<System.Collections.Generic.KeyNotFoundException> "should raise" (fun () ->
NonEmptyList.pick (fun _ -> None) nel |> ignore)

true

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