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

Adding Result/Error constructs to tzrs feature in spinoso-time #1927

Merged
merged 21 commits into from
Jul 11, 2022
Merged

Conversation

b-n
Copy link
Member

@b-n b-n commented Jun 29, 2022

#1902 Added a new backend - however some of the idiomatic principles weren't followed. Specifically there are some From impls which can panic - These sorts of things should be TryFrom as an example.

This PR fixes a number of these issues. Specifically it aims to achieve these:

  • ops would ideally be checked_add and checked_sub due to overflows
  • Offset::fixed() should be fallible (range +/- 246060)
  • Offset::from(&str) should be fallible
  • Offset::from(&str) tests should also test out of bound Regexs (e.g. +HH:MMM, HHMM, /HH-MM, +HHH:MM, +HH::MM)
  • Offset::from(&str) should not accept +0060, +2400 timezones. (they are out of range).
    - [ ] Attempt to unify the errors across chrono and tz-rs since things like ComponentOutOfRangeError might be sharable now (This makes more sense to try after we align some errors)
  • Use the more idiomatic TimeError instead of TimeErr

@b-n b-n self-assigned this Jun 29, 2022
@b-n b-n added S-wip Status: This pull request is a work in progress. A-ruby-core Area: Ruby Core types. C-quality Category: Refactoring, cleanup, and quality improvements. B-Artichoke Backend: Implementation of artichoke-core using Rust-native backend. labels Jun 29, 2022
@b-n
Copy link
Member Author

b-n commented Jun 29, 2022

@lopopolo I've done some digging, and it doesn't look like there are any "good" options for doing checked add on the datetimes. I was somewhat surprise there wasn't something like std::ops::CheckedAdd or anything similar. We can just add the Add trait, but something doesn't feel super right about this:

impl Add<T> for Time {
  type Output = Result<Self, Error>;
  fn try_from(value: T) -> Self::Output { }
}

This would end us in situations of

let now = Time::now();
let next = now + 1; // <-- next is a Result<Time, Error>, not a Time;

And I didn't try it until just now, but I see rust doesn't like function overloading (thank goodness), so this means just impl functions won't cut it.

My thoughts is implementing a trait:

pub trait CheckedAdd<T>: Sized {
    type Error;
    fn checked_add(value: T) -> Result<Self, Self::Error>;
}

Chrono currently uses num-traits for some of their additions of two times, however unfortuantely the implementation can only add the same type to the same type where as we are wanting to add a different type to another type.

Do you have any other bright[er] ideas than implementing this trait?

@lopopolo
Copy link
Member

lopopolo commented Jun 29, 2022

My thoughts is implementing a trait:

For these cases, I think we can just implement Time::checked_add and Time::checked_sub that take Time as regular methods. (and maybe similar methods that take f64, i64, and Duration?)

I don't think the complexity of a trait is worth it here, especially because in the trampolines for Time#add and similar, we do the unboxing to specific values, so if we are in the branch that unboxes a Float, we know we need to call Time::checked_add_f64, or something like that.

Then if we still want to implement std::ops::Add, the body of the trait impl can be time.checked_add(other).unwrap().

@b-n
Copy link
Member Author

b-n commented Jun 29, 2022

we can just implement Time::checked_add and Time::checked_sub that take Time

Funnily enough, it's not supported (in ruby 2.6.3, and also in 3.1.2):

irb(main):004:0> Time.at(1000) + Time.at(1000)
Traceback (most recent call last):
        6: from /home/ben/.rbenv/versions/2.6.3/bin/irb:23:in `<main>'
        5: from /home/ben/.rbenv/versions/2.6.3/bin/irb:23:in `load'
        4: from /home/ben/.rbenv/versions/2.6.3/lib/ruby/gems/2.6.0/gems/irb-1.0.0/exe/irb:11:in `<top (required)>'
        3: from (irb):4
        2: from (irb):4:in `rescue in irb_binding'
        1: from (irb):4:in `+'
TypeError (time + time?)

A couple of options:

  1. checked_add(value: Duration) - this is the type we can use for addition because it supports both seconds and nanos without float problems
  2. checked_add_duration(value: Duration) - slighly more explicit version of the above

I'm in favour of checked_add(value: Duration) - e.g. we force rust people to think of adding durations by default. Do you have a pref?

Either way, documenting here for posterity, but i'll add checked_[add|sub]_[i8|i16|i32|i64|u8|u16|u32|u64] like we currently are.

spinoso-time/src/time/tzrs/convert.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/offset/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/offset/error.rs Outdated Show resolved Hide resolved
Comment on lines 387 to 395
let invalid_fixed_strings = [
"+01:010", "+010:10", "+010:010", "0110", "01:10", "01-10", "+01-10", "+01::10",
];

for invalid_string in invalid_fixed_strings {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yay table-driven tests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hah yes! I was going to ask whether there is a style preference here. The same thing could be done with other tests and cheap hashmap tables (or a tuple lists i guess).

spinoso-time/src/time/tzrs/offset/mod.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/build.rs Outdated Show resolved Hide resolved
@lopopolo
Copy link
Member

irb(main):004:0> Time.at(1000) + Time.at(1000)

Add but Time subtraction is supported!

[3.1.2] > Time.now - Time.now
=> -2.0e-06

A couple of options:

  1. checked_add(value: Duration) - this is the type we can use for addition because it supports both seconds and nanos without float problems
  2. checked_add_duration(value: Duration) - slighly more explicit version of the above

I'm in favor of this, but with the caveat that spinoso-time should probably provide some free functions which convert i64 and f64 to core::time::Duration. It would be not fun to implement all of that conversion code in the trampolines.

@lopopolo
Copy link
Member

Either way, documenting here for posterity, but i'll add checked_[add|sub]_[i8|i16|i32|i64|u8|u16|u32|u64] like we currently are.

Let's impl these APIs in terms of Duration like you suggested, but make it easy for glue code in the trampolines to get a Duration from the primitive types. I guess that means these converter APIs would return Option<Duration> in case a negative value is passed?

So something like:

let float = ...;
let duration = spinoso_time::duration_from_f64(float)
    .or_else(|| {
        float.checked_neg().and_then(spinoso_time::duration_from_f64)
    })
    .ok_or_else(|| FloatDomainError::with_message("out of range")?;

Or maybe spinoso_time::duration_from_f64 returns an enum so callers can disambiguate when they need to call checked_add or checked_sub?

@b-n
Copy link
Member Author

b-n commented Jun 29, 2022

Let's impl these APIs in terms of Duration like you suggested, but make it easy for glue code in the trampolines to get a Duration from the primitive types. I guess that means these converter APIs would return Option in case a negative value is passed?

I've got one better I think (psuedo codeish):

fn checked_add(&self, value: Duration) -> Self {
  //magic
}

fn checked_add_f64(&self, value: f64) -> Self {
  let nanos = nanos_from_float(value);
  let duration = Duration::new(value.abs().floor(), nanos); 
  if value > 0 {
    self.checked_add(duration)
  } else {
    self.checked_sub(duration)
  }
}

@b-n b-n force-pushed the time-errors branch 2 times, most recently from ef35066 to bc0b159 Compare June 29, 2022 18:46
@lopopolo
Copy link
Member

this looks mostly good so far and I think you've got a solution to the ops problem in progress. I'm gonna step back on this PR. Once it's no longer draft, ping me and I'll take another look.

@b-n
Copy link
Member Author

b-n commented Jul 3, 2022

@lopopolo How do you feel about 58d30dc ?

(IMO, it needs a bit of work on the wording, but my brain is not turning up anything else noteworthy today)

@b-n b-n removed the S-wip Status: This pull request is a work in progress. label Jul 3, 2022
@b-n b-n requested a review from lopopolo July 3, 2022 13:44
@b-n b-n marked this pull request as ready for review July 3, 2022 14:18
@b-n b-n changed the title [WIP] Adding Result/Error constructs to tzrs feature in spinoso-time Adding Result/Error constructs to tzrs feature in spinoso-time Jul 6, 2022
b-n added 2 commits July 6, 2022 10:23
The previous changes I had were getting very specific and causing
addition unneeded complexity. Instead of abstracting across many
classes, pulling it all together into one place
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved

/// A wrapper around some of the errors provided by `tz-rs`.
#[allow(clippy::module_name_repetitions)]
#[derive(Debug)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can this enum derive Clone?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not unless we make some PRs into tz-rs. the wrapped tz-rs errors don't implement it at present unfortunately.

spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
let found_date_times = DateTime::find(year, month, day, hour, minute, second, nanoseconds, tz)?;

// .latest() will always return Some(DateTime)
let dt = found_date_times.latest().expect("No datetime found with this offset");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this expect guaranteed to not panic?

The docs for DateTime::find and its return type FoundDateTimeList indicate that the returned result may be empty:

https://docs.rs/tz-rs/latest/tz/datetime/struct.FoundDateTimeList.html

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I think that's true only when using .unique after having a look at the underlying implementation. .latest will always return a result that we're able to work with luckily enough!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

    /// Returns the latest found date time if existing
    pub fn latest(&self) -> Option<DateTime> {
        // Found date times are computed in ascending order of Unix times
        match *self.0.last()? {
            FoundDateTimeKind::Normal(date_time) => Some(date_time),
            FoundDateTimeKind::Skipped { after_transition, .. } => Some(after_transition),
        }
    }

that self.0.last()? means that if the vec in self.0 is empty, None is returned. We can leave this for now, but I'd love to maybe find a test case for this empty vec scenario.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hah, yes, good point!

I'll do some more digging through the generation and the tests there, there must be a use case where it does indeed fail. If it exists, I will find it 😄

spinoso-time/src/time/tzrs/mod.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/mod.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/mod.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/offset.rs Outdated Show resolved Hide resolved
b-n added 2 commits July 10, 2022 10:45
Co-authored-by: Ryan Lopopolo <rjl@hyperbo.la>

The removal of the checked_add_[i|u][8|16|32] is there since we can
leave this easily up to the caller.
Copy link
Member

@lopopolo lopopolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two missing . in some doc comments but this is good to go! Thanks for pushing this through. This is a final approval, merge whenever you feel comfortable (you don't need to kick it back to me if you fix the typos).

🎉

spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
spinoso-time/src/time/tzrs/error.rs Outdated Show resolved Hide resolved
Co-authored-by: Ryan Lopopolo <rjl@hyperbo.la>
@b-n b-n merged commit f32107a into trunk Jul 11, 2022
@b-n b-n deleted the time-errors branch July 11, 2022 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ruby-core Area: Ruby Core types. B-Artichoke Backend: Implementation of artichoke-core using Rust-native backend. C-quality Category: Refactoring, cleanup, and quality improvements.
Development

Successfully merging this pull request may close these issues.

None yet

2 participants