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, IComparable<Timestamp> 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 /// Given another timestamp, returns 0 if the timestamps are equivalent, -1 if this timestamp precedes the other, and 1 otherwise 227 /// </summary> 228 /// <remarks> 229 /// Make sure the timestamps are normalized. Comparing non-normalized timestamps is not specified and may give unexpected results. 230 /// </remarks> 231 /// <param name="other">Timestamp to compare</param> 232 /// <returns>an integer indicating whether this timestamp precedes or follows the other</returns> CompareTo(Timestamp other)233 public int CompareTo(Timestamp other) 234 { 235 return other == null ? 1 236 : Seconds < other.Seconds ? -1 237 : Seconds > other.Seconds ? 1 238 : Nanos < other.Nanos ? -1 239 : Nanos > other.Nanos ? 1 240 : 0; 241 } 242 243 /// <summary> 244 /// Compares two timestamps and returns whether the first is less than (chronologically precedes) the second 245 /// </summary> 246 /// <remarks> 247 /// Make sure the timestamps are normalized. Comparing non-normalized timestamps is not specified and may give unexpected results. 248 /// </remarks> 249 /// <param name="a"></param> 250 /// <param name="b"></param> 251 /// <returns>true if a precedes b</returns> operator <(Timestamp a, Timestamp b)252 public static bool operator <(Timestamp a, Timestamp b) 253 { 254 return a.CompareTo(b) < 0; 255 } 256 257 /// <summary> 258 /// Compares two timestamps and returns whether the first is greater than (chronologically follows) the second 259 /// </summary> 260 /// <remarks> 261 /// Make sure the timestamps are normalized. Comparing non-normalized timestamps is not specified and may give unexpected results. 262 /// </remarks> 263 /// <param name="a"></param> 264 /// <param name="b"></param> 265 /// <returns>true if a follows b</returns> operator >(Timestamp a, Timestamp b)266 public static bool operator >(Timestamp a, Timestamp b) 267 { 268 return a.CompareTo(b) > 0; 269 } 270 271 /// <summary> 272 /// Compares two timestamps and returns whether the first is less than (chronologically precedes) the second 273 /// </summary> 274 /// <remarks> 275 /// Make sure the timestamps are normalized. Comparing non-normalized timestamps is not specified and may give unexpected results. 276 /// </remarks> 277 /// <param name="a"></param> 278 /// <param name="b"></param> 279 /// <returns>true if a precedes b</returns> operator <=(Timestamp a, Timestamp b)280 public static bool operator <=(Timestamp a, Timestamp b) 281 { 282 return a.CompareTo(b) <= 0; 283 } 284 285 /// <summary> 286 /// Compares two timestamps and returns whether the first is greater than (chronologically follows) the second 287 /// </summary> 288 /// <remarks> 289 /// Make sure the timestamps are normalized. Comparing non-normalized timestamps is not specified and may give unexpected results. 290 /// </remarks> 291 /// <param name="a"></param> 292 /// <param name="b"></param> 293 /// <returns>true if a follows b</returns> operator >=(Timestamp a, Timestamp b)294 public static bool operator >=(Timestamp a, Timestamp b) 295 { 296 return a.CompareTo(b) >= 0; 297 } 298 299 300 /// <summary> 301 /// Returns whether two timestamps are equivalent 302 /// </summary> 303 /// <remarks> 304 /// Make sure the timestamps are normalized. Comparing non-normalized timestamps is not specified and may give unexpected results. 305 /// </remarks> 306 /// <param name="a"></param> 307 /// <param name="b"></param> 308 /// <returns>true if the two timestamps refer to the same nanosecond</returns> operator ==(Timestamp a, Timestamp b)309 public static bool operator ==(Timestamp a, Timestamp b) 310 { 311 return ReferenceEquals(a, b) || (ReferenceEquals(a, null) ? (ReferenceEquals(b, null) ? true : false) : a.Equals(b)); 312 } 313 314 /// <summary> 315 /// Returns whether two timestamps differ 316 /// </summary> 317 /// <remarks> 318 /// Make sure the timestamps are normalized. Comparing non-normalized timestamps is not specified and may give unexpected results. 319 /// </remarks> 320 /// <param name="a"></param> 321 /// <param name="b"></param> 322 /// <returns>true if the two timestamps differ</returns> operator !=(Timestamp a, Timestamp b)323 public static bool operator !=(Timestamp a, Timestamp b) 324 { 325 return !(a == b); 326 } 327 328 /// <summary> 329 /// Returns a string representation of this <see cref="Timestamp"/> for diagnostic purposes. 330 /// </summary> 331 /// <remarks> 332 /// Normally the returned value will be a JSON string value (including leading and trailing quotes) but 333 /// when the value is non-normalized or out of range, a JSON object representation will be returned 334 /// instead, including a warning. This is to avoid exceptions being thrown when trying to 335 /// diagnose problems - the regular JSON formatter will still throw an exception for non-normalized 336 /// values. 337 /// </remarks> 338 /// <returns>A string representation of this value.</returns> ToDiagnosticString()339 public string ToDiagnosticString() 340 { 341 return ToJson(Seconds, Nanos, true); 342 } 343 } 344 } 345