I’ve been trying an exercise from leetcode.com - https://leetcode.com/problems/summary-ranges/
Basically, given a list of integers (sorted and unique), produce a list of ranges.
e.g. [0; 1; 2; 4; 5; 7; 12; 13; 14; 15; 18; 20] → [“0 → 2”; “4 → 5”; “7”; “12 → 15”; “18”; “20”]

I’m already up to my fourth version of the code:

Recursion;

Recursion with shorter lists;

List.fold;

List.foldBack (removing extra list reversal).

But I’m wondering if there are more improvements/optimisations/tidy-ups that I can do.

Here’s the current code:

let describe range =
match range with
| [] -> failwith "empty range"
| [single] -> single.ToString()
| [lower ; upper] -> lower.ToString() + " -> " + upper.ToString()
| _ -> failwith "too many items"
let summaryRanges numbers =
(numbers, [])
||> List.foldBack (fun number ranges ->
match ranges, number with
| [], currentNumber -> [[currentNumber]]
| currentRange::restOfRanges, currentNumber ->
match currentRange with
| [single] when single = currentNumber + 1 -> [currentNumber ; single]::restOfRanges
| [_] -> [currentNumber]::currentRange::restOfRanges
| [lower ; upper] when lower = currentNumber + 1 -> [currentNumber ; upper]::restOfRanges
| [_ ; _] -> [currentNumber]::currentRange::restOfRanges
| _ -> failwith "too many items in the range"
)
|> List.map describe
[0; 1; 2; 4; 5; 7; 12; 13; 14; 15; 18; 20] |> summaryRanges

In particular, I’m wondering if there is anything I can do to try and lessen the code repetition, but any advice would be appreciated.

In F# you should think about the types involved. Think about what a range is and define a type to represent it. That will remove the failwiths and code repetition.

Types, yes, that’s the ticket, and something I should have been able to come up with myself; thank you.

I got so ‘hung up’ on using lists, and lists of lists, that I totally forgot that I could use some simple types to do it better.

Here’s the latest version, with some other changes:

type SummaryItem =
| Single of Value:int
| Range of Lower:int * Upper:int
let describe item =
match item with
| Single value -> value.ToString()
| Range (lower, upper) -> lower.ToString() + " -> " + upper.ToString()
let summaryRanges numbers =
(numbers, [])
||> List.foldBack (fun number items ->
match items with
| [] -> [Single (Value = number)]
| head::tail ->
let newHead =
match head with
| Single value when value = number + 1 -> [Range (Lower = number, Upper = value)]
| Range (lower, upper) when lower = number + 1 -> [Range (Lower = number, Upper = upper)]
| Single _
| Range (_, _) -> [Single (Value = number) ; head]
newHead @ tail
)
|> List.map describe
[0; 1; 2; 4; 5; 7; 12; 13; 14; 15; 18; 20] |> summaryRanges

If there are further improvements then I’d appreciate being told about them.

let rec finishRange lastVal = function
| x :: xs when x = lastVal + 1 -> finishRange x xs
| xs -> (lastVal, xs)
let rec ranges = function
| startOfRange :: xs ->
let (endOfRange, rest) = finishRange startOfRange xs
(startOfRange, endOfRange) :: ranges rest
| [] -> []
let describe = function
| (x, y) when x = y -> string x
| (x, y) -> sprintf "%i->%i" x y
let summarizeRanges xs =
let summaries = ranges xs |> List.map describe
printfn "%A" summaries

But I have no arguments why this implementation is better than your second implementation. It offers no improvements, just a different code style.
I don’t think there’s much to optimize here (it’s going to be O(n) pretty much no matter what). Once you get rid of the failwiths, I think the rest is going to just be subjective.