///|
/// A calendar date in the ISO 8601 calendar, as the `[[ISOYear]]`,
/// `[[ISOMonth]]` and `[[ISODay]]` internal slots.
///
/// The fields are declared in descending significance so that the derived
/// comparison is the chronological one.
pub struct IsoDate {
/// The ISO year, in `-271821 ..= 275760`.
year : Int
/// The month, in `1 ..= 12`.
month : Int
/// The day of the month, in `1 ..= 31`.
day : Int
} derive(Eq, Compare, Debug)
///|
pub impl Show for IsoDate with fn output(self, logger) {
let buf = StringBuilder::new()
write_date(buf, self.year, self.month, self.day)
logger.write_string(buf.to_string())
}
///|
/// The ISO date of the Unix epoch.
pub let iso_date_unix_epoch : IsoDate = { year: 1970, month: 1, day: 1 }
///|
/// Creates a date without checking that it exists or is in range.
fn IsoDate::new_unchecked(year : Int, month : Int, day : Int) -> IsoDate {
{ year, month, day }
}
///|
/// Returns the year.
pub fn IsoDate::year(self : IsoDate) -> Int {
self.year
}
///|
/// Returns the month, 1-based.
pub fn IsoDate::month(self : IsoDate) -> Int {
self.month
}
///|
/// Returns the day of the month.
pub fn IsoDate::day(self : IsoDate) -> Int {
self.day
}
///|
/// `RegulateISODate`: brings the fields into range according to `overflow`.
fn IsoDate::regulate(
year : Int,
month : Int,
day : Int,
overflow : Overflow,
) -> IsoDate raise TemporalError {
match overflow {
Constrain => {
let month = month.max(1).min(12)
let day = day.max(1).min(iso_days_in_month(year, month))
{ year, month, day }
}
Reject => {
if !is_valid_iso_date(year, month, day) {
raise RangeError("\{year}-\{month}-\{day} is not a valid ISO date")
}
{ year, month, day }
}
}
}
///|
/// Creates a date, regulating it and then checking it is inside the range
/// Temporal supports.
fn IsoDate::new_with_overflow(
year : Int,
month : Int,
day : Int,
overflow : Overflow,
) -> IsoDate raise TemporalError {
let date = IsoDate::regulate(year, month, day, overflow)
date.check_within_limits()
date
}
///|
/// Raises unless the date lies within the range Temporal supports.
fn IsoDate::check_within_limits(self : IsoDate) -> Unit raise TemporalError {
if !iso_datetime_within_limits(self, iso_time_noon) {
raise RangeError("date is outside the ISO date-time limits")
}
}
///|
/// Returns whether the date names a day that exists.
pub fn IsoDate::is_valid(self : IsoDate) -> Bool {
is_valid_iso_date(self.year, self.month, self.day)
}
///|
/// Returns whether `year`-`month`-`day` names a day that exists.
pub fn is_valid_iso_date(year : Int, month : Int, day : Int) -> Bool {
if month < 1 || month > 12 {
return false
}
day >= 1 && day <= iso_days_in_month(year, month)
}
///|
/// `ISODateToEpochDays`: days since 1970-01-01.
pub fn IsoDate::to_epoch_days(self : IsoDate) -> Int64 {
epoch_days_from_gregorian_date(self.year, self.month, self.day)
}
///|
/// The epoch nanoseconds of this date at midnight UTC.
fn IsoDate::as_nanoseconds(self : IsoDate) -> @int128.Int128 {
utc_epoch_nanoseconds(self, iso_time_midnight)
}
///|
/// `ISODateToEpochDays` over unbalanced fields: `month` may be outside
/// `1 ..= 12` and `day` outside the month's length, and the excess carries.
fn iso_date_to_epoch_days(year : Int, month : Int, day : Int) -> Int64 {
let resolved_year = year + div_euclid_i(month, 12)
let resolved_month = rem_euclid_i(month, 12)
epoch_days_from_gregorian_date(resolved_year, resolved_month, 1) +
day.to_int64() -
1L
}
///|
/// `BalanceISODate`: carries out-of-range month and day values into the year.
fn IsoDate::balance(year : Int, month : Int, day : Int) -> IsoDate {
let epoch_days = iso_date_to_epoch_days(year, month, day)
let (year, month, day) = ymd_from_epoch_days(epoch_days)
{ year, month, day }
}
///|
/// Balances a date while rejecting an intermediate that escapes the supported
/// epoch-day range.
///
/// `day` is an `Int64` because it accumulates a duration's day count before
/// being balanced, which can overflow an `Int` well before it overflows the
/// date range check.
fn IsoDate::try_balance(
year : Int,
month : Int,
day : Int64,
) -> IsoDate raise TemporalError {
let epoch_days = iso_date_to_epoch_days(year, month, 1) + day - 1L
if epoch_days.abs() > MAX_EPOCH_DAYS {
raise RangeError("epoch days exceed the maximum range")
}
let (year, month, day) = ymd_from_epoch_days(epoch_days)
{ year, month, day }
}
///|
/// `BalanceISOYearMonth`: carries an out-of-range month into the year.
fn balance_iso_year_month(year : Int, month : Int) -> (Int, Int) {
(year + div_euclid_i(month - 1, 12), rem_euclid_i(month - 1, 12) + 1)
}
///|
/// `BalanceISOYearMonth` over 64-bit inputs, clamping the resulting year to
/// the `Int` range so that a wildly out-of-range duration still produces a
/// value the range check can reject.
fn balance_iso_year_month_clamped(year : Int64, month : Int64) -> (Int, Int) {
let y = year + div_euclid(month - 1L, 12L)
let m = rem_euclid(month - 1L, 12L) + 1L
(y.max(-2147483648L).min(2147483647L).to_int(), m.to_int())
}
///|
/// Returns whether `year`-`month` is within the supported range.
///
/// The endpoints are partial months, so the boundary years need a month check
/// as well.
fn year_month_within_limits(year : Int, month : Int) -> Bool {
if year < -271821 || year > 275760 {
false
} else if year == -271821 && month < 4 {
false
} else if year == 275760 && month > 9 {
false
} else {
true
}
}
///|
/// `AddISODate`: adds a date duration, regulating the intermediate year and
/// month before the days are carried.
fn IsoDate::add_date_duration(
self : IsoDate,
duration : DateDuration,
overflow : Overflow,
) -> IsoDate raise TemporalError {
// Years and months are applied first, and the day is constrained against
// the resulting month before any day arithmetic happens. This is what makes
// "January 31 plus one month" land on the end of February.
let (year, month) = balance_iso_year_month_clamped(
self.year.to_int64() + duration.years,
self.month.to_int64() + duration.months,
)
let intermediate = IsoDate::new_with_overflow(year, month, self.day, overflow)
let additional_days = duration.days + 7L * duration.weeks
IsoDate::try_balance(
intermediate.year,
intermediate.month,
intermediate.day.to_int64() + additional_days,
)
}
///|
/// `DifferenceISODate`: the duration from this date to `other`, expressed in
/// units no larger than `largest_unit`.
fn IsoDate::diff_iso_date(
self : IsoDate,
other : IsoDate,
largest_unit : DateTimeUnit,
) -> DateDuration raise TemporalError {
let sign = -self.compare(other).compare(0)
if sign == 0 {
return DateDuration::default()
}
let mut years = 0
let mut months = 0
if largest_unit is (Year | Month) {
// Start from the raw year difference rather than stepping one year at a
// time; the answer is at most one away from it.
let mut candidate_years = other.year - self.year
if candidate_years != 0 {
candidate_years = candidate_years - sign
}
while !iso_date_surpasses(
IsoDate::new_unchecked(
self.year + candidate_years,
self.month,
self.day,
),
other,
sign,
) {
years = candidate_years
candidate_years = candidate_years + sign
}
let mut candidate_months = sign
let mut intermediate = balance_iso_year_month(
self.year + years,
self.month + candidate_months,
)
while !iso_date_surpasses(
IsoDate::new_unchecked(intermediate.0, intermediate.1, self.day),
other,
sign,
) {
months = candidate_months
candidate_months = candidate_months + sign
intermediate = balance_iso_year_month(
intermediate.0,
intermediate.1 + sign,
)
}
if largest_unit is Month {
months = months + years * 12
years = 0
}
}
let intermediate = balance_iso_year_month(
self.year + years,
self.month + months,
)
let constrained = IsoDate::new_with_overflow(
intermediate.0,
intermediate.1,
self.day,
Constrain,
)
// Whatever the calendar units did not cover is a whole number of days, so
// the remainder is a plain epoch-day subtraction rather than another loop.
let days = epoch_days_from_gregorian_date(other.year, other.month, other.day) -
epoch_days_from_gregorian_date(
constrained.year,
constrained.month,
constrained.day,
)
let (weeks, days) = if largest_unit is Week {
(days / 7L, days % 7L)
} else {
(0L, days)
}
DateDuration::new(years.to_int64(), months.to_int64(), weeks, days)
}
///|
/// Returns whether stepping from `this` in direction `sign` has gone past
/// `other`.
fn iso_date_surpasses(this : IsoDate, other : IsoDate, sign : Int) -> Bool {
this.compare(other).compare(0) * sign == 1
}