/// Recalculates a `Relative` TimeSpec's epoch using `anchor_epoch` as the base
/// instead of `now`. `Absolute` timespecs are returned unchanged.
///|
fn resolve_relative_with_anchor(
ts : TimeSpec,
anchor_epoch : Int64,
) -> TimeSpec {
match ts {
Absolute(_, _) => ts // Absolute remains unchanged
Relative(_, duration) => {
let duration_ms = duration.0
let new_epoch = anchor_epoch + duration_ms
Relative(EpochTime(new_epoch), duration)
}
}
}
/// Parses a time range with optional `since` and `until` bounds.
///
/// Supports two input styles (mutually exclusive):
///
/// 1. **Two-argument** (recommended): pass `since` and/or `until` individually.
/// 2. **Tilde notation**: pass `input` as a tilde-delimited `"since~until"` string (e.g. `"5d~3m"`, `"5d~"`, `"~3m"`).
///
/// `input` and `since`/`until` cannot be combined; doing so raises `ParseError`.
///
/// Parameters:
///
/// * `input` : A tilde-delimited range string. Split on tilde (`~`) to derive since/until.
/// * `since` : The since-side timespec string. Overrides `input` when non-empty.
/// * `until` : The until-side timespec string. Overrides `input` when non-empty.
/// * `epoch` : Base epoch offset. Defaults to Unix epoch.
/// * `default_sign` : Sign interpretation for unsigned durations. Defaults to `Plus`.
/// * `now` : Clock function. Defaults to `@env.now`.
/// * `default_tz_offset` : Default timezone for TZ-less datetimes. Defaults to `Local`.
/// * `swap` : When `true`, swaps since/until if since > until. Defaults to `false`.
/// * `parse_datetime` : Pluggable datetime parser. Defaults to `default_parse_datetime`.
///
/// Returns a `TimeRange` with anchor resolution applied when one side is
/// `Absolute` and the other is `Relative`.
///|
pub fn parse_range(
input? : String = "",
since? : String = "",
until? : String = "",
epoch? : EpochTime = EpochTime(0L),
default_sign? : Sign = Plus,
default_tz_offset? : TzOffset = Local,
now? : () -> UInt64 = @env.now,
swap? : Bool = false,
parse_datetime? : (String) -> Int64? = default_parse_datetime,
) -> TimeRange raise ParseError {
// Mutual exclusion check between input and since/until
let mut since = since
let mut until = until
if input.length() > 0 && (since.length() > 0 || until.length() > 0) {
raise ParseError("cannot specify both input and since/until in parse_range")
}
// Expand since/until from input
if input.length() > 0 {
match input.split_once("~") {
Some((s, u)) => {
since = s.to_owned()
until = u.to_owned()
}
None => since = input // No tilde -> treat as since
}
}
// Parse since (if non-empty)
let mut since_ts : TimeSpec? = if since.trim().length() > 0 {
parse_timespec(
since,
epoch~,
default_sign~,
default_tz_offset~,
now~,
parse_datetime~,
)
} else {
None
}
// Parse until (if non-empty)
let mut until_ts : TimeSpec? = if until.trim().length() > 0 {
parse_timespec(
until,
epoch~,
default_sign~,
default_tz_offset~,
now~,
parse_datetime~,
)
} else {
None
}
// Anchor resolution (only when both are present)
match (since_ts, until_ts) {
(Some(s), Some(u)) => {
let (resolved_s, resolved_u) = resolve_anchor(s, u)
since_ts = Some(resolved_s)
until_ts = Some(resolved_u)
}
_ => ()
}
// swap: exchange since and until if since > until
if swap {
match (since_ts, until_ts) {
(Some(s), Some(u)) =>
if s.to_epoch_ms() > u.to_epoch_ms() {
let tmp = since_ts
since_ts = until_ts
until_ts = tmp
}
_ => ()
}
}
{ since: since_ts, until: until_ts }
}
/// Resolves anchor relationships between since and until TimeSpecs.
///
/// When one side is `Absolute` and the other is `Relative`, the absolute side
/// becomes the anchor and the relative side is recalculated against it.
/// When both are the same kind, they remain independently resolved.
///|
fn resolve_anchor(
since_ts : TimeSpec,
until_ts : TimeSpec,
) -> (TimeSpec, TimeSpec) {
let s_abs = since_ts.is_absolute()
let u_abs = until_ts.is_absolute()
if s_abs && !u_abs {
// since is Absolute -> recalculate until relative to since
let resolved = resolve_relative_with_anchor(
until_ts,
since_ts.to_epoch_ms(),
)
(since_ts, resolved)
} else if u_abs && !s_abs {
// until is Absolute -> recalculate since relative to until
let resolved = resolve_relative_with_anchor(
since_ts,
until_ts.to_epoch_ms(),
)
(resolved, until_ts)
} else {
// Same kind -> independent
(since_ts, until_ts)
}
}