-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathPortableMonotonicStamp.cs
692 lines (633 loc) · 32.6 KB
/
PortableMonotonicStamp.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using HpTimeStamps.BigMath;
using JetBrains.Annotations;
[assembly: InternalsVisibleTo("UnitTests")]
namespace HpTimeStamps
{
/// <summary>
/// A portable monotonic stamp suitable for use across process boundaries and
/// for serialization. Time is stored in UTC so any information about local time zone will
/// be lost.
///
/// Represented internally as a 128 bit signed integer that represents nanoseconds elapsed
/// since <see cref="DateTime.MinValue"/>, which is January 1, 0001 AD (UTC).
/// </summary>
/// <remarks> note on message suppression: although the struct is technically mutable, mutation
///can only occur during serialization and deserialization. Value changing mutation can only
///occur during deserialization. This type is essentially immutable: EVERY publicly exposed non-static method is readonly.
///Every static method / operator accepting values of this type accepts them by readonly-reference. Also,
///even if the struct WERE mutable, that would only be a problem if the value were to change
///WHILE IT IS STORED IN A SORTED OR HASH-BASED ORDERED COLLECTION. Since no commonly used collections
///return such types by mutable reference, you cannot change value while resides therein anyway.</remarks>
[DataContract]
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public struct PortableMonotonicStamp : IEquatable<PortableMonotonicStamp>,
IComparable<PortableMonotonicStamp>
{
#region Static Values
/// <summary>
/// Number of nanoseconds in a second
/// </summary>
public const long NanosecondsFrequency = 1_000_000_000;
/// <summary>
/// Minimum value representable
/// </summary>
public static ref readonly PortableMonotonicStamp MinValue => ref TheMinValue;
/// <summary>
/// Maximum value representable
/// </summary>
public static ref readonly PortableMonotonicStamp MaxValue => ref TheMaxValue;
/// <summary>
/// Portable stamp representing the unix epoch (Jan 1, 1970 @ midnight, UTC)
/// </summary>
public static ref readonly PortableMonotonicStamp UnixEpochStamp => ref TheUnixEpochStamp;
#endregion
#region Conversion Operators
/// <summary>
/// Convert a monotonic stamp to a portable stamp
/// </summary>
/// <param name="monotonicStamp">the monotonic stamp to convert</param>
/// <remarks>Will be roundtrippable (less timezone info ... will be made UTC)
/// unless the factors for conversion between monotonic stamps and nanoseconds are
/// not conducive to even division. <see cref="MonotonicStampContext.EasyConversionToAndFromNanoseconds"/>.</remarks>
public static explicit operator PortableMonotonicStamp(
in MonotonicTimeStamp<MonotonicStampContext> monotonicStamp) =>
MonoPortableConversionHelper<MonotonicStampContext>
.ConvertToPortableMonotonicStamp(monotonicStamp);
/// <summary>
/// Convert a date time to a portable timestamp
/// </summary>
/// <param name="convertMe">value to convert</param>
/// <returns>converted value</returns>
/// <remarks>If the source is a <see cref="DateTime"/>, will be roundtrippable back (with timezone locality stripped) </remarks>
public static implicit operator PortableMonotonicStamp(DateTime convertMe)
{
Int128 ticksSinceUtcDotNetEpoch =
((Int128)convertMe.ToUniversalTime().Ticks * 100) - MinValueUtcDtNanoseconds;
Debug.Assert(ticksSinceUtcDotNetEpoch >= MinValueUtcDtNanoseconds && ticksSinceUtcDotNetEpoch <= MaxValueUtcDtNanoseconds);
return new PortableMonotonicStamp(in ticksSinceUtcDotNetEpoch);
}
/// <summary>
/// Convert a monotonic stamp into a date time
/// </summary>
/// <param name="monotonicStamp"></param>
public static explicit operator DateTime(in PortableMonotonicStamp monotonicStamp)
{
if (monotonicStamp._dateTimeNanosecondOffsetFromMinValueUtc < MinValueUtcDtNanoseconds ||
monotonicStamp._dateTimeNanosecondOffsetFromMinValueUtc > MaxValueUtcDtNanoseconds)
{
throw new ArgumentOutOfRangeException(nameof(monotonicStamp), monotonicStamp,
"The portable monotonic stamp is too big or too small to be expressed as a datetime.");
}
Int128 elapsedSinceEpoch = (MinValueUtcDtNanoseconds + monotonicStamp._dateTimeNanosecondOffsetFromMinValueUtc) / 100;
Debug.Assert(elapsedSinceEpoch >= long.MinValue && elapsedSinceEpoch <= long.MaxValue);
return new DateTime((long)elapsedSinceEpoch, DateTimeKind.Utc);
}
#endregion
#region Parse Methods
/// <summary>
/// Parse a stringified portable monotonic stamp back to a stamp
/// </summary>
/// <param name="text">to convert back</param>
/// <returns>The portable stamp</returns>
/// <exception cref="ArgumentNullException"><paramref name="text"/> was null.</exception>
/// <exception cref="InvalidPortableStampStringException"><paramref name="text"/> could not be
/// deserialized to a <see cref="PortableMonotonicStamp"/>.</exception>
public static PortableMonotonicStamp Parse(string text) =>
Parse((text ?? throw new ArgumentNullException(nameof(text))).AsSpan());
/// <summary>
/// Parse a stringified portable monotonic stamp back to a stamp
/// </summary>
/// <param name="text">to convert back</param>
/// <returns>The portable stamp</returns>
/// <exception cref="InvalidPortableStampStringException"><paramref name="text"/> could not be
/// deserialized to a <see cref="PortableMonotonicStamp"/>.</exception>
public static PortableMonotonicStamp Parse(in ReadOnlySpan<char> text)
{
(DateTime dt, int nanoSec) = PortableTsParser.ParseStringifiedPortableStampToDtAndNano(in text);
return (PortableMonotonicStamp)dt +
PortableDuration.FromNanoseconds(nanoSec);
}
/// <summary>
/// Try to parse a stringified portable monotonic stamp back to a stamp
/// </summary>
/// <param name="text">to deserialize</param>
/// <returns>On success, a portable monotonic stamp. Null otherwise.</returns>
public static PortableMonotonicStamp? TryParse(string text)
{
try
{
return !string.IsNullOrWhiteSpace(text) ? Parse(text.AsSpan()) : null;
}
catch (InvalidPortableStampStringException)
{
return null;
}
}
/// <summary>
/// Try to parse a stringified portable monotonic stamp back to a stamp
/// </summary>
/// <param name="text">to deserialize</param>
/// <returns>On success, a portable monotonic stamp. Null otherwise.</returns>
public static PortableMonotonicStamp? TryParse(in ReadOnlySpan<char> text)
{
try
{
return Parse(in text);
}
catch (InvalidPortableStampStringException)
{
return null;
}
}
#endregion
/// <summary>
/// Amount of time in nanoseconds since the .NET UTC Epoch.
/// </summary>
/// <remarks>Value of .NET UTC epoch is calculated by calling <see cref="DateTime.ToUniversalTime"/>
/// on <see cref="DateTime.MinValue"/></remarks>
public readonly string NanosecondsSinceUtcEpoch =>
$"{_dateTimeNanosecondOffsetFromMinValueUtc:N} nanoseconds since epoch.";
/// <summary>
/// The year (A.D. 0001 - 9999)
/// </summary>
public readonly int Year => GetComponents().Year;
/// <summary>
/// The month (1-12)
/// </summary>
public readonly int Month => GetComponents().Month;
/// <summary>
/// The day (1-28, 1-29, 1-30, 1-31 depending on <see cref="Month"/>)
/// </summary>
public readonly int Day => GetComponents().Day;
/// <summary>
/// The hour (0-23)
/// </summary>
public readonly int Hour => GetComponents().Hour;
/// <summary>
/// Then minutes (0-59)
/// </summary>
public readonly int Minutes => GetComponents().Minute;
/// <summary>
/// The seconds (0-59)
/// </summary>
public readonly int Seconds => GetComponents().WholeSeconds;
/// <summary>
/// The fractional seconds (1 - 9,999,999)
/// </summary>
public readonly int FractionalSeconds => GetFractionalSeconds();
/// <summary>
/// Amount of time elapsed since epoch
/// </summary>
public readonly PortableDuration TimeSinceEpoch => new(in _dateTimeNanosecondOffsetFromMinValueUtc);
#region CTORS and related
internal PortableMonotonicStamp(in Int128 nanosecondSinceDtUtcEpoch)
{
if (nanosecondSinceDtUtcEpoch > MaxValueUtcDtNanoseconds ||
nanosecondSinceDtUtcEpoch < MinValueUtcDtNanoseconds)
throw new ArgumentOutOfRangeException(nameof(nanosecondSinceDtUtcEpoch),
nanosecondSinceDtUtcEpoch.ToString("N"),
"The offset supplied places this stamp outside the supported range of .NET time.");
_dateTimeNanosecondOffsetFromMinValueUtc = nanosecondSinceDtUtcEpoch;
_serialized = null;
}
static PortableMonotonicStamp()
{
MaxValueUtcDtNanoseconds = DateTime.MaxValue.ToUniversalTime().Ticks * (Int128)100;
MinValueUtcDtNanoseconds = DateTime.MinValue.ToUniversalTime().Ticks * (Int128)100;
TheMinValue = new PortableMonotonicStamp(MinValueUtcDtNanoseconds);
TheMaxValue = new PortableMonotonicStamp(MaxValueUtcDtNanoseconds);
TheUnixEpochStamp = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
Validate();
}
[Conditional("DEBUG")] // DEBUG they are used // RELEASE the method doesn't get called
[SuppressMessage("ReSharper", "RedundantAssignment")]
static void Validate()
{
Debug.Assert(PortableDuration.TicksPerSecond == 1_000_000_000);
Debug.Assert(TimeSpan.TicksPerSecond == 10_000_000);
Int128 minValueNanoseconds = MinValue._dateTimeNanosecondOffsetFromMinValueUtc;
Int128 maxValueNanoseconds = MaxValue._dateTimeNanosecondOffsetFromMinValueUtc;
Int128 toTimeSpanTicksMin = minValueNanoseconds / 100;
Int128 toTimeSpanTicksMax = maxValueNanoseconds / 100;
Debug.Assert(toTimeSpanTicksMin >= long.MinValue);
Debug.Assert(toTimeSpanTicksMax <= long.MaxValue);
DateTime min = new DateTime((long)toTimeSpanTicksMin, DateTimeKind.Utc);
DateTime max = new DateTime((long)toTimeSpanTicksMax, DateTimeKind.Utc);
Debug.Assert(min == DateTime.MinValue.ToUniversalTime() && max ==
DateTime.MaxValue.ToUniversalTime());
}
#endregion
#region Public Methods
/// <summary>
/// Convert this to a datetime in the local datetime timezone.
/// </summary>
/// <returns>A local datetime</returns>
/// <exception cref="PortableTimestampOverflowException">Cannot be converted to
/// DateTime because of overflow.</exception>
[System.Diagnostics.Contracts.Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly DateTime ToLocalDateTime()
{
try
{
return ToUtcDateTime().ToLocalTime();
}
catch (ArgumentOutOfRangeException inner)
{
throw new PortableTimestampOverflowException(
"Overflow prevented conversion of portable monotonic stamp to a local DateTime.", inner);
}
}
/// <summary>
/// Convert this into a utc datetime
/// </summary>
/// <returns>a utc datetime</returns>
/// <exception cref="PortableTimestampOverflowException">Cannot be converted to
/// DateTime because of overflow.</exception>
[System.Diagnostics.Contracts.Pure]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly DateTime ToUtcDateTime()
{
try
{
return (DateTime)this;
}
catch (ArgumentOutOfRangeException inner)
{
throw new PortableTimestampOverflowException("Overflow prevented conversion of portable monotonic stamp to a UTC DateTime.", inner);
}
}
/// <summary>
/// Get a string representation of this value in ISO 8601 format
/// in UTC.
/// </summary>
/// <returns>A string representation.</returns>
public override readonly string ToString() => BuildString(false);
/// <summary>
/// Get a string representation of this value in ISO 8601 format
/// in LOCAL.
/// </summary>
/// <returns>A string representation.</returns>
public readonly string ToLocalString() => BuildString(true);
#endregion
#region Equality/Comparison and Related Methods and Operators
/// <summary>
/// Test two portable monotonic stamps for value equality
/// </summary>
/// <param name="lhs">left hand operand</param>
/// <param name="rhs">right hand operand</param>
/// <returns>true if they have the same value, false otherwise</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator ==(in PortableMonotonicStamp lhs, in PortableMonotonicStamp rhs) =>
lhs._dateTimeNanosecondOffsetFromMinValueUtc == rhs._dateTimeNanosecondOffsetFromMinValueUtc;
/// <summary>
/// Test two portable monotonic stamps for value inequality
/// </summary>
/// <param name="lhs">left hand operand</param>
/// <param name="rhs">right hand operand</param>
/// <returns>true if they have different values, false otherwise</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator !=(in PortableMonotonicStamp lhs, in PortableMonotonicStamp rhs) =>
!(lhs == rhs);
/// <summary>
/// Test two portable monotonic stamps to see if <paramref name="lhs"/> is greater than
/// <paramref name="rhs"/>.
/// </summary>
/// <param name="lhs">left hand operand</param>
/// <param name="rhs">right hand operand</param>
/// <returns>true if <paramref name="lhs"/> is greater than
/// <paramref name="rhs"/>, false otherwise</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator >(in PortableMonotonicStamp lhs, in PortableMonotonicStamp rhs) =>
lhs._dateTimeNanosecondOffsetFromMinValueUtc > rhs._dateTimeNanosecondOffsetFromMinValueUtc;
/// <summary>
/// Test two portable monotonic stamps to see if <paramref name="lhs"/> is less than
/// <paramref name="rhs"/>.
/// </summary>
/// <param name="lhs">left hand operand</param>
/// <param name="rhs">right hand operand</param>
/// <returns>true if <paramref name="lhs"/> is less than
/// <paramref name="rhs"/>, false otherwise</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator <(in PortableMonotonicStamp lhs, in PortableMonotonicStamp rhs) =>
lhs._dateTimeNanosecondOffsetFromMinValueUtc < rhs._dateTimeNanosecondOffsetFromMinValueUtc;
/// <summary>
/// Test two portable monotonic stamps to see if <paramref name="lhs"/> is greater or equal to than
/// <paramref name="rhs"/>.
/// </summary>
/// <param name="lhs">left hand operand</param>
/// <param name="rhs">right hand operand</param>
/// <returns>true if <paramref name="lhs"/> is greater than
/// <paramref name="rhs"/>, false otherwise</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator >=(in PortableMonotonicStamp lhs, in PortableMonotonicStamp rhs) =>
!(lhs < rhs);
/// <summary>
/// Test two portable monotonic stamps to see if <paramref name="lhs"/> is less or equal to than
/// <paramref name="rhs"/>.
/// </summary>
/// <param name="lhs">left hand operand</param>
/// <param name="rhs">right hand operand</param>
/// <returns>true if <paramref name="lhs"/> is less than or equal to
/// <paramref name="rhs"/>, false otherwise</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool operator <=(in PortableMonotonicStamp lhs, in PortableMonotonicStamp rhs) =>
!(lhs > rhs);
/// <summary>
/// A value based hash code for this value
/// </summary>
/// <returns>a hash code</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public override readonly int GetHashCode() => _dateTimeNanosecondOffsetFromMinValueUtc.GetHashCode();
/// <summary>
/// Check to see if this value has the same value as another object
/// </summary>
/// <param name="obj">the other object</param>
/// <returns>True if the other object is a <see cref="PortableMonotonicStamp"/>
/// that has the same value as this one. False otherwise
/// </returns>
public override readonly bool Equals(object obj) => obj is PortableMonotonicStamp pmts && pmts == this;
/// <summary>
/// Test to see whether this value is the same as another value
/// of the same type.
/// </summary>
/// <param name="other">the other value</param>
/// <returns>True if the other value has the same value as this one, false otherwise.</returns>
public readonly bool Equals(PortableMonotonicStamp other) => other == this;
/// <summary>
/// Compare this value to another value of the same type to establish the ordering relation between them.
/// </summary>
/// <param name="other">the other object</param>
/// <returns>
/// Zero if this value and <paramref name="other"/> have identical position within sort order.
/// A positive number if this value succeeds <paramref name="other"/> in the sort order.
/// A negative number if this value precedes <paramref name="other"/> in the sort order.
/// </returns>
public readonly int CompareTo(PortableMonotonicStamp other) => Compare(in this, in other);
/// <summary>
/// Compare two <see cref="PortableMonotonicStamp"/> values to establish the ordering between them.
/// </summary>
/// <param name="lhs">Left hand operand</param>
/// <param name="rhs">Right hand operand</param>
/// <returns>
/// Zero if <paramref name="lhs"/> and <paramref name="rhs"/> have identical position within sort order.
/// A positive number if <paramref name="lhs"/> succeeds <paramref name="rhs"/> in the sort order.
/// A negative number if <paramref name="lhs"/> precedes <paramref name="rhs"/> in the sort order.
/// </returns>
public static int Compare(in PortableMonotonicStamp lhs, in PortableMonotonicStamp rhs)
{
if (lhs == rhs) return 0;
return lhs > rhs ? 1 : -1;
}
#endregion
#region Arithmetic operators
/// <summary>
/// Adds a duration to a stamp, yielding a stamp
/// </summary>
/// <param name="ms">the stamp addend</param>
/// <param name="d">the duraton addend</param>
/// <returns>the sum</returns>
/// <exception cref="PortableTimestampOverflowException">Caused overflow</exception>
public static PortableMonotonicStamp operator +(in PortableMonotonicStamp ms, in PortableDuration d)
{
Int128 sum = ms._dateTimeNanosecondOffsetFromMinValueUtc + d._ticks;
if (sum >= MinValueUtcDtNanoseconds && sum <= MaxValueUtcDtNanoseconds)
{
return new PortableMonotonicStamp(in sum);
}
throw new PortableTimestampOverflowException("The sum caused overflow.");
}
/// <summary>
/// Adds a duration to a stamp, yielding a stamp
/// </summary>
/// <param name="ms">the stamp addend</param>
/// <param name="d">the duraton addend</param>
/// <returns>the sum</returns>
/// <exception cref="PortableTimestampOverflowException">Caused overflow</exception>
public static PortableMonotonicStamp operator +(in PortableDuration d, in PortableMonotonicStamp ms)
=> ms + d;
/// <summary>
/// Adds a timespan to a stamp, yielding a stamp
/// </summary>
/// <param name="ms">the stamp addend</param>
/// <param name="ts">the timespan addend</param>
/// <returns>the sum</returns>
/// <exception cref="PortableTimestampOverflowException">Caused overflow</exception>
public static PortableMonotonicStamp operator +(in PortableMonotonicStamp ms, TimeSpan ts)
{
Int128 sum = ms._dateTimeNanosecondOffsetFromMinValueUtc + ((Int128)ts.Ticks * 100);
if (sum >= MinValueUtcDtNanoseconds && sum <= MaxValueUtcDtNanoseconds)
{
return new PortableMonotonicStamp(in sum);
}
throw new PortableTimestampOverflowException("The sum caused overflow.");
}
/// <summary>
/// Adds a duration to a stamp, yielding a stamp
/// </summary>
/// <param name="ms">the stamp addend</param>
/// <param name="ts">the timespan addend</param>
/// <returns>the sum</returns>
/// <exception cref="PortableTimestampOverflowException">Caused overflow</exception>
public static PortableMonotonicStamp operator +(TimeSpan ts, in PortableMonotonicStamp ms)
=> ms + ts;
/// <summary>
/// Subtracts a duration from a stamp, yielding a stamp
/// </summary>
/// <param name="minuend">Minuend</param>
/// <param name="subtrahend">the subtrahend</param>
/// <returns>the sum</returns>
/// <exception cref="PortableTimestampOverflowException">Caused overflow</exception>
public static PortableMonotonicStamp operator -(in PortableMonotonicStamp minuend, in PortableDuration subtrahend)
{
//issue 22 bug fix (+ -> -)
Int128 sum = minuend._dateTimeNanosecondOffsetFromMinValueUtc - subtrahend._ticks;
if (sum >= MinValueUtcDtNanoseconds && sum <= MaxValueUtcDtNanoseconds)
{
return new PortableMonotonicStamp(in sum);
}
throw new PortableTimestampOverflowException("The sum caused overflow.");
}
/// <summary>
/// Subtracts a timespan from a stamp, yielding a stamp
/// </summary>
/// <param name="minuend">Minuend</param>
/// <param name="subtrahend">the subtrahend</param>
/// <returns>the sum</returns>
/// <exception cref="PortableTimestampOverflowException">Caused overflow</exception>
public static PortableMonotonicStamp operator -(in PortableMonotonicStamp minuend, TimeSpan subtrahend)
{
Int128 difference = minuend._dateTimeNanosecondOffsetFromMinValueUtc - ((Int128)subtrahend.Ticks * 100);
if (difference >= MinValueUtcDtNanoseconds && difference <= MaxValueUtcDtNanoseconds)
{
return new PortableMonotonicStamp(in difference);
}
throw new PortableTimestampOverflowException("The subtraction caused overflow.");
}
/// <summary>
/// Subtracts a stamp from another stamp yielding the duration between them.
/// </summary>
/// <param name="minuend">the minuend</param>
/// <param name="subtrahend">the subtrahend</param>
/// <returns>the duration between the stamps</returns>
public static PortableDuration operator -(in PortableMonotonicStamp minuend,
in PortableMonotonicStamp subtrahend)
{
Int128 ticksDiff = minuend._dateTimeNanosecondOffsetFromMinValueUtc -
subtrahend._dateTimeNanosecondOffsetFromMinValueUtc;
return new PortableDuration(in ticksDiff);
}
/// <summary>
/// Subtracts a DateTime from a stamp yielding the duration between them.
/// </summary>
/// <param name="minuend">the minuend</param>
/// <param name="subtrahend">the subtrahend</param>
/// <returns>the duration between the stamps</returns>
public static PortableDuration operator -(in PortableMonotonicStamp minuend,
DateTime subtrahend) => minuend - (PortableMonotonicStamp)subtrahend;
/// <summary>
/// Subtract a portable stamp from a date time yielding the duration between them
/// </summary>
/// <param name="minuend">the minuend</param>
/// <param name="subtrahend">the subtrahend</param>
/// <returns>the difference</returns>
public static PortableDuration operator -(DateTime minuend, in PortableMonotonicStamp subtrahend) =>
((PortableMonotonicStamp)minuend) - subtrahend;
#endregion
#region Private Methods
private readonly string BuildString(bool local)
{
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 2 0 2 0 - 1 2 - 1 2 T 1 8 : 2 1 : 4 3 . 2
// 21 22 23 24 25 26 27 28 29 30 31 32
// 5 9 2 9 0 8 - 0 5 : 0 0
const int insertBeforeIdx = 27;
//if we have non-zero values (from nanoseconds vs datetime/timespan's tenth of a microsecond resolution), fill them in
//otherwise, don't waste space. Maybe reconsider this later. ... want always same width? ... want specify width?
//
var result = new StringBuilder((local ? ToLocalDateTime() : ToUtcDateTime()).ToString("O"));
int fractionalSeconds = GetFractionalSeconds() % 100;
int penultimateChar = Math.DivRem(fractionalSeconds, 10, out int ultimateChar);
if (ultimateChar != 0)
{
string insertMe = penultimateChar == 0
? "0" + fractionalSeconds
: fractionalSeconds.ToString();
result.Insert(insertBeforeIdx, insertMe);
}
else if (penultimateChar != 0)
{
result.Insert(insertBeforeIdx, penultimateChar.ToString());
}
return result.ToString();
}
private readonly (short Year, byte Month, byte Day, byte Hour, byte Minute, short WholeSeconds) GetComponents()
{
DateTime utc = ToUtcDateTime();
short year, wholeSeconds;
byte month, day, hour, minute;
unchecked
{
year = (short)utc.Year;
month = (byte)utc.Month;
day = (byte)utc.Day;
hour = (byte)utc.Hour;
minute = (byte)utc.Minute;
wholeSeconds = (short)utc.Second;
// (Int128 wholeSecondsFromStamp, Int128 fractionalSecondsStamp) =
// Int128.DivRem(in _dateTimeNanosecondOffsetFromMinValueUtc, NanosecondsFrequency);
//#if DEBUG
// int wsfs = (int) (wholeSecondsFromStamp % 60);
// Debug.Assert(wsfs == wholeSeconds);
//#endif
// fractionalSeconds = (int) fractionalSecondsStamp;
}
return (year, month, day, hour, minute, wholeSeconds);
}
private readonly int GetFractionalSeconds()
{
(Int128 _, Int128 fractionalSecondsStamp) =
Int128.DivRem(in _dateTimeNanosecondOffsetFromMinValueUtc, NanosecondsFrequency);
return (int)fractionalSecondsStamp;
}
[OnSerializing]
private void OnSerializing(StreamingContext context)
{
string serialized = _serialized;
if (serialized == null)
{
string writeMe = ToString();
Interlocked.Exchange(ref _serialized, writeMe);
Debug.Assert(string.Equals(_serialized, writeMe));
}
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
string text = Interlocked.Exchange(ref _serialized, null);
if (text != null)
{
(DateTime dtPortion, int nanoSecondsPortion) =
PortableTsParser.ParseStringifiedPortableStampToDtAndNano(text);
PortableMonotonicStamp value = (PortableMonotonicStamp) dtPortion +
PortableDuration.FromNanoseconds(nanoSecondsPortion);
Update(ref _dateTimeNanosecondOffsetFromMinValueUtc, in value);
}
void Update(ref Int128 target, in PortableMonotonicStamp newVal)
{
LogSubstitution(target, newVal._dateTimeNanosecondOffsetFromMinValueUtc, newVal.ToString());
target = newVal._dateTimeNanosecondOffsetFromMinValueUtc;
}
}
[Conditional("DEBUG")]
static void LogSubstitution(Int128 target, Int128 newVal, string portableStamp)
{
if (target != newVal)
{
Debug.WriteLine(
$"Having deserialized monotonic stamp {portableStamp}, parsed nanoseconds substituted with from-string parsed value. Old value nanoseconds: {target}, New value nanoseconds. {newVal}");
}
}
#endregion
#region Private Data
// ReSharper disable once InconsistentNaming -- internal where private normal for efficiency
[DataMember] internal Int128 _dateTimeNanosecondOffsetFromMinValueUtc;
[DataMember] [CanBeNull] private volatile string _serialized;
private static readonly Int128 MaxValueUtcDtNanoseconds;
internal static readonly Int128 MinValueUtcDtNanoseconds;
private static readonly PortableMonotonicStamp TheMinValue;
private static readonly PortableMonotonicStamp TheMaxValue;
private static readonly PortableMonotonicStamp TheUnixEpochStamp;
#endregion
}
internal static class MonoPortableConversionHelper<TStampContext> where TStampContext : struct, IEquatable<TStampContext>, IComparable<TStampContext>, IMonotonicStampContext
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static PortableMonotonicStamp ConvertToPortableMonotonicStamp(MonotonicTimeStamp<TStampContext> monotonicStamp)
{
var (utcReferenceTime, offsetFromReference, _) = monotonicStamp.Value;
Debug.Assert(utcReferenceTime.Kind == DateTimeKind.Utc);
Int128 refTimeNanosecondsSinceMin = (((Int128)utcReferenceTime.Ticks * 100) - PortableMonotonicStamp.MinValueUtcDtNanoseconds);
PortableDuration pd = (PortableDuration)offsetFromReference;
return new PortableMonotonicStamp(pd._ticks + refTimeNanosecondsSinceMin);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static MonotonicTimeStamp<TStampContext> ConvertPortableToMonotonic(in PortableMonotonicStamp ps)
{
MonotonicTimeStamp<TStampContext> referenceMonoStamp = MonotonicTimeStamp<TStampContext>.ReferenceTimeStamp;
PortableDuration portableStampTimeSinceEpoch = new PortableDuration(PortableMonotonicStamp.MinValueUtcDtNanoseconds +
ps._dateTimeNanosecondOffsetFromMinValueUtc);
PortableDuration referenceStampTimeSinceEpoch =
new PortableDuration((Int128)referenceMonoStamp.Value.UtcReferenceTime.Ticks * 100);
PortableDuration difference = portableStampTimeSinceEpoch - referenceStampTimeSinceEpoch;
return referenceMonoStamp + ((Duration)difference);
}
}
}