-
Notifications
You must be signed in to change notification settings - Fork 226
Implement DateFromFieldsError, a narrow error type for Date::try_from_fields #7100
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
Conversation
Still one little test failure to debug but this should be mostly ready. I also want to add additional tests to make sure that the correct MonthCode error is returned.
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this approach, and fine grained errors are always good.
components/calendar/src/error.rs
Outdated
} | ||
|
||
/// Internal narrow error type for functions that only fail on invalid month codes | ||
pub(crate) struct InvalidMonthCodeError; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
praise: appreciate the comment that this is internal since if this ever were to be public it should probably be an enum with a single variant
missing_fields_strategy: Some(MissingFieldsStrategy::Reject), | ||
}; | ||
self.from_fields(fields, options) | ||
self.from_fields(fields, options).map_err(|e| match e { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
observation: ick, not super happy about needing this
suggestion: perhaps DateFromFieldsError can have a .to_date_error_for_konth_code(code)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I consider try_new_from_codes to be legacy at this point so I want to keep this remapping localized here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that's a good justification. refactoring the conversion into a separate method may still be nice
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added comments.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should be fairly simple to write a ArithmeticDate::from_codes
that does not use from_fields
. The code will be much simpler and the errors much cleaner.
impl<C: DateFieldsResolver + CalendarArithmetic> ArithmeticDate<C> {
pub(crate) fn from_codes(
era: Option<&str>,
year: i32,
month_code: types::MonthCode,
day: u8,
calendar: &C
) -> Result<Self, DateError> {
let year = if let Some(era) = era {
calendar.year_info_from_era(era, year)?
} else {
calendar.year_info_from_extended(year)
};
let month = calendar.ordinal_month_from_code(&year, month_code, Default::default())?;
range_check(day, "day", 1..=C::days_in_provided_month(year, month))?;
Ok(ArithmeticDate::new_unchecked(year, month, day))
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah... that's nicer, but I don't see a way to do it without implementing Calendar::from_codes
on every calendar. I really want to keep the number of ways to construct dates to a minimum. We already have 4 code paths (Calendar::from_fields, Calendar::from_iso, Calendar::from_rata_die, and the manual Date constructor). We don't need to add Calendar::from_codes back as a fifth code path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am also in favor of reusing from_fields in from_codes. Potential cleanup followup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you would need to implement from_codes with a single call in every calendar. I don't think that's the end of the world
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this conflicts with #7088
semver says:
These variants are new since 2.0. |
We should not block a 2.1-blocking PR on a non-2.1-blocking PR
autosubmit = true |
missing_fields_strategy: Some(MissingFieldsStrategy::Reject), | ||
}; | ||
self.from_fields(fields, options) | ||
self.from_fields(fields, options).map_err(|e| match e { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it should be fairly simple to write a ArithmeticDate::from_codes
that does not use from_fields
. The code will be much simpler and the errors much cleaner.
impl<C: DateFieldsResolver + CalendarArithmetic> ArithmeticDate<C> {
pub(crate) fn from_codes(
era: Option<&str>,
year: i32,
month_code: types::MonthCode,
day: u8,
calendar: &C
) -> Result<Self, DateError> {
let year = if let Some(era) = era {
calendar.year_info_from_era(era, year)?
} else {
calendar.year_info_from_extended(year)
};
let month = calendar.ordinal_month_from_code(&year, month_code, Default::default())?;
range_check(day, "day", 1..=C::days_in_provided_month(year, month))?;
Ok(ArithmeticDate::new_unchecked(year, month, day))
}
}
/// assert!(matches!(err, DateFromFieldsError::InvalidMonthCode)); | ||
/// ``` | ||
#[displaydoc("Invalid month code syntax")] | ||
InvalidMonthCode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see much value in distinguishing this from UnknownMonthCodeForCalendar
. We don't do this for eras either, "b"
is invalid era syntax, but we just return UnknownEra
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was unsure about this too: in my original comment I laid out the three types of invalid month code for full clarity, but I think error-wise we only need two, especially if future calendars add more types of month code.
I'm happy with having two error types. I don't really think InvalidMonthCode and UnknownMonthCodeForCalendar are differently actionable, whereas the third one very much is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A slight difference is that I return back the unknown month code in UnknownMonthCodeForCalendar
since it definitely fits in a TinyStr4, whereas InvalidMonthCode
could be something that doesn't fit in a TinyStr4.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, when I'm talking about "differently actionable" I mean whether or not the user will have a different set of actions to take based on that. The set of actions to take are more or less the same here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I removed the MonthCode field from the errors, so now there isn't a technical reason not to merge them. It's still easy in code for me to return these as different error variants. I'm mostly neutral on whether to fold them into one. I lean toward narrower variants; for example, if we merge these, should we also merge InconsistentYear and InconsistentMonth into InconsistentFields?
pub enum DateFromFieldsError { | ||
/// A field is out of range for its domain. | ||
#[displaydoc("{0}")] | ||
Range(RangeError), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please flatten to keep consistency with DateError
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consistency with DateError
is a non-goal. I think RangeError
is much better to keep as a sub-error because then it has its own methods and can be used as its own variable. Also, we can't add additional fields since enum variant structs are exhaustive. We very frequently change enum variant structs to standalone structs because of these reasons.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consistency with DateError is a non-goal
imho consistency within an API is always a goal.
"enum variant structs" can be non-exhaustive.
RangeError
does not have any methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think with the original RangeError the tradeoff was that matching on sub-structs is more annoying than matching on struct variants.
Though I suspect most users won't actually care to match on it?
I don't think exhaustiveness matters for this particular error.
Aside: it's unfortunate RangeError is public: I wish that field
were an enum not a string.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should clarify that the reason I see consistency with DateError as a non-goal is because I consider DateError and try_new_from_codes to be on the deprecation track.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DateError is also used by calendar-specific constructors. I also disagree about deprecating from_codes. And in any case this will only happen in the long term
components/calendar/src/error.rs
Outdated
/// assert!(matches!(err, DateFromFieldsError::UnknownMonthCodeForYear(_))); | ||
/// ``` | ||
#[displaydoc("Month code exists in calendar, but not for this year: {0:?}")] | ||
UnknownMonthCodeForYear(MonthCode), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not unknown, it's invalid
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CC @Manishearth for feedback on naming of the error variant
pub enum EcmaReferenceYearError { | ||
InvalidMonthCode, | ||
UnknownMonthCodeForCalendar(crate::types::MonthCode), | ||
NotEnoughFields, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
issue: this should be a range error (for days). NotEnoughFields
is not an error that can happen, as that method has non-optional month and day
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought it can happen if you attempt it with an ordinal month instead of a code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
icu4x/components/calendar/src/calendar_arithmetic.rs
Lines 683 to 689 in ef40c67
MissingFieldsStrategy::Ecma => { | |
if fields.extended_year.is_some() || fields.era_year.is_some() { | |
// The ECMAScript strategy is to pick day 1, always, regardless of whether | |
// that day exists for the month/year combo | |
1 | |
} else { | |
return Err(DateError::NotEnoughFields); |
icu4x/components/calendar/src/calendar_arithmetic.rs
Lines 715 to 720 in ef40c67
MissingFieldsStrategy::Ecma => { | |
match (fields.month_code, fields.ordinal_month) { | |
(Some(month_code), None) => { | |
cal.reference_year_from_month_day(month_code, day)? | |
} | |
_ => return Err(DateError::NotEnoughFields), |
yep.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this error is for the method
fn ecma_reference_year(
&self,
_month_code: MonthCode,
_day: u8,
) -> Result<i32, EcmaReferenceYearError>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and I guess also for reference_year_from_month_day
which has the same signature - all required fields are present
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd still like internal/unstable enums to have the correct variants, because they do influence the variants for public enums
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the only error here should be a bad month code.
even better!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd still like internal/unstable enums to have the correct variants, because they do influence the variants for public enums
Right, we should still strive for that, I was just observing that we don't need to perfectly design this enum just yet. We should still fix what we notice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NotEnoughFields
is reachable from the default impl of pub trait Rules
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll remove the InvalidMonthCode
variant when adding the internal ValidMonthCode type.
For now I'll leave NotEnoughFields
since it is how we signal that the function is "not implemented". Or, I could rename it to NotImplemented
, or I could make the Rules function return Option<Result>
.
}; | ||
fn ecma_reference_year( | ||
&self, | ||
month_code: types::MonthCode, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion: if we allow invalid month codes to be constructed, this method should take ordinal: u8, is_leap: bool
to remove the parsing error case (or introduce a new ValidMonthCode
type). once we move to &str
inputs we basically don't need MonthCode
anymore.
this also applies to all other (internal) methods that use MonthCode
. validate early so not every method needs a InvalidMonthCode
error variant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i cautiously like this idea, though I think it can be a non-2.1 blocking followup as a part of graduating these traits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it can only be non-blocking if we don't sprinkle MonthCode
in other parts of the API, like error variants, and the input itself
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you both are okay with adding icu_calendar::types::ValidMonthCode
, I will proceed with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm in favor of that. This issue is a mandatory 2.1 blocker so we can do things piecemeal
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Looked at the PR, not in favor of the PR, stated why there)
I think here we can have MonthCode, or a tuple, or an unstable lightweight ValidMonthCode
type. The latter two are more optimized, and we can change our mind when graduating the trait. We should note this thing as a decision-to-be-made on the graduation issue and move on.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a decision about unstable code and does not block this PR, nor should its fix involve public stable API changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can make ValidMonthCode pub(crate)/unstable in the other PR for now, and remove it from the error variant. I think in both cases where we convert to DateError, we have access to the input month code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, that works!
DateFromFieldsOptions::from_add_options(options), | ||
) | ||
.map_err(|e| { | ||
// TODO: Use a narrower error type here. For now, convert into DateError. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
observation: this is unstable code, so it's fine to use DateError
for now
mod calendar_arithmetic; | ||
mod duration; | ||
mod error; | ||
pub mod error; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just highlighting that I'm making this module public. It is in-scope of this PR since I am adding a new Error enum and I need a place to put it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LG with the try_parse change.
There are some bikesheddy questions which I moved into #7010 (comment) |
Shane set autosubmit |
#7010 This adds ValidMonthCode as an internal type. Suggested in #7100 --------- Co-authored-by: Robert Bastian <[email protected]>
#7010