///|
// Python aware datetimes sharing the same tzinfo compare wall-clock fields.
// Distinct zones (and fixed-offset representations) compare actual UTC instants.
fn arithmetic_date_at_least(left : DateTime, right : DateTime) -> Bool {
match (left.timezone, right.timezone) {
(Some(first), Some(last)) if physical_equal(first, last) =>
locator_microseconds(left) >= locator_microseconds(right)
_ => rruleset_instant(left) >= rruleset_instant(right)
}
}
///|
/// Generate equally spaced numeric dates using Matplotlib's endpoint/linspace
/// algorithm. Endpoints carry their own offsets; a retained rule zone supplies
/// wall-clock duration arithmetic across DST. This retains the upstream
/// endpoint adjustments, including negative steps and floating ceil edge cases. Zero spacing, arithmetic overflow, negative sample counts and
/// counts beyond MoonBit's Int32 array index range raise DateError.
///
/// ```mbt check
/// test {
/// let values = @date.drange(
/// @date.datetime(2011, 1, 1),
/// @date.datetime(2011, 1, 2),
/// @date.timedelta(hours=4),
/// )
/// assert_eq(values.length(), 6)
/// assert_eq(@date.num2date(values[5]).hour(), 20)
/// }
/// ```
pub fn drange(
dstart : DateTime,
dend : DateTime,
delta : TimeDelta,
) -> Array[Double] raise DateError {
// The upstream datetime -> UTC conversion cannot construct year 0/10000,
// even though its subsequent floating ordinal may round to a day boundary.
for value in [dstart, dend] {
let instant = rruleset_instant(value)
if instant < -62_135_596_800_000_000L || instant >= 253_402_300_800_000_000L {
raise DateError::InvalidDate(
message="drange UTC input is outside years 1..9999",
)
}
}
let first = dstart.to_ordinal()
let stop = dend.to_ordinal()
let step = delta.total_seconds() / 86400.0
if step == 0.0 {
raise DateError::InvalidDate(message="drange spacing must not be zero")
}
let samples = ((stop - first) / step).ceil()
// Supported dates and microsecond spacing bound the real quotient below
// Int64, but check before the backend's saturating float-to-int conversion.
if samples.is_nan() ||
samples.is_inf() ||
samples <= -9.223372036854776e18 ||
samples >= 9.223372036854776e18 {
raise DateError::InvalidDate(
message="drange sample count is not representable",
)
}
let mut count = samples.to_int64()
let mut end = dstart.add_timedelta_multiple(delta, count)
if arithmetic_date_at_least(end, dend) {
end = end.add_timedelta_multiple(delta, -1L)
count -= 1L
}
let last_instant = rruleset_instant(end)
if last_instant < -62_135_596_800_000_000L ||
last_instant >= 253_402_300_800_000_000L {
raise DateError::InvalidDate(
message="drange UTC endpoint is outside years 1..9999",
)
}
let last = end.to_ordinal()
if count < -1L {
raise DateError::InvalidDate(message="drange has a negative sample count")
}
if count >= 2147483647L {
raise DateError::InvalidDate(
message="drange exceeds the Int32 array index range",
)
}
let size = (count + 1L).to_int()
let result : Array[Double] = Array(capacity=size)
if size == 0 {
return result
}
if size == 1 {
return [first]
}
let difference = last - first
let interpolation_step = difference / count.to_double()
for index in 0..