///|
/// Half-open time range [start, finish). This representation makes adjacent
/// windows composable without double-counting their shared endpoint.
pub struct TimeRange {
start : DateTime
finish : DateTime
} derive(Eq, Debug)
///|
pub suberror TimeRangeError {
EmptyOrReversedRange(DateTime, DateTime)
} derive(Eq, Debug)
///|
pub fn TimeRange::new(
start : DateTime,
finish : DateTime,
) -> TimeRange raise TimeRangeError {
if start.to_epoch_second() >= finish.to_epoch_second() {
raise EmptyOrReversedRange(start, finish)
}
{ start, finish }
}
///|
pub fn TimeRange::duration_seconds(self : TimeRange) -> Int64 {
self.finish.to_epoch_second() - self.start.to_epoch_second()
}
///|
pub fn TimeRange::contains(self : TimeRange, value : DateTime) -> Bool {
self.start.to_epoch_second() <= value.to_epoch_second() &&
value.to_epoch_second() < self.finish.to_epoch_second()
}
///|
pub fn TimeRange::overlaps(self : TimeRange, other : TimeRange) -> Bool {
self.start.to_epoch_second() < other.finish.to_epoch_second() &&
other.start.to_epoch_second() < self.finish.to_epoch_second()
}
///|
pub fn TimeRange::is_adjacent(self : TimeRange, other : TimeRange) -> Bool {
self.finish == other.start || other.finish == self.start
}
///|
pub fn TimeRange::intersection(
self : TimeRange,
other : TimeRange,
) -> TimeRange? {
if !self.overlaps(other) {
return None
}
let start = if self.start.to_epoch_second() >= other.start.to_epoch_second() {
self.start
} else {
other.start
}
let finish = if self.finish.to_epoch_second() <=
other.finish.to_epoch_second() {
self.finish
} else {
other.finish
}
Some({ start, finish })
}
///|
pub fn TimeRange::merge(self : TimeRange, other : TimeRange) -> TimeRange? {
if !self.overlaps(other) && !self.is_adjacent(other) {
return None
}
let start = if self.start.to_epoch_second() <= other.start.to_epoch_second() {
self.start
} else {
other.start
}
let finish = if self.finish.to_epoch_second() >=
other.finish.to_epoch_second() {
self.finish
} else {
other.finish
}
Some({ start, finish })
}
///|
fn sort_ranges(values : Array[TimeRange]) -> Array[TimeRange] {
let result = values.copy()
result.sort_by((left, right) => left.start.compare(right.start))
result
}
///|
/// Merge overlapping ranges and, by default, directly adjacent ranges.
pub fn merge_time_ranges(
values : Array[TimeRange],
merge_adjacent? : Bool = true,
) -> Array[TimeRange] {
let result : Array[TimeRange] = []
for value in sort_ranges(values) {
if result.length() == 0 {
result.push(value)
continue
}
let previous = result[result.length() - 1]
let can_merge = previous.overlaps(value) ||
(merge_adjacent && previous.is_adjacent(value))
if can_merge {
result[result.length() - 1] = previous.merge(value).unwrap()
} else {
result.push(value)
}
}
result
}
///|
/// Find free half-open windows inside bounds after normalizing busy ranges.
pub fn free_time_ranges(
bounds : TimeRange,
busy : Array[TimeRange],
) -> Array[TimeRange] {
let clipped : Array[TimeRange] = []
for value in busy {
match bounds.intersection(value) {
Some(overlap) => clipped.push(overlap)
None => ()
}
}
let occupied = merge_time_ranges(clipped)
let free : Array[TimeRange] = []
let mut cursor = bounds.start
for value in occupied {
if cursor.to_epoch_second() < value.start.to_epoch_second() {
free.push({ start: cursor, finish: value.start })
}
if cursor.to_epoch_second() < value.finish.to_epoch_second() {
cursor = value.finish
}
}
if cursor.to_epoch_second() < bounds.finish.to_epoch_second() {
free.push({ start: cursor, finish: bounds.finish })
}
free
}
///|
pub fn TimeRange::to_text(self : TimeRange) -> String {
self.start.to_iso_string() +
" / " +
self.finish.to_iso_string() +
" (" +
self.duration_seconds().to_string() +
"s)"
}
///|
pub fn ranges_total_seconds(values : Array[TimeRange]) -> Int64 {
let mut total = 0L
for value in merge_time_ranges(values) {
total += value.duration_seconds()
}
total
}