#region Copyright notice and license // Protocol Buffers - Google's data interchange format // Copyright 2015 Google Inc. All rights reserved. // https://developers.google.com/protocol-buffers/ // // Redistribution and use in source and binary forms, with or without // modification, are permitted provided that the following conditions are // met: // // * Redistributions of source code must retain the above copyright // notice, this list of conditions and the following disclaimer. // * Redistributions in binary form must reproduce the above // copyright notice, this list of conditions and the following disclaimer // in the documentation and/or other materials provided with the // distribution. // * Neither the name of Google Inc. nor the names of its // contributors may be used to endorse or promote products derived from // this software without specific prior written permission. // // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #endregion using System; using System.Globalization; using System.Text; namespace Google.Protobuf.WellKnownTypes { public partial class Timestamp : ICustomDiagnosticMessage { private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); // Constants determined programmatically, but then hard-coded so they can be constant expressions. private const long BclSecondsAtUnixEpoch = 62135596800; internal const long UnixSecondsAtBclMaxValue = 253402300799; internal const long UnixSecondsAtBclMinValue = -BclSecondsAtUnixEpoch; internal const int MaxNanos = Duration.NanosecondsPerSecond - 1; private static bool IsNormalized(long seconds, int nanoseconds) => nanoseconds >= 0 && nanoseconds <= MaxNanos && seconds >= UnixSecondsAtBclMinValue && seconds <= UnixSecondsAtBclMaxValue; /// /// Returns the difference between one and another, as a . /// /// The timestamp to subtract from. Must not be null. /// The timestamp to subtract. Must not be null. /// The difference between the two specified timestamps. public static Duration operator -(Timestamp lhs, Timestamp rhs) { ProtoPreconditions.CheckNotNull(lhs, "lhs"); ProtoPreconditions.CheckNotNull(rhs, "rhs"); checked { return Duration.Normalize(lhs.Seconds - rhs.Seconds, lhs.Nanos - rhs.Nanos); } } /// /// Adds a to a , to obtain another Timestamp. /// /// The timestamp to add the duration to. Must not be null. /// The duration to add. Must not be null. /// The result of adding the duration to the timestamp. public static Timestamp operator +(Timestamp lhs, Duration rhs) { ProtoPreconditions.CheckNotNull(lhs, "lhs"); ProtoPreconditions.CheckNotNull(rhs, "rhs"); checked { return Normalize(lhs.Seconds + rhs.Seconds, lhs.Nanos + rhs.Nanos); } } /// /// Subtracts a from a , to obtain another Timestamp. /// /// The timestamp to subtract the duration from. Must not be null. /// The duration to subtract. /// The result of subtracting the duration from the timestamp. public static Timestamp operator -(Timestamp lhs, Duration rhs) { ProtoPreconditions.CheckNotNull(lhs, "lhs"); ProtoPreconditions.CheckNotNull(rhs, "rhs"); checked { return Normalize(lhs.Seconds - rhs.Seconds, lhs.Nanos - rhs.Nanos); } } /// /// Converts this timestamp into a . /// /// /// The resulting DateTime will always have a Kind of Utc. /// If the timestamp is not a precise number of ticks, it will be truncated towards the start /// of time. For example, a timestamp with a value of 99 will result in a /// value precisely on a second. /// /// This timestamp as a DateTime. /// The timestamp contains invalid values; either it is /// incorrectly normalized or is outside the valid range. public DateTime ToDateTime() { if (!IsNormalized(Seconds, Nanos)) { throw new InvalidOperationException(@"Timestamp contains invalid values: Seconds={Seconds}; Nanos={Nanos}"); } return UnixEpoch.AddSeconds(Seconds).AddTicks(Nanos / Duration.NanosecondsPerTick); } /// /// Converts this timestamp into a . /// /// /// The resulting DateTimeOffset will always have an Offset of zero. /// If the timestamp is not a precise number of ticks, it will be truncated towards the start /// of time. For example, a timestamp with a value of 99 will result in a /// value precisely on a second. /// /// This timestamp as a DateTimeOffset. /// The timestamp contains invalid values; either it is /// incorrectly normalized or is outside the valid range. public DateTimeOffset ToDateTimeOffset() { return new DateTimeOffset(ToDateTime(), TimeSpan.Zero); } /// /// Converts the specified to a . /// /// /// The Kind of is not DateTimeKind.Utc. /// The converted timestamp. public static Timestamp FromDateTime(DateTime dateTime) { if (dateTime.Kind != DateTimeKind.Utc) { throw new ArgumentException("Conversion from DateTime to Timestamp requires the DateTime kind to be Utc", "dateTime"); } // Do the arithmetic using DateTime.Ticks, which is always non-negative, making things simpler. long secondsSinceBclEpoch = dateTime.Ticks / TimeSpan.TicksPerSecond; int nanoseconds = (int) (dateTime.Ticks % TimeSpan.TicksPerSecond) * Duration.NanosecondsPerTick; return new Timestamp { Seconds = secondsSinceBclEpoch - BclSecondsAtUnixEpoch, Nanos = nanoseconds }; } /// /// Converts the given to a /// /// The offset is taken into consideration when converting the value (so the same instant in time /// is represented) but is not a separate part of the resulting value. In other words, there is no /// roundtrip operation to retrieve the original DateTimeOffset. /// The date and time (with UTC offset) to convert to a timestamp. /// The converted timestamp. public static Timestamp FromDateTimeOffset(DateTimeOffset dateTimeOffset) { // We don't need to worry about this having negative ticks: DateTimeOffset is constrained to handle // values whose *UTC* value is in the range of DateTime. return FromDateTime(dateTimeOffset.UtcDateTime); } internal static Timestamp Normalize(long seconds, int nanoseconds) { int extraSeconds = nanoseconds / Duration.NanosecondsPerSecond; seconds += extraSeconds; nanoseconds -= extraSeconds * Duration.NanosecondsPerSecond; if (nanoseconds < 0) { nanoseconds += Duration.NanosecondsPerSecond; seconds--; } return new Timestamp { Seconds = seconds, Nanos = nanoseconds }; } /// /// Converts a timestamp specified in seconds/nanoseconds to a string. /// /// /// If the value is a normalized duration in the range described in timestamp.proto, /// is ignored. Otherwise, if the parameter is true, /// a JSON object with a warning is returned; if it is false, an is thrown. /// /// Seconds portion of the duration. /// Nanoseconds portion of the duration. /// Determines the handling of non-normalized values /// The represented duration is invalid, and is false. internal static string ToJson(long seconds, int nanoseconds, bool diagnosticOnly) { if (IsNormalized(seconds, nanoseconds)) { // Use .NET's formatting for the value down to the second, including an opening double quote (as it's a string value) DateTime dateTime = UnixEpoch.AddSeconds(seconds); var builder = new StringBuilder(); builder.Append('"'); builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture)); Duration.AppendNanoseconds(builder, nanoseconds); builder.Append("Z\""); return builder.ToString(); } if (diagnosticOnly) { return string.Format(CultureInfo.InvariantCulture, "{{ \"@warning\": \"Invalid Timestamp\", \"seconds\": \"{0}\", \"nanos\": {1} }}", seconds, nanoseconds); } else { throw new InvalidOperationException("Non-normalized timestamp value"); } } /// /// Returns a string representation of this for diagnostic purposes. /// /// /// Normally the returned value will be a JSON string value (including leading and trailing quotes) but /// when the value is non-normalized or out of range, a JSON object representation will be returned /// instead, including a warning. This is to avoid exceptions being thrown when trying to /// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized /// values. /// /// A string representation of this value. public string ToDiagnosticString() { return ToJson(Seconds, Nanos, true); } } }