///|
/// Normalize arbitrary weekday values into Monday=0 through Sunday=6.
pub fn normalize_weekday(weekday : Int) -> Int {
let mut result = weekday % 7
if result < 0 {
result += 7
}
result
}
///|
/// Construct a safe calendar bucket.
pub fn CalendarBucket::new(
start_second : Double,
end_second : Double,
congestion : Double,
label? : String = "custom",
) -> CalendarBucket {
{
start_second: clamp(start_second, 0.0, 86400.0),
end_second: clamp(end_second, 0.0, 86400.0),
congestion: clamp(congestion, 0.0, 1.0),
label,
}
}
///|
/// Select a bucket when its half-open range contains the requested second.
pub fn CalendarBucket::contains(
self : CalendarBucket,
second_of_day : Double,
) -> Bool {
second_of_day >= self.start_second && second_of_day < self.end_second
}
///|
/// Return the bucket's midpoint in seconds.
pub fn CalendarBucket::midpoint(self : CalendarBucket) -> Double {
(self.start_second + self.end_second) / 2.0
}
///|
/// Select a day's congestion, falling back when no bucket matches.
pub fn DayCalendar::congestion_at(
self : DayCalendar,
second_of_day : Double,
) -> Double {
let second = clamp(second_of_day, 0.0, 86399.999999)
let mut result = clamp(self.fallback_congestion, 0.0, 1.0)
for bucket in self.buckets {
if bucket.contains(second) {
result = bucket.congestion
}
}
result
}
///|
/// Find a day by weekday and evaluate its buckets.
pub fn RouteCalendar::congestion_at(
self : RouteCalendar,
weekday : Int,
second_of_day : Double,
) -> Double {
let wanted = normalize_weekday(weekday)
let mut result = clamp(self.default_congestion, 0.0, 1.0)
for day in self.days {
if normalize_weekday(day.weekday) == wanted {
result = day.congestion_at(second_of_day)
}
}
result
}
///|
/// Return the selected bucket label, or `default` when no bucket matches.
pub fn DayCalendar::label_at(
self : DayCalendar,
second_of_day : Double,
) -> String {
for bucket in self.buckets {
if bucket.contains(second_of_day) {
return bucket.label
}
}
"default"
}
///|
/// Count valid buckets in a day.
pub fn DayCalendar::valid_bucket_count(self : DayCalendar) -> Int {
let mut count = 0
for bucket in self.buckets {
if validate_calendar_bucket(bucket) {
count += 1
}
}
count
}
///|
/// Validate and normalize a route calendar.
pub fn normalize_calendar(calendar : RouteCalendar) -> RouteCalendar {
let days : Array[DayCalendar] = []
for day in calendar.days {
let buckets : Array[CalendarBucket] = []
for bucket in day.buckets {
if validate_calendar_bucket(bucket) {
buckets.push({
..bucket,
congestion: clamp(bucket.congestion, 0.0, 1.0),
})
}
}
days.push({
weekday: normalize_weekday(day.weekday),
buckets,
fallback_congestion: clamp(day.fallback_congestion, 0.0, 1.0),
})
}
{ days, default_congestion: clamp(calendar.default_congestion, 0.0, 1.0) }
}
///|
/// Build the reference weekly calendar used by examples and benchmarks.
pub fn demo_calendar() -> RouteCalendar {
let morning = CalendarBucket::new(
7.0 * 3600.0,
9.5 * 3600.0,
0.72,
label="morning-peak",
)
let evening = CalendarBucket::new(
17.0 * 3600.0,
19.5 * 3600.0,
0.86,
label="evening-peak",
)
let quiet = CalendarBucket::new(0.0, 6.0 * 3600.0, 0.18, label="overnight")
let days : Array[DayCalendar] = []
for weekday in 0..<5 {
days.push({
weekday,
buckets: [quiet, morning, evening],
fallback_congestion: 0.35,
})
}
for weekday in 5..<7 {
days.push({ weekday, buckets: [quiet], fallback_congestion: 0.25 })
}
normalize_calendar({ days, default_congestion: 0.35 })
}
///|
/// Return a human-readable label for the current calendar bucket.
pub fn calendar_label(
calendar : RouteCalendar,
weekday : Int,
second_of_day : Double,
) -> String {
let wanted = normalize_weekday(weekday)
for day in calendar.days {
if normalize_weekday(day.weekday) == wanted {
return day.label_at(second_of_day)
}
}
"default"
}
///|
/// Interpolate between two congestion values over a time interval.
pub fn interpolate_congestion(
start : Double,
end : Double,
progress : Double,
) -> Double {
let p = clamp(progress, 0.0, 1.0)
clamp(start + (end - start) * p, 0.0, 1.0)
}
///|
/// Evaluate a calendar over evenly spaced samples.
pub fn sample_calendar(
calendar : RouteCalendar,
weekday : Int,
start_second : Double,
interval : Double,
count : Int,
) -> Array[Double] {
let result : Array[Double] = []
for index in 0..