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 Microseconds and Nanoseconds to TimeStamp, DateTime, DateTimeOffset, and TimeOnly #23799

Closed
Tracked by #64603
ChristopherHaws opened this issue Oct 10, 2017 · 82 comments · Fixed by #67666
Closed
Tracked by #64603
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-System.DateTime
Milestone

Comments

@ChristopherHaws
Copy link

ChristopherHaws commented Oct 10, 2017

Currently, the value of a single Tick in .NET is 100ns, however there is no API surface for accessing microseconds or nanoseconds other than using Ticks to manually calculate these numbers. In order to allow for working with these higher precision times, I am proposing the following new APIs:

Approved API

namespace System {
    public struct DateTime {
        /// <exception cref="ArgumentOutOfRangeException">When microsecond is not between 0 and 999.</exception>
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond);
        /// <exception cref="ArgumentOutOfRangeException">When microsecond is not between 0 and 999.</exception>
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.DateTimeKind kind);
        /// <exception cref="ArgumentOutOfRangeException">When microsecond is not between 0 and 999.</exception>
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.Globalization.Calendar calendar);
        /// <returns>The microseconds component, expressed as a value between 0 and 999.</returns>
        public int Microsecond { get; }
        /// <returns>The nanoseconds component, expressed as a value between 0 and 900.</returns>
        public int Nanosecond { get; }
        /// <exception cref="ArgumentOutOfRangeException">The resulting DateTime is less than DateTime.MinValue or greater than DateTime.MaxValue.</exception>
        public DateTime AddMicroseconds(double value);
    }
    public struct DateTimeOffset {
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.TimeSpan offset);
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, System.TimeSpan offset, System.Globalization.Calendar calendar);
        /// <returns>The microseconds component, expressed as a value between 0 and 999.</returns>
        public int Microsecond { get; }
        /// <returns>The nanoseconds component, expressed as a value between 0 and 900.</returns>
        public int Nanosecond { get; }
        /// <exception cref="ArgumentOutOfRangeException">The resulting DateTimeOffset is less than DateTimeOffset.MinValue or greater than DateTimeOffset.MaxValue.</exception>
        public DateTimeOffset AddMicroseconds(double value);
    }
    public struct TimeSpan {
        public const long TicksPerMicrosecond = 10L;
        public const long NanosecondsPerTick = 100L;
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds, int microseconds);
        /// <returns>The microseconds component, expressed as a value between 0 and 999.</returns>
        public int Microseconds { get; }
        /// <returns>The nanoseconds component, expressed as a value between 0 and 900.</returns>
        public int Nanoseconds { get; }
        /// <returns>The total number of microseconds represented by this instance.</returns>
        public double TotalMicroseconds { get; }
        /// <returns>The total number of nanoseconds represented by this instance.</returns>
        public double TotalNanoseconds { get; }
        /// <exception cref="OverflowException">When value is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue.</exception>
        public static TimeSpan FromMicroseconds(double microseconds);
    }
    public struct TimeOnly {
        /// <param name="microsecond">The microsecond (0 through 999).</param>
        /// <exception cref="ArgumentOutOfRangeException">When microsecond is not between 0 and 999.</exception>
        public TimeOnly(int day, int hour, int minute, int second, int millisecond, int microsecond);
        /// <returns>The microsecond component of the time represented by this instant, expressed as a value between 0 and 999.</returns>
        public int Microsecond { get; }
        /// <returns>The nanosecond component of the time represented by this instant, expressed as a value between 0 and 900.</returns>
        public int Nanosecond { get; }
    }
}

Original API Proposal

namespace System {
    public struct DateTime {
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds);
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, System.DateTimeKind kind);
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, System.Globalization.Calendar calendar);
        /// <returns>The microseconds component, expressed as a value between 0 and 999.</returns>
        public int Microsecond { get; }
        /// <returns>The nanoseconds component, expressed as a value between 0 and 900.</returns>
        public int Nanosecond { get; }
        /// <exception cref="ArgumentOutOfRangeException">The resulting DateTime is less than DateTime.MinValue or greater than DateTime.MaxValue.</exception>
        public DateTime AddMicroseconds(int microseconds);
    }
    public struct DateTimeOffset {
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, System.TimeSpan offset);
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, System.TimeSpan offset, System.Globalization.Calendar calendar);
        /// <returns>The microseconds component, expressed as a value between 0 and 999.</returns>
        public int Microsecond { get; }
        /// <returns>The nanoseconds component, expressed as a value between 0 and 900.</returns>
        public int Nanosecond { get; }
        /// <exception cref="ArgumentOutOfRangeException">The resulting DateTimeOffset is less than DateTimeOffset.MinValue or greater than DateTimeOffset.MaxValue.</exception>
        public DateTimeOffset AddMicroseconds(int microseconds);
    }
    public struct TimeSpan {
        public const long TicksPerMicrosecond = 10L;
        public const double TicksPerNanosecond = 0.01D;
        /// <exception cref="ArgumentOutOfRangeException">When microseconds is not between 0 and 999.</exception>
        public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds, int microseconds);
        /// <returns>The microseconds component, expressed as a value between 0 and 999.</returns>
        public int Microseconds { get; }
        /// <returns>The nanoseconds component, expressed as a value between 0 and 900.</returns>
        public int Nanoseconds { get; }
        /// <returns>The total number of microseconds represented by this instance.</returns>
        public double TotalMicroseconds { get; }
        /// <returns>The total number of nanoseconds represented by this instance.</returns>
        /// <exception cref="OverflowException">When value starts to approach TimeSpan.MinValue or TimeSpan.MaxValue.</exception>
        public double TotalNanoseconds { get; }
        /// <exception cref="OverflowException">When value is less than TimeSpan.MinValue or greater than TimeSpan.MaxValue.</exception>
        public static TimeSpan FromMicroseconds(double value);
    }
    public struct TimeOnly {
        /// <param name="microsecond">The microsecond (0 through 999).</param>
        /// <exception cref="ArgumentOutOfRangeException">When microsecond is not between 0 and 999.</exception>
        public TimeOnly(int day, int hour, int minute, int second, int millisecond, int microsecond);
        /// <returns>The microsecond component of the time represented by this instant, expressed as a value between 0 and 999.</returns>
        public int Microsecond { get; }
        /// <returns>The nanosecond component of the time represented by this instant, expressed as a value between 0 and 900.</returns>
        public int Nanosecond { get; }
    }
}

Example Usage

public class Samples
{
    public void DateTimeSamples()
    {
        new DateTime(0001, 01, 01, 00, 00, 00, 00, 999).Ticks;      // 9990
        DateTime.Parse("0001-01-01 00:00:00.0009990").Microsecond;  // 999
        DateTime.Parse("0001-01-01 00:00:00.0000009").Nanosecond;   // 900
        DateTime.Zero.AddMicroseconds(999).Ticks;                   // 9990
    }

    public void DateTimeOffsetSamples()
    {
        new DateTimeOffset(0001, 01, 01, 00, 00, 00, 00, 999, TimeSpan.FromHours(-7)).Ticks;        // 9990
        DateTimeOffset.Parse("0001-01-01 00:00:00.0009990 -7").Microsecond;                         // 999
        DateTimeOffset.Parse("0001-01-01 00:00:00.0000009 -7").Nanosecond;                          // 900
        new DateTimeOffset().AddMicroseconds(999).Ticks;                                            // 9990
    }

    public void TimeSpanSamples()
    {
        new TimeSpan(999 * TimeSpan.TicksPerMicrosecond).Ticks;            // 9990
        new TimeSpan((long)(900 * TimeSpan.TicksPerNanosecond)).Ticks;  // 9
        new TimeSpan(0001, 01, 01, 00, 00, 00, 00, 999).Ticks;          // 9990
        TimeSpan.Parse("0001-01-01 00:00:00.0009990").Microsecond;      // 999
        TimeSpan.Parse("0001-01-01 00:00:00.0000009").Nanosecond;       // 900
        TimeSpan.Zero.AddMicroseconds(999).Ticks;                       // 9990
    }
}

Notes

  • TimeSpan.TotalMicroseconds and TimeSpan.TotalNanoseconds return double still since that is what the other Total properties return. This could be long but I kept it as double for consistency.
  • TimeSpan.FromMicroseconds takes a double to keep the API consistent with the other From methods.
@svick
Copy link
Contributor

svick commented Oct 10, 2017

Is the common use case really to have separate values for seconds, milliseconds, microseconds and nanoseconds? Wouldn't it be better to instead expose a floating point number that's the number of seconds, including the fractional part?

I realize this would be inconsistent (seconds and milliseconds are already separate), but I think usefulness beats consistency.

Though some of the proposed members (like AddMicroseconds or TotalMicroseconds) would still make sense.

(Also, I'm not sure what exactly would be the right API to expose that.)

@tarekgh
Copy link
Member

tarekgh commented Oct 10, 2017

Is the common use case really to have separate values for seconds, milliseconds, microseconds and nanoseconds? Wouldn't it be better to instead expose a floating point number that's the number of seconds, including the fractional part?

If the caller has the values of seconds, milliseconds, microseconds, nanoseconds, would need to do some calculations to convert these values into the second fraction to be able to call the API. I think the request here is to help users not do the calculations themselves. users can achieve the result today by calculating the ticks from the second fractions. so I am seeing the proposed APIs just a helper methods.

@tarekgh
Copy link
Member

tarekgh commented Oct 10, 2017

@ChristopherHaws what is the need for the To/From Unix APIs? could you please clarify how these exactly get used?

@ChristopherHaws
Copy link
Author

ChristopherHaws commented Oct 10, 2017

@tarekgh Correct, there is no functional changes. It is just additional properties and methods for making it easier to work with microseconds and nanoseconds, which already exists in the Ticks. Here is a recent PR where I made some helper methods for doing just this: https://github.com/fluentassertions/fluentassertions/pull/669/files

I wasnt sure if unix has nanoseconds, but they do have the concept of microseconds, which is stored as a 64-bit unsigned integer. Here is a thread for the GO language where they are discussing use cases: golang/go#18935 (comment). One of the example mentioned (golang/go#18935 (comment)) is that there are many system calls in unix that now support microseconds and the standard protobuf timestamp uses nanoseconds.

Specificly TimeSpanConversionExtensions.cs and FluentDateTimeExtensions.cs

@svick
Copy link
Contributor

svick commented Oct 11, 2017

@tarekgh

If the caller has the values of seconds, milliseconds, microseconds, nanoseconds, would need to do some calculations to convert these values into the second fraction to be able to call the API.

I get that. My question is: how often does that happen? As an example, what's more likely, that the input is "3 minutes 14 seconds 159 milliseconds" or "3 minutes 14.159 seconds"? I think it's the latter and the current API does not handle that case well and the API proposed here does not improve it much either.

@ChristopherHaws
Copy link
Author

@svick I think that both ways are valid for different reasons. The way I proposed in the issue are simply extending the existing API's standard. If DateTime.Seconds had been a method instead, I would say there should be an overload that allows you to decide if it includes the higher precision values or not. I cant think of a good name, but something like might work better for what you are talking about:

namespace System {
    public struct DateTime {
        public double GetPart(DateTimePart.Seconds, DateTimePartOptions.IncludeHigherPrecision);
    }
}

@tarekgh
Copy link
Member

tarekgh commented Oct 11, 2017

@ChristopherHaws

  • I am not seeing a big value to have From/To Unix proposed APIs. if you agree, we can remove them from the proposal.
  • Also in DateTime/TimeSpan, the minimum unit we are using is the ticks (which is long value and not double). 1 Tick is 100 nanoseconds. so the proposed APIs that takes nanoseconds is kind doesn't make sense except if we ask users passing it as multiple of 100 nanoseconds. and that can complicate the APIs and make it inconsistent with the rest. Users can easily add ticks which covers adding 100 nanoseconds. I suggest removing all proposed APIs which is dealing with the nanoseconds from the proposal too.

please let me know what you think.

@Clockwork-Muse
Copy link
Contributor

I am not seeing a big value to have From/To Unix proposed APIs. if you agree, we can remove them from the proposal.

Um, but what about #23747 ?

@tarekgh
Copy link
Member

tarekgh commented Oct 11, 2017

@Clockwork-Muse for #23747, I am seeing a lot of people is using

new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)

this why #23747 would have a value. but for the proposed Unix APIs here, I am not seeing many people using it.

@ChristopherHaws
Copy link
Author

ChristopherHaws commented Oct 11, 2017

@tarekgh

  • I am ok with removing the From/To Unix API's. At some point I could see the value in having To/From UnixMicroseconds, but I would say that they are much less important than the rest of the helpers, so I will remove them from the proposal. If this is something that people need, it can be part of a separate proposal.
  • I can see the issues with adding nanoseconds (mainly needing to add in 100ns increments). I wish there were a better name that could be given to the method to make that more obvious, but I cant really think of one. Would a valid compromise be to keep the read apis, but remove the write apis? For example, keep Nanoseconds { get; } but remove FromNanoseconds(double value); and the constructors containing nanoseconds. I would be ok with this, but I still don't like that as a consumer of DateTime, if I want to add nanoseconds, I need to know how to convert between ticks and nanoseconds. For this reason, I would recommend we keep the TicksPerNanosecond const.
  • I fixed the doubles, thanks for pointing this out.
    • DateTime.Microsecond and DateTime.Nanosecond return int now since the values will always be between 0 and 999.
    • DateTime.AddMicroseconds takes an int now since the value must be between 0 and 999.
    • DateTimeOffset.Microsecond and DateTimeOffset.Nanosecond return int now since the values will always be between 0 and 999.
    • DateTimeOffset.AddMicroseconds takes an int now since the value must be between 0 and 999.
    • TimeSpan.Microseconds and TimeSpan.Nanoseconds return int now since the values will always be between 0 and 999.
    • TimeSpan.TotalMicroseconds and TimeSpan.TotalNanoseconds return double still since that is what the other total properties return. This could be long but I kept it as double for consistency. Let me know if I should change this.
    • TimeSpan.FromMicroseconds takes a double still to keep the api consistent with the other From methods.
    • TimeSpan.TicksPerNanosecond is a double since it is equal to 0.01d.

Removed from Proposal

namespace System {
    public struct DateTime {
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, int nanoseconds);
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, int nanoseconds, System.DateTimeKind kind);
        public DateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, int nanoseconds, System.Globalization.Calendar calendar);
        public System.DateTime AddNanoseconds(int nanoseconds);
    }
    public struct DateTimeOffset {
        public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, int nanoseconds, System.TimeSpan offset);
        public DateTimeOffset(int year, int month, int day, int hour, int minute, int second, int millisecond, int microseconds, int nanoseconds, System.TimeSpan offset, System.Globalization.Calendar calendar);
        public static System.DateTimeOffset FromUnixTimeMicroseconds(long microseconds);
        public static System.DateTimeOffset FromUnixTimeNanoseconds(long nanoseconds);
        public System.DateTimeOffset AddNanoseconds(int nanoseconds);
        public static long ToUnixTimeMicroseconds();
        public static long ToUnixTimeNanoseconds();
    }
    public struct TimeSpan {
        public static System.TimeSpan FromNanoseconds(double value);
        public TimeSpan(int days, int hours, int minutes, int seconds, int milliseconds, int microseconds, int nanoseconds);
    }
 }

@tarekgh
Copy link
Member

tarekgh commented Oct 11, 2017

@ChristopherHaws thanks for your updates. I'll try to look more get back to you if I have any other comment. could you please move the allowed values of the millisecond, Microseconds...etc. to the proposal, section to be clear about how the APIs will work and when we throw?

@ChristopherHaws
Copy link
Author

@tarekgh Added in the form of xml docs to the proposal. Thanks!

@tarekgh
Copy link
Member

tarekgh commented Oct 11, 2017

@iamcarbon I am seeing your reaction to the proposal. do you have any specific feedback to understand why you don't like the proposal? Thanks.

@iamcarbon
Copy link

@tarekgh DateTime is only accurate to 100 nanoseconds. It's confusing to have a property that cannot represent its full range.

@tarekgh
Copy link
Member

tarekgh commented Oct 11, 2017

It's confusing to have a property that cannot represent its full range.

I am not sure I fully understand this but DateTime has Ticks property that can reflect the ticks (in hundreds of nanoseconds). Also, it has MinValue and MaxValue properties which can tell the min and max of the supported range. if you are referring to a specific property in the proposal, please let me know so we can evaluate your feedback.

@ChristopherHaws
Copy link
Author

@tarekgh I believe he is referring to the fact that the Nanoseconds property would only ever return one of 10 values (0, 100, 200, 300, 400, 500, 600, 700, 800, 900).

I personally don't have any issue with this, as the alternative is that anyone who needs to know the amount of nanoseconds would have to calculate it from the ticks, but it should definitely be pointed out in the documentation. IMO, since DateTime does have nanoseconds in it's precision, a consumer should be able to access it as a property.

@tarekgh
Copy link
Member

tarekgh commented Oct 11, 2017

This is a limitation of DateTime. even if we were supporting nanoseconds we could get cases asking for femtoseconds :-)

At any case, we have to have a minimum unit we can support considering we want to keep DateTime size not exceeding long size for perf reasons.

@ChristopherHaws
Copy link
Author

@tarekgh I agree. I think he was just pointing out that the signature states that it returns an int between 0 and 900, but the values are not contiguous which might confuse some people that don't realize that DateTime's precision is 100ns. I think asking users to convert ticks manually is much more confusing and potentially error prone.

@danmoseley
Copy link
Member

@ChristopherHaws of course a hypothetical future runtime/platform could support a finer resolution with the same API, and I don't believe we would consider that a breaking change.

@ChristopherHaws
Copy link
Author

ChristopherHaws commented Oct 11, 2017

@danmosemsft True. That's another good reason for these helper methods/properties. If someone is making an assumption about the resolution of ticks, their code would break if that resolution changed from underneath them. With these helper methods, consumers can be sure that TimeSpan.Microseconds returns microseconds and not nanoseconds or picoseconds. :)

@tarekgh
Copy link
Member

tarekgh commented Oct 12, 2017

@joperezr this proposal looks good to me. if you agree, please mark the issue as ready for design review. Thanks.

@mattjohnsonpint
Copy link
Contributor

Be very careful with this. Exposing micros and nanos directly can easily lead to overflow exceptions or floating point errors.

For example, TimeSpan.MaxValue.TotalNanoseconds would give a double of 9.22337203685477E+20, which cannot be converted to a long without overflowing. Essentially this is the same as:

(long) (TimeSpan.MaxValue.TotalSeconds * 1000 * 1000 * 1000)  // -9223372036854775808

There's also a loss of precision with such extreme floating point values - which is counter-intuitive when working with APIs that promise nanoseconds.

I advise caution. We don't want people to think DateTime, DateTimeOffset and TimeSpan now have increased precision.

@ChristopherHaws
Copy link
Author

ChristopherHaws commented Oct 19, 2017

@mj1856 Agreed. I was thinking about this as well. From what I can see, TotalMicroseconds shouldn't have any issue, but TotalNanoseconds could have issues when you start approaching TimeSpan.Max and TimeSpan.Min. Please correct me if I am wrong about this. I will add an overflow exception to the original post so that it is not overlooked. If you have any thoughts on a better way to handle this, I am all ears! :)

@ChristopherHaws
Copy link
Author

@tarekgh Do you mean long? The only reason it returns double in the current proposal is because the other Total properties return double's.

@tarekgh
Copy link
Member

tarekgh commented Oct 19, 2017

@ChristopherHaws ignore my comment. I was looking at different place :-(

@ChristopherHaws
Copy link
Author

@mj1856 I was wrong, there is a potential for loss of precision with double even with microseconds. One alternative could be to use decimal since it's 128-bit. Thoughts?

void Main()
{
    TimeSpan.MaxValue.TotalMicroseconds();          // 9.22337203685478E+17
    TimeSpan.MaxValue.TotalMicrosecondsAsDecimal(); // 922337203685477580.7
    TimeSpan.MaxValue.TotalNanoseconds();           // 9.22337203685478E+20
    TimeSpan.MaxValue.TotalNanosecondsAsDecimal();  // 922337203685477580700
}

public static class TimeSpanEx
{
    public const Int64 TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
    public const Double TicksPerNanosecond = TicksPerMicrosecond / 1000d;

    public static Double TotalMicroseconds(this TimeSpan self) => self.Ticks * (1d / TicksPerMicrosecond);
    public static Decimal TotalMicrosecondsAsDecimal(this TimeSpan self) => self.Ticks * (1m / TicksPerMicrosecond);

    public static Double TotalNanoseconds(this TimeSpan self) => self.Ticks * (1d / TicksPerNanosecond);
    public static Decimal TotalNanosecondsAsDecimal(this TimeSpan self) => self.Ticks * (1m / (Decimal)TicksPerNanosecond);
}

@mattjohnsonpint
Copy link
Contributor

Well, the fact is that all three types in question already have their range locked in, and over that range one cannot represent nanoseconds in 64 bits without losing some of them. Floating point type allow for loss of precision, but one needs System.Numerics.BigInteger to truly work with nanoseconds without any loss.
We're also touching on why ticks go to 7 decimals and no further. 8 decimals and you start losing information over this range.

I'm not saying don't do this, I'm just saying check that the APIs can handle everything at all boundaries, especially with nanoseconds.

@joperezr
Copy link
Member

joperezr commented Oct 24, 2017

Video

Given the above concerns about overflow and precision, let's discuss a bit more on what would the right return type should be.

@tarekgh
Copy link
Member

tarekgh commented Oct 24, 2017

My take here:

  • for using Decimal type, I think double is covering the whole range. I am not sure if we are losing any precision here. @ChristopherHaws, when you tried to print the calculated result when using double, did you use G17 formatting?
  • We should have the documentation to warn the consumer if they want to use integral types (e.g long), this can cause overflow.

@ChristopherHaws
Copy link
Author

@tarekgh Here are the results using G17 formatting:

void Main()
{
    TimeSpan.MaxValue.TotalMicroseconds().ToString("G17", CultureInfo.InvariantCulture);            // 9.2233720368547763E+17
    TimeSpan.MaxValue.TotalMicrosecondsAsDecimal().ToString("G17", CultureInfo.InvariantCulture);   // 9.2233720368547758E+17
    TimeSpan.MaxValue.TotalNanoseconds().ToString("G17", CultureInfo.InvariantCulture);             // 9.2233720368547758E+20
    TimeSpan.MaxValue.TotalNanosecondsAsDecimal().ToString("G17", CultureInfo.InvariantCulture);    // 9.2233720368547758E+20
}

public static class TimeSpanEx
{
    public const Int64 TicksPerMicrosecond = TimeSpan.TicksPerMillisecond / 1000;
    public const Double TicksPerNanosecond = TicksPerMicrosecond / 1000d;

    public static Double TotalMicroseconds(this TimeSpan self) => self.Ticks * (1d / TicksPerMicrosecond);
    public static Decimal TotalMicrosecondsAsDecimal(this TimeSpan self) => self.Ticks * (1m / TicksPerMicrosecond);

    public static Double TotalNanoseconds(this TimeSpan self) => self.Ticks * (1d / TicksPerNanosecond);
    public static Decimal TotalNanosecondsAsDecimal(this TimeSpan self) => self.Ticks * (1m / (Decimal)TicksPerNanosecond);
}

@tarekgh
Copy link
Member

tarekgh commented Apr 5, 2022

@billknye good catch. I'll follow up to change the parameter name to microseconds. I'll need to review all Add methods in all types to ensure we are consistent at least inside the same type.

@deeprobin

I would be happy to take over the implementation of this issue.

We appreciate that. Do you think you can get this soon? I am asking because I would like to include these APIs in our next preview to have a chance of getting early feedback.

@tannergooding
Copy link
Member

Users, realistically, shouldn't expect or rely on nanosecond level precision and I doubt that will substantially change (outside extremely domain specific scenarios) in any near future.

Also noting that users shouldn't even really try to rely on 100ns level accuracy as the latency required to query the OS API is often up to 300ns. Its generally "safe"/"best" to try and target microsecond (at most) if you are wanting to do "high performance" work. There are many other considerations required even for accurate microsecond level accuracy, such as upping the process/thread priority and often opting into various multimedia light-up APIs.

@Thaina
Copy link

Thaina commented Apr 5, 2022

Do we have any plan for time that more precise than tick in any future?
I just wonder that the TimeOnly still have tick precision, while some system represent time in nanosec and we need to convert it to tick, which then lose their precision

Just because the underlying OS API returns nanoseconds doesn't mean the hardware actually supports nanoseconds. Most hardware/operating systems have about a 100-300ns latency on querying the hardware timer + normalizing it to account for frequency changes, cross-core queries, etc. Anything more precise and you start hitting latency issues with the actual instructions as well.

Nanosecond level accuracy requires a 1GHz timer and the ability to query and return that information on the CPU in the same amount of time. Considering that most CPUs are 2-3GHz (and the world record is 8.7GHz), this would require the ability to query the information from user mode and return it in under 10 cycles, which is less latency than cached memory access in many cases.

Users, realistically, shouldn't expect or rely on nanosecond level precision and I doubt that will substantially change (outside extremely domain specific scenarios) in any near future.

It's not about precision of the hardware but precision of the data we get from the system. To be honest I just get the time in nanosec from firebase database. Which manage their timestamp data in that precision

Arguably firebase also not really that precise on their time, they also always convert their timeStamp into C# DateTimeOffset. But they could use that time for ID and proof of edit. And I just thought that it might be possible to use that precise time as a data in database. It will become problem when try to query for it. Suppose there is scientist record nanosec precision data from LHC in database for example

@deeprobin
Copy link
Contributor

We appreciate that. Do you think you can get this soon? I am asking because I would like to include these APIs in our next > preview to have a chance of getting early feedback.

@tarekgh

I think this week should still be feasible.
I would start right away because I would like to consume this API as well since I had to 'workaround' this in another PR - See #64860 (comment).

@tannergooding
Copy link
Member

tannergooding commented Apr 5, 2022

Suppose there is scientist record nanosec precision data from LHC in database for example

.NET has a GC and likely isn't a good choice for things that require nanosecond level accuracy in direct measurement. A GC pause could end up causing all sorts of issues. Processing data with nanosecond level precision is different, but likewise needs special considerations (including in overflow)

If you're requiring nanosecond level precision, you are likely working on incredibly specialized hardware and without the overhead of a proper OS.

There is little need to go and invent something new and break existing code/assumptions (and it is breaking since TicksPerSecond is a hardcoded public constant) for hardware that the majority of users won't be running on and in a scenario where the extra precision is going to just be 0 or otherwise irrelevant.

@Thaina
Copy link

Thaina commented Apr 5, 2022

Suppose there is scientist record nanosec precision data from LHC in database for example

.NET has a GC and likely isn't a good choice for things that require nanosecond level accuracy in direct measurement. A GC pause could end up causing all sorts of issues. Processing data with nanosecond level precision is different, but likewise needs special considerations (including in overflow)

If you're requiring nanosecond level precision, you are likely working on incredibly specialized hardware and without the overhead of a proper OS.

There is little need to go and invent something new and break existing code/assumptions (and it is breaking since TicksPerSecond is a hardcoded public constant) for hardware that the majority of users won't be running on and in a scenario where the extra precision is going to just be 0 or otherwise irrelevant.

As I said it didn't meant it need to do something in realtime nanosec. But I mean the data that the app will process might have come with nanosec precision. The hardware might be anything in anywhere. It just that the data was recorded and come to us in that format. Such as public research data provided from scientist and we just get it as SQL record

Also I don't suggest that we need to change any existing code to support nanosec. I mean maybe we should add struct PreciseTime or something like that? I don't know, I just want to know what would be the solution, are there any plan about it?

@tannergooding
Copy link
Member

It would need to be an independent proposal. My own view is that its not worth exposing such domain specific functionality and that's better relegated to a community provide NuGet package or domain specific code solution.

You should feel free to open an API proposal and note that others may have their own view/opinion.

@deeprobin
Copy link
Contributor

deeprobin commented Apr 5, 2022

@tarekgh

The Calendar-constructors using Calender use ToDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond).

This is based on an abstract method which is Public API.
Thus the calendar overload would also need a new API in Calendar.

With the current API, our only option is to ignore the microsecond parameter in the constructor in conjunction with Calendar.

I would propose:

public virtual DateTime ToDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond, int era);

See

public abstract DateTime ToDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int era);

@ChristopherHaws ChristopherHaws changed the title Add Microseconds and Nanoseconds to TimeStamp, DateTime, and DateTimeOffset Add Microseconds and Nanoseconds to TimeStamp, DateTime, DateTimeOffset, and TimeOnly Apr 5, 2022
@tarekgh
Copy link
Member

tarekgh commented Apr 5, 2022

@deeprobin

Let's not block on that. We can just use ToDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond) and manually add the extra ticks passed in the microseconds parameter inside the DateTime constructor. This still will hold correct result.

@ChristopherHaws
Copy link
Author

public DateTimeOffset AddMicroseconds(double value);

@bartonjs The other DateTimeOffset methods use the unit name instead of value (its the opposite of DateTime). Should we switch this one back to microseconds for consistency?

public struct DateTimeOffset : IComparable, IComparable<DateTimeOffset>, IEquatable<DateTimeOffset>, IFormattable
{
    public DateTimeOffset AddMilliseconds(double milliseconds);
}

@tarekgh
Copy link
Member

tarekgh commented Apr 5, 2022

@ChristopherHaws this is already noted here #23799 (comment) and I am already following up. Let's go with public DateTimeOffset AddMilliseconds(double milliseconds); and will let you know if there will be any objection.

@ChristopherHaws
Copy link
Author

@tarekgh Perfect. Would you like me to update my original proposal with the changes? Thanks!

@tarekgh
Copy link
Member

tarekgh commented Apr 5, 2022

Would you like me to update my original proposal with the changes?

Sure, I appreciate that. Keeping the old one is good too for the record. I mean you can have 2 sections, one titled with Approved Proposal and the second titled with Original Proposal.

@ChristopherHaws
Copy link
Author

ChristopherHaws commented Apr 5, 2022

@tarekgh Also, in the review meeting, you talked about the overflow exception on TotalNanoseconds being wrong. Should this exception be removed as well?

public struct TimeSpan {
    /// <returns>The total number of nanoseconds represented by this instance.</returns>
    /// <exception cref="OverflowException">When value starts to approach TimeSpan.MinValue or TimeSpan.MaxValue.</exception>
    public double TotalNanoseconds { get; }
}

BTW, thanks for all your work on this issue. It's been 5 years in the making! I'm excited to get my hands on this in the next preview (hopefully) :)

@tarekgh
Copy link
Member

tarekgh commented Apr 5, 2022

in the review meeting, you talked about the overflow exception on TotalNanoseconds being wrong. Should this exception be removed as well?

Yes, please remove it. We'll have the triple slashes docs on the new APIs reviewed to match the final behavior.

BTW, thanks for all your work on this issue. It's been 5 years in the making! I'm excited to get my hands on this in the next preview (hopefully) :)

Thanks to you too and all community members contributed to this issue. I am excited too to see these new APIs in the product.

@deeprobin
Copy link
Contributor

Thanks to you too and all community members contributed to this issue. I am excited too to see these new APIs in the product.

@tarekgh

I have basically implemented the proposal. Only a few small things like tests and the reference assembly are missing. These will come tomorrow.

But if I may give you a little foretaste: My branch

@tarekgh
Copy link
Member

tarekgh commented Apr 5, 2022

@deeprobin cool!

I'll wait for the official PR. Take your time for adding the tests and doing any polishing there. Thanks for your help!

@ChristopherHaws
Copy link
Author

@deeprobin I added 2 comments to the commit I could see based on a very quick look through. I too will wait for the PR before doing an in depth review :)

Thanks!

@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Apr 6, 2022
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Apr 14, 2022
@tarekgh
Copy link
Member

tarekgh commented Apr 14, 2022

@deeprobin thanks for your comment dotnet/core#7378 (comment), that issue intended to list the preview 4 release notes and not for discussions. Could you move this comment here instead?

@deeprobin
Copy link
Contributor

@tarekgh done 👍🏼

@tarekgh With pleasure. It makes me happy every time to improve the .NET ecosystem a bit more. I now also follow up with yet another API for the next preview:

.NET Libraries: Use TimeSpan everywhere we use an int for seconds, milliseconds, and timeouts (Group 1/3) dotnet/runtime#64860

namespace System {
    public static class GC {
       public static GCNotificationStatus WaitForFullGCApproach(TimeSpan timeout);
       public static GCNotificationStatus WaitForFullGCComplete(TimeSpan timeout);
    }
}

namespace System.ComponentModel.DataAnnotations {
    public class RegularExpressionAttribute {
       public TimeSpan MatchTimeout { get; } 
    }
}

namespace System.Diagnostics {
    public class Process {
       public bool WaitForExit(TimeSpan timeout);
       public bool WaitForInputIdle(TimeSpan timeout);
    }
}

namespace System.IO {
    public class FileSystemWatcher {
       public WaitForChangedResult WaitForChanged(WatcherChangeTypes changeType, TimeSpan timeout);
    }

    public sealed class NamedPipeClientStream : PipeStream {
       public void Connect(TimeSpan timeout);
       public Task ConnectAsync(TimeSpan timeout, CancellationToken cancellationToken);
    }
}

namespace System.Net.NetworkInformation {
    public class Ping {
       public PingReply Send(IPAddress address, TimeSpan timeout, byte[]? buffer, PingOptions? options);
       public PingReply Send(string hostNameOrAddress, TimeSpan timeout, byte[]? buffer, PingOptions? options);
       public Task<PingReply> SendPingAsync(IPAddress address, TimeSpan timeout, byte[]? buffer, PingOptions? options, CancellationToken cancellationToken);
       public Task<PingReply> SendPingAsync(string hostNameOrAddress, TimeSpan timeout, byte[]? buffer, PingOptions? options, CancellationToken cancellationToken);
    }
}

namespace System.Net.Sockets {
    public class NetworkStream : Stream {
       public void Close(TimeSpan timeout);
    }

    public class Socket {
       public bool Poll(TimeSpan timeout, SelectMode mode);
       public static void Select(IList checkRead, IList checkWrite, IList checkError, TimeSpan timeout);
    }
}

namespace System.ServiceProcess {
    public class ServiceBase {
       public void RequestAdditionalTime(TimeSpan time);
    }
}

namespace System.Threading.Tasks {
    public class Task {
       public bool Wait(TimeSpan timeout, CancellationToken cancellationToken);
    } 
} 

namespace System.Timers {
    public class Timer {
       public Timer(TimeSpan interval);
    }
}

/cc @danmoseley @reflectronic

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-System.DateTime
Projects
None yet