-
Notifications
You must be signed in to change notification settings - Fork 97
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
Added an implementation of From<Time> for Duration #131
Conversation
Thanks for the PR! I'm planning to review tonight along with responding to the other open conversations. |
Trying to write a test for this I'm running into the same compile time ?Sized problem as in the Torque PR. Is there a problem with my test?
|
Line 278 in 8c5eb43
|
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.
Couple comments in the diff.
Did the + ?Sized
addition get your tests working?
src/si/time.rs
Outdated
V: ::num::Num + ::num::AsPrimitive<f64> + ::Conversion<V>, | ||
{ | ||
fn from(t: Time<U, V>) -> Duration { | ||
let secs = t.value.as_().abs(); |
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.
t.value
isn't guaranteed to be in seconds (see base.rs). from_base<D, U, V, N>(v)
can be used to convert from the Quantity
's base units into a specific unit (e.g. seconds).
As a slight aside from_base
calculates the result using the underlying storage type's conversion factor type and then converts back to the underlying storage type before returning. For f32
/f64
this doesn't make a difference since the underlying storage type and it's conversion factor type are the same. For integer types their conversion factor type is Ratio
. I'm wondering if it would make sense to have from_base
and related methods (or similar helper methods) return the conversion factor type which could allow this method to do a more precise conversion to Duration
(e.g. Ratio<V> -> u64
instead of V -> u64
).
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 also tried the obvious thing:
fn from(t: Time<U, V>) -> Duration {
let secs = t.get::<second>();
let nanos = t.get::<nanosecond>(); // NOTE: needs a modulo 1e9 here
Duration::new(secs as u64, nanos as u32)
}
but I haven't yet found a way to get it to compile. TBH I'm pretty frustrated with how hard even basic things are. I try to do a thing, and then I invariably do it wrong because I can't make sense of the current code. I tried calling from_base, but there were enough unsatisfied trait bound errors that my brain kinda switched off.
It might just be that I'm not good enough to contribute here, because this conversion ought to be the simplest thing in the world to implement.
src/si/time.rs
Outdated
{ | ||
fn from(t: Time<U, V>) -> Duration { | ||
let secs = t.value.as_().abs(); | ||
let nanos = (secs * 1e9) as u64 % 1e9 as u64; |
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 worried about overflow here. It would be good to find a way to extract the nanoseconds without multiplication. Does converting the original value into nanoseconds and then extracting the portion needed for Duration
work?
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.
Converting the original value to nanoseconds is a multiplication operation... there's no way around that part. Taking off the integer part before multiplying can solve any potential overflow problems: let nanos = secs.fract() * 1e9;
, but it doesn't solve any of the base problems in your other point...
It's been almost three years since I started the project and I still spend an excessive amount of time staring at compiler errors trying to figure things out so you're definitely not alone. I got the code compiling, but it doesn't pass tests. Now to find a correct, and hopefully efficient, algorithm to convert into a
diff --git a/src/si/time.rs b/src/si/time.rs
index 4f267a2..b6fbd8f 100644
--- a/src/si/time.rs
+++ b/src/si/time.rs
@@ -50,16 +50,19 @@ quantity! {
impl<U, V> From<Time<U, V>> for Duration
where
U: ::si::Units<V> + ?Sized,
- V: ::num::Num + ::num::AsPrimitive<f64> + ::Conversion<V>,
+ V: ::num::Num + ::num::AsPrimitive<u64> + ::num::AsPrimitive<u32> + ::Conversion<V>,
+ second: ::Conversion<V, T = V::T>,
+ nanosecond: ::Conversion<V, T = V::T>,
{
fn from(t: Time<U, V>) -> Duration {
- let secs = t.value.as_().abs();
- let nanos = (secs * 1e9) as u64 % 1e9 as u64;
- Duration::new(secs as u64, nanos as u32)
+ let secs = t.get::<second>().as_();
+ let nanos = (t % Time::<U, V>::new::<second>(V::one())).get::<nanosecond>().as_();
+
+ Duration::new(secs, nanos)
}
}
-#[cfg(all(test, feature = "std"))]
+#[cfg(test)]
mod tests {
storage_types! {
types: Float;
@@ -72,8 +75,9 @@ mod tests {
fn from() {
let t = Time::new::<second>(-21.5);
let d: Duration = t.into();
+
assert_eq!(21, d.as_secs());
- assert_eq!(5e8 as u32, d.subsec_nanos());
+ assert_eq!(500_000_000, d.subsec_nanos());
}
}
} |
Resuming work, the reason your test failed was because you didn't call Your "use quickcheck" idea is why the tests module was gated on std; I was copying from Length (but took out quickcheck during the compiler struggles). |
src/si/time.rs
Outdated
secs = V::zero() - secs; | ||
} | ||
let secs = secs.as_(); | ||
let t = if t < Time::<U, V>::new::<second>(V::zero()) { |
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 understand why this is here, but do we want to silently convert negative times to (inherently positive) durations? I suppose this might be a matter of "time vs. time interval."
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 think it's really a silent conversion, the user has to manually call into
in a context where the conversion is into an inherently positive type. I guess it's a question of which is least surprising:
- Absolute Value
- Wrapping Around
- Minimum 0
- panic!
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 know that I would prefer panicking, although I’d imagine that others would see it differently.
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.
Really? I just put that option there for completeness, From must not fail
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.
In the context of that documentation page, "fallible functions" seem to be loosely be "functions that return Result<T, E>
or Option<T>
:
When constructing a function that is capable of failing the return type will generally be of the form
Result<T, E>
.
As such, I read that note as simply meaning "From
does not return an error type." A more general argument for this interpretation is that it's almost nonsensical to say "this code must not panic" (with a couple of notable exceptions) in Safe Rust.
Panics signal an irrecoverable error, something that makes no sense and for which there's no sensible alternative route. In my mind, converting a negative time to a Duration
is such an error, and it makes sense to panic, as this likely signals a logic error. However, panicking from within a library crate is certainly not ideal.
I actually might argue, if possible, that we should use TryFrom
instead of From
.
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 TryFrom
vs From
is a different issue, there's certainly an argument to be made there. The trick is, doing that sort of error management (while certainly more rigorous) is enough of an ergonomics hit that if that's the direction uom
decides to go I'm going to just continue defining my own conversion functions where and as I need them. I'm not saying you're wrong, you're not, TryFrom
is definitely a more correct fit (even setting aside the absolute value question, NaN is sufficient to justify TryFrom
), it just feels cumbersome (and the implementation is going to be even more of a nightmare).
So with that out of the way, I think your reading of the note is wrong, and here's why. There's absolutely no point saying "From
does not return an error type", because there is no flexibility in from
's return type. There's simply no choice what you return. It would be warning us to make sure our code builds, or warning us to not write bugs into our code.
If you assume they added (and emphasized) that warning for a reason, there must be more to it than that. It must be more general than "don't do what you can't do". The reason they warn us not to let it fail is because From is integral to Rust's failure handling mechanisms, and it's not at all clear what should happen when failure handling fails.
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... I think I just accidentally convinced myself that this PR should be rejected on account of NaN and the infinities. sigh
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.
Not to beat a dead horse, but I still believe that the meaning for that documentation for From
is simply "the return value is not an error type." This is especially supported by the fact that you can impl From<A> for Option<B>
to mimic TryFrom
(and so there is indeed some flexibility in the return type). Regardless, I do think that TryFrom
is the better fit here. May I ask why From
is necessary for your use case/why TryFrom
is insufficient?
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 that the trait is insufficient on its own (I mean, it's a bit cumbersome to have to do yet more error handling, but at the end of the day that's not that big a deal), it's more that the difficulty of making the change is greater than the amount I care about it being made. I've already sunk 100x more time into this PR than its actually worth to me, and if I were to double that investment I still don't think that's enough to adequately implement TryFrom.
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 makes sense, thanks. I think I might try to make up a TryFrom
PR, fwiw. Thanks for your efforts on this.
assert_eq!(21, d.as_secs()); | ||
// assert_eq!(5e8 as u32, d.subsec_nanos()); | ||
assert_eq!(d.as_secs(), 21); | ||
assert!(499_999_999 <= d.subsec_nanos() && d.subsec_nanos() <= 500_000_001); |
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.
Is there a reason that we can't require this to be exactly 500,000,000?
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.
Precision loss when I subtract t from zero is my current best guess. The way I implemented it originally, by casting the time to f64 and then converting that to a Duration did allow this to be 500,000,000 exactly. Perhaps it's only the f32 test that loses precision?
This resolves a request in #58 (comment)