///|
/// A calendar date with no time and no time zone, such as a birthday.
///
/// Reference:
pub struct PlainDate {
iso : IsoDate
calendar : Calendar
} derive(Eq)
///|
pub impl Show for PlainDate with fn output(self, logger) {
logger.write_string(self.to_string_with_options(DisplayCalendar::default()))
}
///|
pub impl Debug for PlainDate with fn to_repr(self) {
Repr::Repr(self.to_string())
}
///|
/// Creates a date without validating it.
fn PlainDate::new_unchecked(iso : IsoDate, calendar : Calendar) -> PlainDate {
{ iso, calendar }
}
///|
/// Creates a date in the ISO calendar, rejecting one that does not exist.
///
/// ```mbt check
/// test {
/// inspect(@temporal.PlainDate::try_new(2024, 2, 29), content="2024-02-29")
/// }
/// ```
pub fn PlainDate::try_new(
year : Int,
month : Int,
day : Int,
) -> PlainDate raise TemporalError {
PlainDate::new_with_overflow(year, month, day, Reject, Calendar::ISO)
}
///|
/// Creates a date, handling out-of-range fields according to `overflow`.
pub fn PlainDate::new_with_overflow(
year : Int,
month : Int,
day : Int,
overflow : Overflow,
calendar : Calendar,
) -> PlainDate raise TemporalError {
let iso = IsoDate::new_with_overflow(year, month, day, overflow)
{ iso, calendar }
}
///|
/// Creates a date from an already-validated ISO date.
pub fn PlainDate::from_iso(iso : IsoDate, calendar : Calendar) -> PlainDate {
{ iso, calendar }
}
///|
/// Returns the underlying ISO date.
pub fn PlainDate::iso(self : PlainDate) -> IsoDate {
self.iso
}
///|
/// Returns the calendar.
pub fn PlainDate::calendar(self : PlainDate) -> Calendar {
self.calendar
}
///|
/// Returns the year.
pub fn PlainDate::year(self : PlainDate) -> Int {
self.iso.year
}
///|
/// Returns the month, 1-based.
pub fn PlainDate::month(self : PlainDate) -> Int {
self.iso.month
}
///|
/// Returns the calendar-independent month code, such as `M02` for February.
pub fn PlainDate::month_code(self : PlainDate) -> String {
month_code_of(self.iso.month)
}
///|
/// Returns the month code for a 1-based ISO month.
fn month_code_of(month : Int) -> String {
if month < 10 {
"M0\{month}"
} else {
"M\{month}"
}
}
///|
/// Returns the day of the month.
pub fn PlainDate::day(self : PlainDate) -> Int {
self.iso.day
}
///|
/// Returns the ISO day of the week, Monday = 1 through Sunday = 7.
pub fn PlainDate::day_of_week(self : PlainDate) -> Int {
iso_day_of_week(self.iso.year, self.iso.month, self.iso.day)
}
///|
/// Returns the 1-based day of the year.
pub fn PlainDate::day_of_year(self : PlainDate) -> Int {
iso_day_of_year(self.iso.year, self.iso.month, self.iso.day)
}
///|
/// Returns the ISO week number.
pub fn PlainDate::week_of_year(self : PlainDate) -> Int {
iso_week_of_year(self.iso.year, self.iso.month, self.iso.day).0
}
///|
/// Returns the year the ISO week belongs to, which can differ from the
/// calendar year at either end of a year.
pub fn PlainDate::year_of_week(self : PlainDate) -> Int {
iso_week_of_year(self.iso.year, self.iso.month, self.iso.day).1
}
///|
/// Returns the number of days in the week, always 7 in the ISO calendar.
pub fn PlainDate::days_in_week(_self : PlainDate) -> Int {
7
}
///|
/// Returns the number of days in this month.
pub fn PlainDate::days_in_month(self : PlainDate) -> Int {
iso_days_in_month(self.iso.year, self.iso.month)
}
///|
/// Returns the number of days in this year.
pub fn PlainDate::days_in_year(self : PlainDate) -> Int {
iso_days_in_year(self.iso.year)
}
///|
/// Returns the number of months in this year, always 12 in the ISO calendar.
pub fn PlainDate::months_in_year(_self : PlainDate) -> Int {
12
}
///|
/// Returns whether this year is a leap year.
pub fn PlainDate::in_leap_year(self : PlainDate) -> Bool {
is_leap_year(self.iso.year)
}
///|
/// Compares two dates chronologically.
///
/// ```mbt check
/// test {
/// let a = @temporal.PlainDate::try_new(2024, 1, 1)
/// let b = @temporal.PlainDate::try_new(2024, 6, 1)
/// inspect(a.compare(b), content="-1")
/// }
/// ```
pub impl Compare for PlainDate with fn compare(self, other) {
self.iso.compare(other.iso)
}
///|
/// Returns whether two dates name the same day in the same calendar.
pub fn PlainDate::equals(self : PlainDate, other : PlainDate) -> Bool {
self.iso == other.iso && self.calendar == other.calendar
}
///|
/// Returns a copy with the given fields replaced.
///
/// ```mbt check
/// test {
/// let date = @temporal.PlainDate::try_new(2024, 3, 15)
/// inspect(date.with_fields(month=1), content="2024-01-15")
/// }
/// ```
pub fn PlainDate::with_fields(
self : PlainDate,
year? : Int,
month? : Int,
day? : Int,
overflow? : Overflow = Constrain,
) -> PlainDate raise TemporalError {
PlainDate::new_with_overflow(
year.unwrap_or(self.iso.year),
month.unwrap_or(self.iso.month),
day.unwrap_or(self.iso.day),
overflow,
self.calendar,
)
}
///|
/// Returns a copy in a different calendar.
pub fn PlainDate::with_calendar(
self : PlainDate,
calendar : Calendar,
) -> PlainDate {
{ iso: self.iso, calendar }
}
///|
/// `CalendarDateAdd` for the ISO calendar.
fn PlainDate::calendar_date_add(
self : PlainDate,
duration : DateDuration,
overflow : Overflow,
) -> PlainDate raise TemporalError {
let result = self.iso.add_date_duration(duration, overflow)
// The intermediate balancing allows one extra day at each end of the epoch
// range, so the result is re-validated as a date in its own right.
PlainDate::new_with_overflow(
result.year,
result.month,
result.day,
Reject,
self.calendar,
)
}
///|
/// Adds a duration to this date.
///
/// The duration's time components are truncated to whole days first, as
/// `AddDurationToDate` requires.
///
/// ```mbt check
/// test {
/// let jan31 = @temporal.PlainDate::try_new(2023, 1, 31)
/// let one_month = @temporal.Duration::of(months=1)
/// // February has no 31st, so `constrain` clamps to the end of the month.
/// inspect(jan31.add(one_month), content="2023-02-28")
/// }
/// ```
pub fn PlainDate::add(
self : PlainDate,
duration : Duration,
overflow? : Overflow = Constrain,
) -> PlainDate raise TemporalError {
self.calendar_date_add(
duration.to_date_duration_record_without_time(),
overflow,
)
}
///|
/// Subtracts a duration from this date.
pub fn PlainDate::subtract(
self : PlainDate,
duration : Duration,
overflow? : Overflow = Constrain,
) -> PlainDate raise TemporalError {
self.add(duration.negated(), overflow~)
}
///|
/// `DaysUntil`: the whole days from this date to `other`.
fn PlainDate::days_until(self : PlainDate, other : PlainDate) -> Int64 {
other.iso.to_epoch_days() - self.iso.to_epoch_days()
}
///|
/// `DifferenceDate`: the unrounded difference between two dates.
fn PlainDate::internal_diff_date(
self : PlainDate,
other : PlainDate,
largest_unit : DateTimeUnit,
) -> DateDuration raise TemporalError {
if self.iso == other.iso {
return DateDuration::default()
}
// A pure day difference needs no calendar walk.
if largest_unit is Day {
return DateDuration::new(0L, 0L, 0L, self.days_until(other))
}
self.iso.diff_iso_date(other.iso, largest_unit)
}
///|
/// `DifferenceTemporalPlainDate`: the rounded difference between two dates.
fn PlainDate::diff_date(
self : PlainDate,
operation : DifferenceOperation,
other : PlainDate,
settings : DifferenceSettings,
) -> Duration raise TemporalError {
if self.calendar != other.calendar {
raise RangeError("cannot compare dates in different calendars")
}
let resolved = ResolvedRoundingOptions::from_diff_settings(
settings,
operation,
UnitGroup::Date,
Day,
Day,
)
if self.iso == other.iso {
return duration_zero
}
let date_diff = self.internal_diff_date(other, resolved.largest_unit)
let mut duration = InternalDurationRecord::from_date_duration(date_diff)
// Rounding to whole days by one is exactly what the difference already
// produced, so the relative rounding pass can be skipped.
let rounding_is_noop = resolved.smallest_unit is Day &&
resolved.increment.get() == 1
if !rounding_is_noop {
let iso_date_time = IsoDateTime::new_unchecked(self.iso, iso_time_midnight)
let origin_epoch_ns = iso_date_time.as_nanoseconds()
let dest_epoch_ns = other.iso.as_nanoseconds()
let dt = PlainDateTime::new_unchecked(iso_date_time, self.calendar)
duration = duration.round_relative_duration(
origin_epoch_ns,
dest_epoch_ns,
dt,
None,
utc_only_provider,
resolved,
)
}
let result = Duration::from_internal(duration, Day)
match operation {
Until => result
Since => result.negated()
}
}
///|
/// Returns the duration from this date until `other`.
///
/// ```mbt check
/// test {
/// let a = @temporal.PlainDate::try_new(2020, 1, 1)
/// let b = @temporal.PlainDate::try_new(2024, 3, 15)
/// let settings = @temporal.DifferenceSettings::new(largest_unit=Year)
/// inspect(a.until(b, settings~), content="P4Y2M14D")
/// }
/// ```
pub fn PlainDate::until(
self : PlainDate,
other : PlainDate,
settings? : DifferenceSettings = DifferenceSettings::default(),
) -> Duration raise TemporalError {
self.diff_date(Until, other, settings)
}
///|
/// Returns the duration from `other` until this date.
pub fn PlainDate::since(
self : PlainDate,
other : PlainDate,
settings? : DifferenceSettings = DifferenceSettings::default(),
) -> Duration raise TemporalError {
self.diff_date(Since, other, settings)
}
///|
/// Combines this date with a time to produce a `PlainDateTime`.
///
/// Without a time, midnight is used.
pub fn PlainDate::to_plain_date_time(
self : PlainDate,
time? : PlainTime,
) -> PlainDateTime raise TemporalError {
let iso_time = match time {
Some(t) => t.iso
None => iso_time_midnight
}
PlainDateTime::from_iso(IsoDateTime::new(self.iso, iso_time), self.calendar)
}
///|
/// Renders the date as `YYYY-MM-DD`, with an optional calendar annotation.
pub fn PlainDate::to_string_with_options(
self : PlainDate,
display_calendar : DisplayCalendar,
) -> String {
let buf = StringBuilder::new()
write_date(buf, self.iso.year, self.iso.month, self.iso.day)
write_calendar_annotation(buf, self.calendar, display_calendar)
buf.to_string()
}
///|
/// Renders the date as `YYYY-MM-DD`.
pub fn PlainDate::to_string(self : PlainDate) -> String {
self.to_string_with_options(DisplayCalendar::default())
}