diff --git a/lib/transit_data/glides_report/util.ex b/lib/transit_data/glides_report/util.ex index d27bca6..2fe93db 100644 --- a/lib/transit_data/glides_report/util.ex +++ b/lib/transit_data/glides_report/util.ex @@ -28,6 +28,61 @@ defmodule TransitData.GlidesReport.Util do |> String.pad_leading(count, "0") end + @doc """ + Combines any consecutive, overlapping ranges in an enumerable. + + All ranges in the enumerable must have a step of 1. + The returned list will be sorted. + + iex> ranges = [1..2, 3..8, 7..12, 7..10, 15..20] + iex> merge_ranges(ranges) + [1..2, 3..12, 15..20] + iex> merge_ranges(Enum.reverse(ranges)) == merge_ranges(ranges) + true + iex> merge_ranges(Enum.shuffle(ranges)) == merge_ranges(ranges) + true + + iex> merge_ranges([1..2//1, 2..0//-1]) + ** (ArgumentError) received range(s) with step != 1: [2..0//-1] + + iex> merge_ranges([1..5//2, 3..12//3]) + ** (ArgumentError) received range(s) with step != 1: [1..5//2, 3..12//3] + + iex> merge_ranges([]) + [] + """ + @spec merge_ranges(Enumerable.t(Range.t())) :: list(Range.t()) + def merge_ranges(ranges) do + bad_ranges = Enum.reject(ranges, &match?(_.._//1, &1)) + + unless bad_ranges == [], + do: raise(ArgumentError, "received range(s) with step != 1: #{inspect(bad_ranges)}") + + ranges + |> Enum.sort_by(fn l.._r//1 -> l end) + |> Enum.reject(&(Range.size(&1) == 0)) + |> Enum.chunk_while( + nil, + fn + range, nil -> + {:cont, range} + + range, acc_range -> + if Range.disjoint?(range, acc_range), + do: {:cont, acc_range, range}, + else: {:cont, merge_range_pair(acc_range, range)} + end, + fn + nil -> {:cont, nil} + final_acc -> {:cont, final_acc, nil} + end + ) + end + + defp merge_range_pair(l1..r1//1, l2..r2//1) do + min(l1, l2)..max(r1, r2)//1 + end + @doc """ Formats the ratio of two numbers as a percentage. """ diff --git a/reports/glides_terminal_departure_accuracy.livemd b/reports/glides_terminal_departure_accuracy.livemd index af298aa..f21125b 100644 --- a/reports/glides_terminal_departure_accuracy.livemd +++ b/reports/glides_terminal_departure_accuracy.livemd @@ -380,28 +380,6 @@ variances = [1, 2, 3, 5, 10] # OVERALL VALUES # ################## -merge_sorted_ranges = fn l1.._r1//1, _l2..r2//1 -> - l1..r2//1 -end - -# Combines any consecutive, overlapping ranges in a pre-sorted list. -merge_ranges = fn ranges -> - Enum.chunk_while( - ranges, - nil, - fn - range, nil -> - {:cont, range} - - range, acc_range -> - if Range.disjoint?(range, acc_range), - do: {:cont, acc_range, range}, - else: {:cont, merge_sorted_ranges.(acc_range, range)} - end, - fn final_acc -> {:cont, final_acc, nil} end - ) -end - # %{ # variance => %{ # stop_id => [actual_departure_range1, actual_departure_range2, ...] @@ -418,9 +396,8 @@ actual_departure_windows_by_variance = |> Map.new(fn {stop_id, timestamps} -> time_ranges = timestamps - |> Enum.sort() |> Enum.map(fn t -> (t - variance_sec)..(t + variance_sec)//1 end) - |> merge_ranges.() + |> GlidesReport.Util.merge_ranges() {stop_id, time_ranges} end) @@ -612,4 +589,4 @@ VegaLite.new(width: 500, height: 300, title: "Percent accurate by bucket size") |> VegaLite.encode_field(:y, "Accuracy (% of total predictions)", type: :quantitative) ``` - +