Skip to content

Add Microseconds and Nanoseconds to TimeStamp, DateTime, DateTimeOffset, and TimeOnly #23799

Closed

Description

@ChristopherHaws

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.

Activity

  1. svick commented on Oct 10, 2017

    @svick
    Member

    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.)

  2. tarekgh commented on Oct 10, 2017

    @tarekgh
    Member

    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.

  3. tarekgh commented on Oct 10, 2017

    @tarekgh
    Member

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

  4. ChristopherHaws commented on Oct 10, 2017

    @ChristopherHaws
    Author

    @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

  5. svick commented on Oct 11, 2017

    @svick
    Member

    @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.

  6. ChristopherHaws commented on Oct 11, 2017

    @ChristopherHaws
    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);
        }
    }
  7. tarekgh commented on Oct 11, 2017

    @tarekgh
    Member

    @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.

  8. Clockwork-Muse commented on Oct 11, 2017

    @Clockwork-Muse
    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 ?

  9. tarekgh commented on Oct 11, 2017

    @tarekgh
    Member

    @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.

  10. ChristopherHaws commented on Oct 11, 2017

    @ChristopherHaws
    Author

    @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);
        }
     }
  11. tarekgh commented on Oct 11, 2017

    @tarekgh
    Member

    @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?

  12. ChristopherHaws commented on Oct 11, 2017

    @ChristopherHaws
    Author

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

  13. tarekgh commented on Oct 11, 2017

    @tarekgh
    Member

    @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.

  14. iamcarbon commented on Oct 11, 2017

    @iamcarbon

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

  15. tarekgh commented on Oct 11, 2017

    @tarekgh
    Member

    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.

  16. 74 remaining items

  17. ChristopherHaws commented on Apr 5, 2022

    @ChristopherHaws
    Author

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

  18. tarekgh commented on Apr 5, 2022

    @tarekgh
    Member

    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.

  19. ChristopherHaws commented on Apr 5, 2022

    @ChristopherHaws
    Author

    @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) :)

  20. tarekgh commented on Apr 5, 2022

    @tarekgh
    Member

    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.

  21. deeprobin commented on Apr 5, 2022

    @deeprobin
    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

  22. tarekgh commented on Apr 5, 2022

    @tarekgh
    Member

    @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!

  23. ChristopherHaws commented on Apr 5, 2022

    @ChristopherHaws
    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!

  24. ghost added
    in-prThere is an active PR which will close this issue when it is merged
    on Apr 6, 2022
  25. ghost removed
    in-prThere is an active PR which will close this issue when it is merged
    on Apr 14, 2022
  26. tarekgh commented on Apr 14, 2022

    @tarekgh
    Member

    @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?

  27. deeprobin commented on Apr 14, 2022

    @deeprobin
    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

  28. ghost locked as resolved and limited conversation to collaborators on May 14, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions