///|
priv enum DateInterval {
Microseconds(Int32)
Subday(Double)
Days(Int32)
Months(Int32)
Years(Int32)
}
///|
fn interval_at_most(target : Double, limit : Double) -> Bool {
target <= limit + 1.0e-12 * Double::max(1.0, Double::abs(limit))
}
///|
fn choose_interval(span : Double, count : Int32) -> DateInterval {
let target = span / Double::from_int(if count < 2 { 2 } else { count - 1 })
if target < 1.0 / 86400.0 {
Microseconds(microsecond_interval(target * 86_400_000_000.0))
} else if interval_at_most(target, 1.0 / 86400.0) {
Subday(1.0 / 86400.0)
} else if interval_at_most(target, 5.0 / 86400.0) {
Subday(5.0 / 86400.0)
} else if interval_at_most(target, 15.0 / 86400.0) {
Subday(15.0 / 86400.0)
} else if interval_at_most(target, 30.0 / 86400.0) {
Subday(30.0 / 86400.0)
} else if interval_at_most(target, 1.0 / 1440.0) {
Subday(1.0 / 1440.0)
} else if interval_at_most(target, 5.0 / 1440.0) {
Subday(5.0 / 1440.0)
} else if interval_at_most(target, 15.0 / 1440.0) {
Subday(15.0 / 1440.0)
} else if interval_at_most(target, 30.0 / 1440.0) {
Subday(30.0 / 1440.0)
} else if interval_at_most(target, 1.0 / 24.0) {
Subday(1.0 / 24.0)
} else if interval_at_most(target, 1.0 / 12.0) {
Subday(1.0 / 12.0)
} else if interval_at_most(target, 1.0 / 6.0) {
Subday(1.0 / 6.0)
} else if interval_at_most(target, 0.5) {
Subday(0.5)
} else if interval_at_most(target, 1.0) {
Days(1)
} else if interval_at_most(target, 2.0) {
Days(2)
} else if interval_at_most(target, 7.0) {
Days(7)
} else if interval_at_most(target, 14.0) {
Days(14)
} else if interval_at_most(target, 45.0) {
Months(1)
} else if interval_at_most(target, 135.0) {
Months(3)
} else if interval_at_most(target, 300.0) {
Months(6)
} else if interval_at_most(target, 730.0) {
Years(1)
} else if interval_at_most(target, 1825.0) {
Years(5)
} else {
Years(10)
}
}
///|
fn date_from_ordinal(value : Double, offset_minutes : Int32) -> DateTime? {
Some(from_ordinal(value, offset_minutes~)) catch {
_ => None
}
}
///|
fn month_start(
year : Int32,
month : Int32,
offset_minutes : Int32,
) -> DateTime? {
if year < 1 || year > 9999 || month < 1 || month > 12 {
None
} else {
Some(try! datetime(year, month, 1, offset_minutes~))
}
}
///|
fn add_months(date : DateTime, months : Int32) -> DateTime? {
let absolute = date.year() * 12 + date.month() - 1 + months
let year = absolute / 12
let month = absolute % 12 + 1
month_start(year, month, date.offset_minutes())
}
///|
fn aligned_month_start(date : DateTime, interval : Int32) -> DateTime? {
let month_index = date.year() * 12 + date.month() - 1
let aligned = month_index / interval * interval
month_start(aligned / 12, aligned % 12 + 1, date.offset_minutes())
}
///|
fn aligned_year_start(date : DateTime, interval : Int32) -> DateTime? {
let year = (date.year() - 1) / interval * interval + 1
month_start(year, 1, date.offset_minutes())
}
///|
fn subday_ticks(
first : Double,
last : Double,
interval : Double,
) -> Array[Double] {
let start = (first / interval).floor() * interval
let result = Array(capacity=8)
let mut value = start
let mut steps = 0
while value <= last + interval * 1.0e-9 && steps < 10000 {
if value >= first - interval * 1.0e-9 {
result.push(value)
}
value = value + interval
steps = steps + 1
}
result
}
///|
fn day_ticks(first : Double, last : Double, interval : Int32) -> Array[Double] {
let step = Double::from_int(interval)
let start = (first / step).floor() * step
let result = Array(capacity=8)
let mut value = start
let mut steps = 0
while value <= last + step * 1.0e-9 && steps < 10000 {
if value >= first - step * 1.0e-9 {
result.push(value)
}
value = value + step
steps = steps + 1
}
result
}
///|
fn shifted_subday_ticks(
first : Double,
last : Double,
interval : Double,
offset_minutes : Int32,
) -> Array[Double] {
let (_, remainder) = delta_split(selected_epoch().seconds, 86400L)
let shift = (remainder.to_double() + Double::from_int(offset_minutes) * 60.0) /
86400.0
let shifted = subday_ticks(first + shift, last + shift, interval)
shifted.map(value => value - shift)
}
///|
fn shifted_day_ticks(
first : Double,
last : Double,
interval : Int32,
offset_minutes : Int32,
) -> Array[Double] {
let (days, remainder) = delta_split(selected_epoch().seconds, 86400L)
let shift = (days % Int64::from_int(interval)).to_double() +
(remainder.to_double() + Double::from_int(offset_minutes) * 60.0) / 86400.0
let shifted = day_ticks(first + shift, last + shift, interval)
shifted.map(value => value - shift)
}
///|
fn calendar_ticks(
first : Double,
last : Double,
interval : DateInterval,
offset_minutes : Int32,
) -> Array[Double] {
let first_date = match date_from_ordinal(first, offset_minutes) {
Some(value) => value
None => return []
}
match interval {
Microseconds(step) => microsecond_ticks(first, last, step)
Subday(step) => shifted_subday_ticks(first, last, step, offset_minutes)
Days(step) => shifted_day_ticks(first, last, step, offset_minutes)
Months(step) => {
let start = match aligned_month_start(first_date, step) {
Some(value) => value
None => return []
}
let result = Array(capacity=8)
let mut current = start
let mut steps = 0
while current.to_ordinal() <= last + 1.0e-9 && steps < 10000 {
let ordinal = current.to_ordinal()
if ordinal >= first - 1.0e-9 {
result.push(ordinal)
}
current = match add_months(current, step) {
Some(value) => value
None => break
}
steps = steps + 1
}
result
}
Years(step) => {
let start = match aligned_year_start(first_date, step) {
Some(value) => value
None => return []
}
let result = Array(capacity=8)
let mut current = start
let mut steps = 0
while current.to_ordinal() <= last + 1.0e-9 && steps < 10000 {
let ordinal = current.to_ordinal()
if ordinal >= first - 1.0e-9 {
result.push(ordinal)
}
current = match add_months(current, step * 12) {
Some(value) => value
None => break
}
steps = steps + 1
}
result
}
}
}
///|
/// Generate deterministic date tick values for a numeric day range.
/// A `timezone` aligns local calendar candidates using exact transition and
/// footer offsets, skipping gaps and retaining both sides of folds.
pub fn date_tick_values(
lower : Double,
upper : Double,
count~ : Int32 = 6,
offset_minutes~ : Int32 = 0,
timezone? : DateTimeZone,
) -> Array[Double] {
if lower.is_nan() || upper.is_nan() || lower.is_inf() || upper.is_inf() {
return []
}
if offset_minutes <= -1440 || offset_minutes >= 1440 {
return []
}
if lower == upper {
return [lower]
}
let ascending = lower <= upper
let first = Double::min(lower, upper)
let last = Double::max(lower, upper)
let interval = choose_interval(last - first, count)
let result = match timezone {
Some(zone) => timezone_calendar_ticks(first, last, interval, zone)
None => calendar_ticks(first, last, interval, offset_minutes)
}
if ascending {
result
} else {
result.rev()
}
}
///|
fn timezone_calendar_ticks(
first : Double,
last : Double,
interval : DateInterval,
zone : DateTimeZone,
) -> Array[Double] {
if interval is Microseconds(step) {
return microsecond_ticks(first, last, step)
}
let first_date = zone.to_datetime(first) catch { _ => return [] }
let last_date = zone.to_datetime(last) catch { _ => return [] }
let (local_first, local_last, _) = recurrence_timezone_local_bounds(
zone, first, last, first_date, last_date,
)
let first_local = unix_microseconds_to_ordinal(
locator_microseconds(local_first),
)
let last_local = unix_microseconds_to_ordinal(
locator_microseconds(local_last),
)
let result : Array[Double] = []
for wall in calendar_ticks(first_local, last_local, interval, 0) {
let date = forward_calendar_date(wall) catch { _ => continue }
for ordinal in zone.local_ordinal_candidates(date) {
if ordinal >= first && ordinal <= last {
result.push(ordinal)
}
}
}
result.sort()
result
}