// Proleptic Gregorian calendar arithmetic and the UTC/local conversion for
// timestamps.
//
// Ion text keeps a timestamp's components in local time plus the local offset,
// while Ion binary keeps them in UTC. Transcoding between the two therefore
// shifts the date and time by the offset, rolling over days, months, leap days,
// and years. `with_offset` moves a timestamp to another offset, keeping the
// instant it names, and `compare_instant` compares those instants.
///|
/// Whether `year` is a leap year in the proleptic Gregorian calendar.
pub fn is_leap_year(year : Int32) -> Bool {
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
}
///|
/// The number of days in `month` of `year`.
pub fn days_in_month(year : Int32, month : Int32) -> Int32 {
match month {
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31
4 | 6 | 9 | 11 => 30
2 => if is_leap_year(year) { 29 } else { 28 }
_ => 30
}
}
///|
/// The civil date `days` days after `year-month-day`, rolling over months and
/// years. `days` may be negative.
pub fn shift_date(
year : Int32,
month : Int32,
day : Int32,
days : Int32,
) -> (Int32, Int32, Int32) {
let mut y = year
let mut m = month
let mut d = day + days
while d < 1 {
m -= 1
if m < 1 {
m = 12
y -= 1
}
d += days_in_month(y, m)
}
while d > days_in_month(y, m) {
d -= days_in_month(y, m)
m += 1
if m > 12 {
m = 1
y += 1
}
}
(y, m, d)
}
///|
/// Integer division that rounds toward negative infinity, so a minute count
/// before midnight maps onto the previous day.
fn floor_div(value : Int32, divisor : Int32) -> Int32 {
let quotient = value / divisor
if value % divisor != 0 && value < 0 {
quotient - 1
} else {
quotient
}
}
///|
/// The timestamp's date and time moved by `minutes`, with `offset`. A
/// timestamp with no time of day is returned unchanged.
fn shift_minutes(
timestamp : Timestamp,
minutes : Int32,
offset : TimestampOffset,
) -> Timestamp {
match
(timestamp.month(), timestamp.day(), timestamp.hour(), timestamp.minute()) {
(Some(month), Some(day), Some(hour), Some(minute)) => {
let total = hour * 60 + minute + minutes
let day_shift = floor_div(total, 1440)
let minutes_of_day = total - day_shift * 1440
let (year, month, day) = shift_date(
timestamp.year(),
month,
day,
day_shift,
)
Timestamp::Timestamp(
year~,
month=Some(month),
day=Some(day),
hour=Some(minutes_of_day / 60),
minute=Some(minutes_of_day % 60),
second=timestamp.second(),
fraction=timestamp.fraction(),
offset=Some(offset),
)
}
_ => timestamp
}
}
///|
/// The minutes east of UTC that an offset shifts local time by. An unknown
/// offset means the components are in UTC.
fn offset_minutes(offset : TimestampOffset?) -> Int32 {
match offset {
Some(TimestampOffset::Known(minutes)) => minutes
_ => 0
}
}
///|
/// The same instant expressed at `offset`: the date and time move by the
/// difference between the offsets. An unknown offset reads and writes the
/// components in UTC. A timestamp with no time of day has no offset, and is
/// returned unchanged.
pub fn Timestamp::with_offset(
self : Timestamp,
offset : TimestampOffset,
) -> Timestamp {
let minutes = offset_minutes(Some(offset)) - offset_minutes(self.offset())
shift_minutes(self, minutes, offset)
}
///|
/// The same instant in UTC, with the offset `Z`: the form whose components
/// Ion binary stores and Ion Hash serializes.
pub fn Timestamp::as_utc(self : Timestamp) -> Timestamp {
self.with_offset(TimestampOffset::Known(0))
}
///|
/// Read the components as UTC and move them to the timestamp's own offset.
/// This mixes UTC components with a local offset, so the input does not name
/// the instant its text shows; build the UTC timestamp and use `with_offset`
/// instead.
#deprecated("build the timestamp in UTC and use `with_offset`")
pub fn Timestamp::as_local(self : Timestamp) -> Timestamp {
match self.offset() {
Some(offset) => shift_minutes(self, offset_minutes(Some(offset)), offset)
None => self
}
}
///|
/// Compare the instants two timestamps name, as `-1`, `0`, or `1`, ignoring
/// their precision and offsets: a component a timestamp leaves out counts as
/// its lowest value, and a timestamp with no time, or an unknown offset, is
/// read in UTC. `2007T`, `2007-01-01`, and `2007-01-01T01:00+01:00` name the
/// same instant, though they are different Ion values.
pub fn Timestamp::compare_instant(self : Timestamp, other : Timestamp) -> Int32 {
let a = self.as_utc()
let b = other.as_utc()
let fields = (timestamp : Timestamp) => {
[
timestamp.year(),
timestamp.month().unwrap_or(1),
timestamp.day().unwrap_or(1),
timestamp.hour().unwrap_or(0),
timestamp.minute().unwrap_or(0),
timestamp.second().unwrap_or(0),
]
}
let (left, right) = (fields(a), fields(b))
for index in 0.. Int32 {
let zero = Decimal::zero()
let a = a.unwrap_or(zero)
let b = b.unwrap_or(zero)
// Scale both coefficients to the smaller exponent, then compare them.
let exponent = @cmp.minimum(a.exponent(), b.exponent())
let scale = (value : Decimal) => {
value.coefficient() *
@bigint.BigInt::from_int(10).pow(
@bigint.BigInt::from_int(value.exponent() - exponent),
)
}
scale(a).compare(scale(b))
}