2

let's consider the following SORTED data:

[1; 2; 3; 5; 6; 20; 21; 22; 23]

and I would like to get:

[ [1; 2; 3]; [5; 6]; [20; 21; 22; 23] ]

what would be the best way to achieve this? (the list is at most 1000 entries).

Thomas
  • 10,933
  • 14
  • 65
  • 136
  • The requirement is unclear. By which criteria are you grouping the elements? – Fyodor Soikin Aug 28 '21 at 23:09
  • As an uninterrupted sequence of integers: with 1,2,3,5,6 I have the 1,2,3 sequence, 4 is missing, then 5,6. Source is sorted, no duplicates, missing numbers can be any, sequence length can be 1 or more. – Thomas Aug 28 '21 at 23:13

4 Answers4

4

Here's a version which is perhaps a bit more idiomatic and, to me at least, a bit more readable. It relies only on the standard function foldBack in the List module and avoids mutability entirely.

let group ls =
    (ls, [])
    ||> List.foldBack (fun l s ->
        match s with 
        | [] | [[]] -> [[l]]
        | (n::res)::ns ->
            if n = l+1
            then (l::n::res)::ns
            else [l]::(n::res)::ns)

It may also be refactored into a more general function

let chunkBy cond ls =
    (ls, [])
    ||> List.foldBack (fun l s ->
        match s with 
        | [] | [[]] -> [[l]]
        | (n::res)::ns ->
            if cond l n
            then (l::n::res)::ns
            else [l]::(n::res)::ns)

let group = chunkBy (fun prev next -> prev + 1 = next)

Notice that this leverages the cons operator :: which is only implemented on lists. The cons operator prepends an item to a list, which is why the function uses foldBack.

torbonde
  • 2,459
  • 1
  • 15
  • 17
1
let group ls =
    [
        let mutable last = 0
        let mutable current = []
        for x in ls do
            if x > (last + 1) then
                if not current.IsEmpty then
                    yield current |> List.rev
                current <- [x]
            else
                current <- x :: current
            last <- x
        if not current.IsEmpty then
            yield current |> List.rev
    ]

group [1; 2; 3; 5; 6; 20; 21; 22; 23]
val it : int list list = [[1; 2; 3]; [5; 6]; [20; 21; 22; 23]]
citykid
  • 9,916
  • 10
  • 55
  • 91
1

Here is an implementation which folds the input list into a new list, grouping as required.

let group l =
  let addNextInt (i : int) (l : List<List<int>>) =
    match l with
    | [] -> [[i]]
    | (x::xs)::tail ->
        if x - i = 1 then (i::x::xs)::tail
        else [i]::(x::xs)::tail
    | _ -> failwith "Unreachable"

  List.foldBack addNextInt l []

// Input: [], Output: []
// Input: [1; 2], Output: [[1; 2]]
// Input: [1; 3], Output: [[1]; [3]]
// Input: [1; 3; 5; 6], Output: [[1]; [3]; [5; 6]]
// Input: [1; 2; 3; 5; 6; 20; 21; 22; 23], Output: [[1; 2; 3]; [5; 6]; [20; 21; 22; 23]]
Roland Andrag
  • 437
  • 4
  • 9
0

So you're implying you want to group by the condition that the difference between two consecutive values should be 1. Or viewed the other way, to split when the difference is other than 1.

Examples for grouping lists by predicate can be found here and here. Considering an input sequence as opposed to a list, we might get some mileage out of the fact that we are able to consume it lazily.

let splitWhen condition (source : seq<'T>) : seq<'T[]> = seq {
    let stateref, acc = ref None, ResizeArray()
    use ie = source.GetEnumerator()
    while ie.MoveNext() do
        let current = ie.Current
        match !stateref with
        | Some previous when condition previous current ->
            yield acc.ToArray()
            acc.Clear()
        | _ -> ()
        acc.Add current
        stateref := Some current
    if acc.Count > 0 then
        yield acc.ToArray() }

[1; 2; 3; 5; 6; 20; 21; 22; 23]
|> splitWhen (fun x0 x1 -> x1 - x0 <> 1)
// val it : seq<int []> = seq [[|1; 2; 3|]; [|5; 6|]; [|20; 21; 22; 23|]]
kaefer
  • 5,491
  • 1
  • 15
  • 20