Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add warning against combining multiple Datelike::with_* #1199

Merged
merged 2 commits into from
Jul 28, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
159 changes: 152 additions & 7 deletions src/traits.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,33 @@
use crate::{IsoWeek, Weekday};

/// The common set of methods for date component.
///
/// Methods such as [`year`], [`month`], [`day`] and [`weekday`] can be used to get basic
/// information about the date.
///
/// The `with_*` methods can change the date.
///
/// # Warning
///
/// The `with_*` methods can be convenient to change a single component of a date, but they must be
/// used with some care. Examples to watch out for:
///
/// - [`with_year`] changes the year component of a year-month-day value. Don't use this method if
/// you want the ordinal to stay the same after changing the year, of if you want the week and
/// weekday values to stay the same.
/// - Don't combine two `with_*` methods to change two components of the date. For example to
/// change both the year and month components of a date. This could fail because an intermediate
/// value does not exist, while the final date would be valid.
///
/// For more complex changes to a date, it is best to use the methods on [`NaiveDate`] to create a
/// new value instead of altering an existing date.
///
/// [`year`]: Datelike::year
/// [`month`]: Datelike::month
/// [`day`]: Datelike::day
/// [`weekday`]: Datelike::weekday
/// [`with_year`]: Datelike::with_year
/// [`NaiveDate`]: crate::NaiveDate
pub trait Datelike: Sized {
/// Returns the year number in the [calendar date](./naive/struct.NaiveDate.html#calendar-date).
fn year(&self) -> i32;
Expand Down Expand Up @@ -55,37 +82,155 @@ pub trait Datelike: Sized {

/// Makes a new value with the year number changed, while keeping the same month and day.
///
/// Returns `None` when the resulting value would be invalid.
/// This method assumes you want to work on the date as a year-month-day value. Don't use it if
/// you want the ordinal to stay the same after changing the year, of if you want the week and
/// weekday values to stay the same.
///
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (February 29 in a non-leap year).
/// - The year is out of range for [`NaiveDate`].
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
///
/// [`NaiveDate`]: crate::NaiveDate
/// [`DateTime<Tz>`]: crate::DateTime
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Datelike};
///
/// assert_eq!(
/// NaiveDate::from_ymd_opt(2020, 5, 13).unwrap().with_year(2023).unwrap(),
/// NaiveDate::from_ymd_opt(2023, 5, 13).unwrap()
/// );
/// // Resulting date 2023-02-29 does not exist:
/// assert!(NaiveDate::from_ymd_opt(2020, 2, 29).unwrap().with_year(2023).is_none());
///
/// // Don't use `with_year` if you want the ordinal date to stay the same:
/// assert_ne!(
/// NaiveDate::from_yo_opt(2020, 100).unwrap().with_year(2023).unwrap(),
/// NaiveDate::from_yo_opt(2023, 100).unwrap() // result is 2023-101
/// );
/// ```
fn with_year(&self, year: i32) -> Option<Self>;

/// Makes a new value with the month number (starting from 1) changed.
///
/// Returns `None` when the resulting value would be invalid.
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `month(4)` when day of the month is 31).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `month` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
///
/// # Examples
///
/// ```
/// use chrono::{NaiveDate, Datelike};
///
/// assert_eq!(
/// NaiveDate::from_ymd_opt(2023, 5, 12).unwrap().with_month(9).unwrap(),
/// NaiveDate::from_ymd_opt(2023, 9, 12).unwrap()
/// );
/// // Resulting date 2023-09-31 does not exist:
/// assert!(NaiveDate::from_ymd_opt(2023, 5, 31).unwrap().with_month(9).is_none());
/// ```
///
/// Don't combine multiple `Datelike::with_*` methods. The intermediate value may not exist.
/// ```
/// use chrono::{NaiveDate, Datelike};
///
/// fn with_year_month(date: NaiveDate, year: i32, month: u32) -> Option<NaiveDate> {
/// date.with_year(year)?.with_month(month)
/// }
/// let d = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
/// assert!(with_year_month(d, 2019, 1).is_none()); // fails because of invalid intermediate value
///
/// // Correct version:
/// fn with_year_month_fixed(date: NaiveDate, year: i32, month: u32) -> Option<NaiveDate> {
/// NaiveDate::from_ymd_opt(year, month, date.day())
/// }
/// let d = NaiveDate::from_ymd_opt(2020, 2, 29).unwrap();
/// assert_eq!(with_year_month_fixed(d, 2019, 1), NaiveDate::from_ymd_opt(2019, 1, 29));
/// ```
fn with_month(&self, month: u32) -> Option<Self>;

/// Makes a new value with the month number (starting from 0) changed.
///
/// Returns `None` when the resulting value would be invalid.
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `month0(3)` when day of the month is 31).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `month0` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_month0(&self, month0: u32) -> Option<Self>;

/// Makes a new value with the day of month (starting from 1) changed.
///
/// Returns `None` when the resulting value would be invalid.
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `day(31)` in April).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `day` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_day(&self, day: u32) -> Option<Self>;

/// Makes a new value with the day of month (starting from 0) changed.
///
/// Returns `None` when the resulting value would be invalid.
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (for example `day0(30)` in April).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `day0` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_day0(&self, day0: u32) -> Option<Self>;

/// Makes a new value with the day of year (starting from 1) changed.
///
/// Returns `None` when the resulting value would be invalid.
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (`with_ordinal(366)` in a non-leap year).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `ordinal` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_ordinal(&self, ordinal: u32) -> Option<Self>;

/// Makes a new value with the day of year (starting from 0) changed.
///
/// Returns `None` when the resulting value would be invalid.
/// # Errors
///
/// Returns `None` when:
///
/// - The resulting date does not exist (`with_ordinal0(365)` in a non-leap year).
/// - In case of [`DateTime<Tz>`] if the resulting date and time fall within a timezone
/// transition such as from DST to standard time.
/// - The value for `ordinal0` is out of range.
///
/// [`DateTime<Tz>`]: crate::DateTime
fn with_ordinal0(&self, ordinal0: u32) -> Option<Self>;

/// Counts the days in the proleptic Gregorian calendar, with January 1, Year 1 (CE) as day 1.
Expand Down