///|
/// A year and a month with no day, such as "the October 2030 issue".
///
/// A reference day is kept internally so that arithmetic and comparison have
/// a concrete date to work from; it is never part of the type's identity.
///
/// Reference:
pub struct PlainYearMonth {
iso : IsoDate
calendar : Calendar
} derive(Eq)
///|
pub impl Show for PlainYearMonth with fn output(self, logger) {
logger.write_string(self.to_string_with_options(DisplayCalendar::default()))
}
///|
pub impl Debug for PlainYearMonth with fn to_repr(self) {
Repr::Repr(self.to_string())
}
///|
/// Creates a year-month, rejecting one outside the supported range.
///
/// `reference_day` chooses the internal reference day; it defaults to the
/// first of the month.
///
/// ```mbt check
/// test {
/// inspect(@temporal.PlainYearMonth::try_new(2030, 10), content="2030-10")
/// }
/// ```
pub fn PlainYearMonth::try_new(
year : Int,
month : Int,
reference_day? : Int = 1,
overflow? : Overflow = Reject,
calendar? : Calendar = Calendar::ISO,
) -> PlainYearMonth raise TemporalError {
if !year_month_within_limits(year, month) {
raise RangeError("year-month is outside the supported range")
}
let iso = IsoDate::regulate(year, month, reference_day, overflow)
{ iso, calendar }
}
///|
/// Returns the year.
pub fn PlainYearMonth::year(self : PlainYearMonth) -> Int {
self.iso.year
}
///|
/// Returns the month, 1-based.
pub fn PlainYearMonth::month(self : PlainYearMonth) -> Int {
self.iso.month
}
///|
/// Returns the calendar-independent month code.
pub fn PlainYearMonth::month_code(self : PlainYearMonth) -> String {
month_code_of(self.iso.month)
}
///|
/// Returns the calendar.
pub fn PlainYearMonth::calendar(self : PlainYearMonth) -> Calendar {
self.calendar
}
///|
/// Returns the number of days in this month.
pub fn PlainYearMonth::days_in_month(self : PlainYearMonth) -> Int {
iso_days_in_month(self.iso.year, self.iso.month)
}
///|
/// Returns the number of days in this year.
pub fn PlainYearMonth::days_in_year(self : PlainYearMonth) -> Int {
iso_days_in_year(self.iso.year)
}
///|
/// Returns the number of months in this year, always 12 in the ISO calendar.
pub fn PlainYearMonth::months_in_year(_self : PlainYearMonth) -> Int {
12
}
///|
/// Returns whether this year is a leap year.
pub fn PlainYearMonth::in_leap_year(self : PlainYearMonth) -> Bool {
is_leap_year(self.iso.year)
}
///|
/// Compares two year-months chronologically.
pub impl Compare for PlainYearMonth with fn compare(self, other) {
let c = self.iso.year.compare(other.iso.year)
if c != 0 {
c
} else {
self.iso.month.compare(other.iso.month)
}
}
///|
/// Returns whether both name the same month in the same calendar.
pub fn PlainYearMonth::equals(
self : PlainYearMonth,
other : PlainYearMonth,
) -> Bool {
self.iso == other.iso && self.calendar == other.calendar
}
///|
/// Returns a copy with the year or month replaced.
pub fn PlainYearMonth::with_fields(
self : PlainYearMonth,
year? : Int,
month? : Int,
overflow? : Overflow = Constrain,
) -> PlainYearMonth raise TemporalError {
PlainYearMonth::try_new(
year.unwrap_or(self.iso.year),
month.unwrap_or(self.iso.month),
reference_day=self.iso.day,
overflow~,
calendar=self.calendar,
)
}
///|
/// Combines this year-month with a day to produce a `PlainDate`.
pub fn PlainYearMonth::to_plain_date(
self : PlainYearMonth,
day : Int,
) -> PlainDate raise TemporalError {
PlainDate::new_with_overflow(
self.iso.year,
self.iso.month,
day,
Reject,
self.calendar,
)
}
///|
/// `AddDurationToYearMonth`: adds a duration of whole years and months.
///
/// Weeks, days and time components are rejected: a year-month has no day to
/// apply them to.
///
/// ```mbt check
/// test {
/// let ym = @temporal.PlainYearMonth::try_new(2024, 1)
/// inspect(ym.add(@temporal.Duration::of(months=13)), content="2025-02")
/// }
/// ```
pub fn PlainYearMonth::add(
self : PlainYearMonth,
duration : Duration,
overflow? : Overflow = Constrain,
) -> PlainYearMonth raise TemporalError {
let date_duration = duration.date_duration()
if date_duration.weeks != 0L ||
date_duration.days != 0L ||
duration.has_time_component() {
raise RangeError("only years and months can be added to a PlainYearMonth")
}
// The arithmetic is anchored to the first of the month, so a short target
// month cannot drag the result into the previous one. The anchor is
// range-checked: the first of the month can fall outside the supported range
// even when the year-month itself does not.
let anchor = IsoDate::new_with_overflow(
self.iso.year,
self.iso.month,
1,
Constrain,
)
let result = anchor.add_date_duration(date_duration, overflow)
PlainYearMonth::try_new(
result.year,
result.month,
overflow~,
calendar=self.calendar,
)
}
///|
/// Subtracts a duration.
pub fn PlainYearMonth::subtract(
self : PlainYearMonth,
duration : Duration,
overflow? : Overflow = Constrain,
) -> PlainYearMonth raise TemporalError {
self.add(duration.negated(), overflow~)
}
///|
/// `DifferenceTemporalPlainYearMonth`.
fn PlainYearMonth::diff(
self : PlainYearMonth,
operation : DifferenceOperation,
other : PlainYearMonth,
settings : DifferenceSettings,
) -> Duration raise TemporalError {
if self.calendar != other.calendar {
raise RangeError("cannot compare year-months in different calendars")
}
let resolved = ResolvedRoundingOptions::from_diff_settings(
settings,
operation,
UnitGroup::Date,
Year,
Month,
)
if resolved.largest_unit is (Week | Day) ||
resolved.smallest_unit is (Week | Day) {
raise RangeError("a year-month difference cannot use weeks or days")
}
if self.iso.year == other.iso.year && self.iso.month == other.iso.month {
return duration_zero
}
// Both sides are anchored to the first of their month so that the day
// components cancel out. As in `add`, the anchors are range-checked.
let start = IsoDate::new_with_overflow(
self.iso.year,
self.iso.month,
1,
Constrain,
)
let end = IsoDate::new_with_overflow(
other.iso.year,
other.iso.month,
1,
Constrain,
)
let date_diff = start.diff_iso_date(end, resolved.largest_unit)
let mut duration = InternalDurationRecord::from_date_duration(
DateDuration::new(date_diff.years, date_diff.months, 0L, 0L),
)
if !(resolved.smallest_unit is Month && resolved.increment.get() == 1) {
let iso_date_time = IsoDateTime::new_unchecked(start, iso_time_midnight)
let dt = PlainDateTime::new_unchecked(iso_date_time, self.calendar)
duration = duration.round_relative_duration(
iso_date_time.as_nanoseconds(),
IsoDateTime::new_unchecked(end, iso_time_midnight).as_nanoseconds(),
dt,
None,
utc_only_provider,
resolved,
)
}
let result = Duration::from_internal(duration, resolved.largest_unit)
match operation {
Until => result
Since => result.negated()
}
}
///|
/// Returns the duration from this year-month until `other`.
pub fn PlainYearMonth::until(
self : PlainYearMonth,
other : PlainYearMonth,
settings? : DifferenceSettings = DifferenceSettings::default(),
) -> Duration raise TemporalError {
self.diff(Until, other, settings)
}
///|
/// Returns the duration from `other` until this year-month.
pub fn PlainYearMonth::since(
self : PlainYearMonth,
other : PlainYearMonth,
settings? : DifferenceSettings = DifferenceSettings::default(),
) -> Duration raise TemporalError {
self.diff(Since, other, settings)
}
///|
/// Parses a year-month from an RFC 9557 string.
///
/// ```mbt check
/// test {
/// inspect(@temporal.PlainYearMonth::of_string("2030-10"), content="2030-10")
/// }
/// ```
pub fn PlainYearMonth::of_string(
source : String,
) -> PlainYearMonth raise TemporalError {
let record = parse_year_month_string(source)
let date = temporal_unwrap(record.date, "date component")
PlainYearMonth::try_new(
date.year,
date.month,
reference_day=date.day,
calendar=calendar_of(record),
)
}
///|
/// Renders the year-month as `YYYY-MM`.
///
/// The reference day is included when a calendar annotation is present, since
/// a non-ISO calendar needs it to reconstruct the same month.
pub fn PlainYearMonth::to_string_with_options(
self : PlainYearMonth,
display_calendar : DisplayCalendar,
) -> String {
let buf = StringBuilder::new()
write_year(buf, self.iso.year)
buf.write_string("-")
write_padded_2(buf, self.iso.month)
if display_calendar is (Always | Critical) || self.calendar != Calendar::ISO {
buf.write_string("-")
write_padded_2(buf, self.iso.day)
}
write_calendar_annotation(buf, self.calendar, display_calendar)
buf.to_string()
}
///|
/// Renders the year-month as `YYYY-MM`.
pub fn PlainYearMonth::to_string(self : PlainYearMonth) -> String {
self.to_string_with_options(DisplayCalendar::default())
}