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
New TimeSpan.From overloads which take integers. #93890
Comments
Tagging subscribers to this area: @dotnet/area-system-datetime Issue DetailsSummary
This has, over time, led to repeated user confusion and bugs in the API surface that users have had to rationalize and deal with. It is also one of the less efficient ways to represent this data and makes it problematic for users wanting or expecting a particular behavior As such, it is proposed that new overloads be provided, allowing users to pass in integers so they can get the desired and intended behavior. It may be worth considering whether or not the API Proposalnamespace System;
public partial struct TimeSpan
{
public static TimeSpan FromDays(int days, int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0);
public static TimeSpan FromHours(int hours = 0, int minutes = 0, int seconds = 0, int milliseconds = 0, int microseconds = 0);
public static TimeSpan FromMinutes(int minutes, int seconds = 0, int milliseconds = 0, int microseconds = 0);
public static TimeSpan FromSeconds(int seconds, int milliseconds = 0, int microseconds = 0);
public static TimeSpan FromMilliseconds(int milliseconds, int microseconds = 0);
public static TimeSpan FromMicroseconds(int microseconds);
} Additional ConsiderationsIt may be desirable to not use default parameters. However, this provides a benefit that a user can do something like After these APIs are exposed, calling APIs where the user specifies a Exposing overloads that take
|
Are the constructors reasonable enough? If not, should the focus instead be on improving the constructors? |
@tannergooding did you mean to have the @vcsjones the idea here is having the users not providing a lot of value as the constructor requires. Having default parameter values will help. |
Nope. The first parameter should be non-defaultable. Fixed. Please feel free to mark
I think the constructors are worse in a lot of ways over static methods. For example, The explicit |
Really? I thought |
No. The exclusively integer based approach as proposed here is more accurate and likely to be faster for several scenarios. It likewise doesn't need to handle edge cases such as |
I'm pretty sure @tarekgh meant |
The consideration is you have Using int ensures there is no loss of data as you can simply upcast to long and then scale, since it will be in range of 2^63, you’re ultimately fine. You could, notably, avoid much of this data loss for the double overload by breaking the double into integral and fractional parts, casting the integral part to an integer, then scaling, then separately scaling by the fractional part and adding them together. However, this makes the code more expensive and results in a non trivial performance regression for which users don’t have an easy workaround. It likewise doesn’t solve the issue around dealing with the fractional data that is likely not as precise as what the user likely typed as a literal (given only powers of 2 and certain multiples of powers of 2 can be represented exactly) It’s an altogether tricky business and a good example of where the convenience factor doesn’t necessarily make something good. It shows how making a fix can then also be problematic due to people depending on the existing behavior or perf characteristics. |
... Sure, but all the internal representation is integer based, so that only applies to the constructor. |
@Clockwork-Muse, I'm unsure what you're trying to say or point you're making. Could you please clarify? What I'm stating is that These new functions address those various concerns and give a much friendlier way to express the data, both from a performance and usability perspective. |
Sorry, yes, I'm agreeing with you here. |
namespace System;
public partial struct TimeSpan
{
public static TimeSpan FromDays(int days);
public static TimeSpan FromDays(int days, int hours = 0, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0);
public static TimeSpan FromHours(int hours);
public static TimeSpan FromHours(int hours, long minutes = 0, long seconds = 0, long milliseconds = 0, long microseconds = 0);
public static TimeSpan FromMinutes(long minutes);
public static TimeSpan FromMinutes(long minutes, long seconds = 0, long milliseconds = 0, long microseconds = 0);
public static TimeSpan FromSeconds(long seconds);
public static TimeSpan FromSeconds(long seconds, long milliseconds = 0, long microseconds = 0);
public static TimeSpan FromMilliseconds(long milliseconds, long microseconds = 0);
public static TimeSpan FromMicroseconds(long microseconds);
} |
@bartonjs @tannergooding I'd be interested in working on this issue if it's still available. I have a few clarifying questions:
|
@tcortega thanks for willing to help with this one. You can work on it.
No, we are not touching these overloads nor deprecating it.
Yes, we are not touching the constructors.
No. analyzer work shouldn't block this issue. |
@tcortega are you going to work in this one? |
Yes, can you assign it to me please? |
@tarekgh I've submitted a preliminary pull request with my current modifications. It'd be great if you could review it and provide feedback on any issues. In the meantime, I'll be working on the test cases for the new methods. |
As an outsider: what is the purpose of special casing Is this a runtime optimization/inlining thing? If it is, I’d argue it should be called out in the source for future readers. |
I'd argue that it is due to allocations purpose but I simply attempted to copy the current structure of the private static TimeSpan IntervalFromDoubleTicks(double ticks)
{
if ((ticks > long.MaxValue) || (ticks < long.MinValue) || double.IsNaN(ticks))
ThrowHelper.ThrowOverflowException_TimeSpanTooLong();
if (ticks == long.MaxValue)
return MaxValue;
return new TimeSpan((long)ticks);
} |
@colejohnson66 You were right anyways, I refactored the code. |
@tarekgh In our prior conversation on PR #94143, you mentioned I should align the overflow check with the existing one in the constructor found at
However, it appears that the current validation is flawed, as demonstrated by the following inconsistency when using > new TimeSpan(2142483647, 0, 0, 0, 0, 0)
7443823.15:41:44.4838400
> TimeSpan.FromDays(2142483647)
┌───────────────────────Exception───────────────────────┐
│ TimeSpan overflowed because the duration is too long. │
└───────────────────────────────────────────────────────┘ |
@tcortega this is a bug we can fix. It doesn't mean we should follow this wrong behavior. |
I had a lot on my plate today, so I'm planning to knock this out tomorrow or the day after. |
Summary
TimeSpan
exposes severalFrom*
methods that allow users to create a timespan using adouble
. However,double
is a binary based floating-point format and thus has natural imprecision that can introduce error. For example,TimeSpan.FromSeconds(101.832)
is not101 seconds, 832 milliseconds
, but rather is101 seconds, 831.9999999999936335370875895023345947265625 milliseconds
.This has, over time, led to repeated user confusion and bugs in the API surface that users have had to rationalize and deal with. It is also one of the less efficient ways to represent this data and makes it problematic for users wanting or expecting a particular behavior
As such, it is proposed that new overloads be provided, allowing users to pass in integers so they can get the desired and intended behavior. It may be worth considering whether or not the
double
overloads should be "deprecated" (either via obsoletion or some analyzer) to help direct users towards success.API Proposal
Additional Considerations
It may be desirable to not use default parameters. However, this provides a benefit that a user can do something like
FromDays(5, seconds: 30)
if that were appropriate for their use case.After these APIs are exposed, calling
From(x)
wherex
was an integer may produce a different result in various edge cases.APIs where the user specifies a
uint
,long
, orulong
would still end up calling thedouble
overload due to the implicit conversion that exists in C#. Thus, an analyzer or deprecation of thedouble
overloads may be desirable to help users achieve success.Exposing overloads that take
decimal
could also be desirable and would avoid the issues around precision loss, but would be much less performant than breaking it apart like this.The text was updated successfully, but these errors were encountered: