1 #region Copyright notice and license 2 // Protocol Buffers - Google's data interchange format 3 // Copyright 2015 Google Inc. All rights reserved. 4 // https://developers.google.com/protocol-buffers/ 5 // 6 // Redistribution and use in source and binary forms, with or without 7 // modification, are permitted provided that the following conditions are 8 // met: 9 // 10 // * Redistributions of source code must retain the above copyright 11 // notice, this list of conditions and the following disclaimer. 12 // * Redistributions in binary form must reproduce the above 13 // copyright notice, this list of conditions and the following disclaimer 14 // in the documentation and/or other materials provided with the 15 // distribution. 16 // * Neither the name of Google Inc. nor the names of its 17 // contributors may be used to endorse or promote products derived from 18 // this software without specific prior written permission. 19 // 20 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 // "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 // LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 // A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 // OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 // SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 // LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 #endregion 32 33 using System; 34 using System.Globalization; 35 using System.Text; 36 37 namespace Google.Protobuf.WellKnownTypes 38 { 39 public partial class Timestamp : ICustomDiagnosticMessage 40 { 41 private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 42 // Constants determined programmatically, but then hard-coded so they can be constant expressions. 43 private const long BclSecondsAtUnixEpoch = 62135596800; 44 internal const long UnixSecondsAtBclMaxValue = 253402300799; 45 internal const long UnixSecondsAtBclMinValue = -BclSecondsAtUnixEpoch; 46 internal const int MaxNanos = Duration.NanosecondsPerSecond - 1; 47 IsNormalized(long seconds, int nanoseconds)48 private static bool IsNormalized(long seconds, int nanoseconds) => 49 nanoseconds >= 0 && 50 nanoseconds <= MaxNanos && 51 seconds >= UnixSecondsAtBclMinValue && 52 seconds <= UnixSecondsAtBclMaxValue; 53 54 /// <summary> 55 /// Returns the difference between one <see cref="Timestamp"/> and another, as a <see cref="Duration"/>. 56 /// </summary> 57 /// <param name="lhs">The timestamp to subtract from. Must not be null.</param> 58 /// <param name="rhs">The timestamp to subtract. Must not be null.</param> 59 /// <returns>The difference between the two specified timestamps.</returns> operator -(Timestamp lhs, Timestamp rhs)60 public static Duration operator -(Timestamp lhs, Timestamp rhs) 61 { 62 ProtoPreconditions.CheckNotNull(lhs, "lhs"); 63 ProtoPreconditions.CheckNotNull(rhs, "rhs"); 64 checked 65 { 66 return Duration.Normalize(lhs.Seconds - rhs.Seconds, lhs.Nanos - rhs.Nanos); 67 } 68 } 69 70 /// <summary> 71 /// Adds a <see cref="Duration"/> to a <see cref="Timestamp"/>, to obtain another <c>Timestamp</c>. 72 /// </summary> 73 /// <param name="lhs">The timestamp to add the duration to. Must not be null.</param> 74 /// <param name="rhs">The duration to add. Must not be null.</param> 75 /// <returns>The result of adding the duration to the timestamp.</returns> operator +(Timestamp lhs, Duration rhs)76 public static Timestamp operator +(Timestamp lhs, Duration rhs) 77 { 78 ProtoPreconditions.CheckNotNull(lhs, "lhs"); 79 ProtoPreconditions.CheckNotNull(rhs, "rhs"); 80 checked 81 { 82 return Normalize(lhs.Seconds + rhs.Seconds, lhs.Nanos + rhs.Nanos); 83 } 84 } 85 86 /// <summary> 87 /// Subtracts a <see cref="Duration"/> from a <see cref="Timestamp"/>, to obtain another <c>Timestamp</c>. 88 /// </summary> 89 /// <param name="lhs">The timestamp to subtract the duration from. Must not be null.</param> 90 /// <param name="rhs">The duration to subtract.</param> 91 /// <returns>The result of subtracting the duration from the timestamp.</returns> operator -(Timestamp lhs, Duration rhs)92 public static Timestamp operator -(Timestamp lhs, Duration rhs) 93 { 94 ProtoPreconditions.CheckNotNull(lhs, "lhs"); 95 ProtoPreconditions.CheckNotNull(rhs, "rhs"); 96 checked 97 { 98 return Normalize(lhs.Seconds - rhs.Seconds, lhs.Nanos - rhs.Nanos); 99 } 100 } 101 102 /// <summary> 103 /// Converts this timestamp into a <see cref="DateTime"/>. 104 /// </summary> 105 /// <remarks> 106 /// The resulting <c>DateTime</c> will always have a <c>Kind</c> of <c>Utc</c>. 107 /// If the timestamp is not a precise number of ticks, it will be truncated towards the start 108 /// of time. For example, a timestamp with a <see cref="Nanos"/> value of 99 will result in a 109 /// <see cref="DateTime"/> value precisely on a second. 110 /// </remarks> 111 /// <returns>This timestamp as a <c>DateTime</c>.</returns> 112 /// <exception cref="InvalidOperationException">The timestamp contains invalid values; either it is 113 /// incorrectly normalized or is outside the valid range.</exception> ToDateTime()114 public DateTime ToDateTime() 115 { 116 if (!IsNormalized(Seconds, Nanos)) 117 { 118 throw new InvalidOperationException(@"Timestamp contains invalid values: Seconds={Seconds}; Nanos={Nanos}"); 119 } 120 return UnixEpoch.AddSeconds(Seconds).AddTicks(Nanos / Duration.NanosecondsPerTick); 121 } 122 123 /// <summary> 124 /// Converts this timestamp into a <see cref="DateTimeOffset"/>. 125 /// </summary> 126 /// <remarks> 127 /// The resulting <c>DateTimeOffset</c> will always have an <c>Offset</c> of zero. 128 /// If the timestamp is not a precise number of ticks, it will be truncated towards the start 129 /// of time. For example, a timestamp with a <see cref="Nanos"/> value of 99 will result in a 130 /// <see cref="DateTimeOffset"/> value precisely on a second. 131 /// </remarks> 132 /// <returns>This timestamp as a <c>DateTimeOffset</c>.</returns> 133 /// <exception cref="InvalidOperationException">The timestamp contains invalid values; either it is 134 /// incorrectly normalized or is outside the valid range.</exception> ToDateTimeOffset()135 public DateTimeOffset ToDateTimeOffset() 136 { 137 return new DateTimeOffset(ToDateTime(), TimeSpan.Zero); 138 } 139 140 /// <summary> 141 /// Converts the specified <see cref="DateTime"/> to a <see cref="Timestamp"/>. 142 /// </summary> 143 /// <param name="dateTime"></param> 144 /// <exception cref="ArgumentException">The <c>Kind</c> of <paramref name="dateTime"/> is not <c>DateTimeKind.Utc</c>.</exception> 145 /// <returns>The converted timestamp.</returns> FromDateTime(DateTime dateTime)146 public static Timestamp FromDateTime(DateTime dateTime) 147 { 148 if (dateTime.Kind != DateTimeKind.Utc) 149 { 150 throw new ArgumentException("Conversion from DateTime to Timestamp requires the DateTime kind to be Utc", "dateTime"); 151 } 152 // Do the arithmetic using DateTime.Ticks, which is always non-negative, making things simpler. 153 long secondsSinceBclEpoch = dateTime.Ticks / TimeSpan.TicksPerSecond; 154 int nanoseconds = (int) (dateTime.Ticks % TimeSpan.TicksPerSecond) * Duration.NanosecondsPerTick; 155 return new Timestamp { Seconds = secondsSinceBclEpoch - BclSecondsAtUnixEpoch, Nanos = nanoseconds }; 156 } 157 158 /// <summary> 159 /// Converts the given <see cref="DateTimeOffset"/> to a <see cref="Timestamp"/> 160 /// </summary> 161 /// <remarks>The offset is taken into consideration when converting the value (so the same instant in time 162 /// is represented) but is not a separate part of the resulting value. In other words, there is no 163 /// roundtrip operation to retrieve the original <c>DateTimeOffset</c>.</remarks> 164 /// <param name="dateTimeOffset">The date and time (with UTC offset) to convert to a timestamp.</param> 165 /// <returns>The converted timestamp.</returns> FromDateTimeOffset(DateTimeOffset dateTimeOffset)166 public static Timestamp FromDateTimeOffset(DateTimeOffset dateTimeOffset) 167 { 168 // We don't need to worry about this having negative ticks: DateTimeOffset is constrained to handle 169 // values whose *UTC* value is in the range of DateTime. 170 return FromDateTime(dateTimeOffset.UtcDateTime); 171 } 172 Normalize(long seconds, int nanoseconds)173 internal static Timestamp Normalize(long seconds, int nanoseconds) 174 { 175 int extraSeconds = nanoseconds / Duration.NanosecondsPerSecond; 176 seconds += extraSeconds; 177 nanoseconds -= extraSeconds * Duration.NanosecondsPerSecond; 178 179 if (nanoseconds < 0) 180 { 181 nanoseconds += Duration.NanosecondsPerSecond; 182 seconds--; 183 } 184 return new Timestamp { Seconds = seconds, Nanos = nanoseconds }; 185 } 186 187 /// <summary> 188 /// Converts a timestamp specified in seconds/nanoseconds to a string. 189 /// </summary> 190 /// <remarks> 191 /// If the value is a normalized duration in the range described in <c>timestamp.proto</c>, 192 /// <paramref name="diagnosticOnly"/> is ignored. Otherwise, if the parameter is <c>true</c>, 193 /// a JSON object with a warning is returned; if it is <c>false</c>, an <see cref="InvalidOperationException"/> is thrown. 194 /// </remarks> 195 /// <param name="seconds">Seconds portion of the duration.</param> 196 /// <param name="nanoseconds">Nanoseconds portion of the duration.</param> 197 /// <param name="diagnosticOnly">Determines the handling of non-normalized values</param> 198 /// <exception cref="InvalidOperationException">The represented duration is invalid, and <paramref name="diagnosticOnly"/> is <c>false</c>.</exception> ToJson(long seconds, int nanoseconds, bool diagnosticOnly)199 internal static string ToJson(long seconds, int nanoseconds, bool diagnosticOnly) 200 { 201 if (IsNormalized(seconds, nanoseconds)) 202 { 203 // Use .NET's formatting for the value down to the second, including an opening double quote (as it's a string value) 204 DateTime dateTime = UnixEpoch.AddSeconds(seconds); 205 var builder = new StringBuilder(); 206 builder.Append('"'); 207 builder.Append(dateTime.ToString("yyyy'-'MM'-'dd'T'HH:mm:ss", CultureInfo.InvariantCulture)); 208 Duration.AppendNanoseconds(builder, nanoseconds); 209 builder.Append("Z\""); 210 return builder.ToString(); 211 } 212 if (diagnosticOnly) 213 { 214 return string.Format(CultureInfo.InvariantCulture, 215 "{{ \"@warning\": \"Invalid Timestamp\", \"seconds\": \"{0}\", \"nanos\": {1} }}", 216 seconds, 217 nanoseconds); 218 } 219 else 220 { 221 throw new InvalidOperationException("Non-normalized timestamp value"); 222 } 223 } 224 225 /// <summary> 226 /// Returns a string representation of this <see cref="Timestamp"/> for diagnostic purposes. 227 /// </summary> 228 /// <remarks> 229 /// Normally the returned value will be a JSON string value (including leading and trailing quotes) but 230 /// when the value is non-normalized or out of range, a JSON object representation will be returned 231 /// instead, including a warning. This is to avoid exceptions being thrown when trying to 232 /// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized 233 /// values. 234 /// </remarks> 235 /// <returns>A string representation of this value.</returns> ToDiagnosticString()236 public string ToDiagnosticString() 237 { 238 return ToJson(Seconds, Nanos, true); 239 } 240 } 241 } 242