3 using System.Globalization;
15 private readonly
int hours;
16 private readonly
int minutes;
17 private readonly
char sign;
23 minutes: value.Minutes,
26 hours = Math.Abs(value.Hours);
27 minutes = Math.Abs(value.Minutes);
28 sign =
Value.Ticks < 0 ?
'-' :
'+';
32 => (hours, minutes)
switch
35 (_, 0) => $
"UTC{sign}{hours}",
36 (_, < 10) => $
"UTC{sign}{hours}:0{minutes}",
37 _ => $
"UTC{sign}{hours}:{minutes}"
41 => HashCode.Combine(
Value.Ticks < 0, hours, minutes);
45 if (dateTime.Kind == DateTimeKind.Unspecified)
47 throw new InvalidOperationException(
48 $
"Cannot determine timezone for {nameof(DateTime)} " +
49 $
"of unspecified kind");
51 var utcDateTime = dateTime.ToUniversalTime();
58 public static Option<SerializableTimeZone>
Parse(
string str)
60 if (!str.StartsWith(
"UTC", StringComparison.OrdinalIgnoreCase))
62 return Option<SerializableTimeZone>.None();
64 string timeZoneStr = str[3..];
65 bool negative = timeZoneStr.StartsWith(
"-");
66 bool valid = negative || timeZoneStr.StartsWith(
"+");
68 if (!valid) {
return Option<SerializableTimeZone>.None(); }
70 timeZoneStr = str[4..];
72 TimeSpan makeTimeSpan(
int hours,
int minutes)
74 ticks: (hours * TimeSpan.TicksPerHour + minutes * TimeSpan.TicksPerMinute)
75 * (negative ? -1L : 1L));
77 if (timeZoneStr.IndexOf(
':') is var hrMinSeparator && hrMinSeparator > 0)
79 if (
int.TryParse(timeZoneStr[..hrMinSeparator], out
int timeZoneHours)
80 &&
int.TryParse(timeZoneStr[(hrMinSeparator + 1)..], out
int timeZoneMinutes))
82 return Option<SerializableTimeZone>.Some(
86 else if (
int.TryParse(timeZoneStr, out
int timeZoneHours))
88 return Option<SerializableTimeZone>.Some(
91 return Option<SerializableTimeZone>.None();
102 =>
ToUtc().value.Equals(other.ToUtc().value);
107 private static DateTime UnixEpoch(DateTimeKind kind)
108 =>
new DateTime(1970, 1, 1, 0, 0, 0, kind);
110 private readonly DateTime value;
115 if (value.Kind == DateTimeKind.Unspecified)
117 throw new Exception($
"Timezone required when constructing {nameof(SerializableDateTime)} " +
118 $
"from {nameof(DateTime)} of unspecified kind");
125 this.value =
new DateTime(
126 value.Year, value.Month, value.Day,
127 value.Hour, value.Minute, value.Second,
128 DateTimeKind.Unspecified);
140 DateTime.SpecifyKind(value -
TimeZone.
Value, DateTimeKind.Utc));
159 => (value - UnixEpoch(value.Kind)).
Ticks / TimeSpan.TicksPerSecond;
161 private static string MakeString(params (
long Value,
string Suffix)[] parts)
163 parts.Select(p => $
"{p.Value.ToString().PadLeft(2, '0')}{p.Suffix}"));
176 (value.Minute,
"MIN"),
177 (value.Second,
"SEC"))
181 =>
ToLocalValue().ToString(CultureInfo.InvariantCulture);
185 value.Year, value.Month, value.Day,
186 value.Hour, value.Minute, value.Second,
189 public static Option<SerializableDateTime>
Parse(
string str)
191 if (
long.TryParse(str, out
long unixTime)
193 && unixTime < (DateTime.MaxValue - UnixEpoch(DateTimeKind.Utc)).TotalSeconds)
198 string[] split = str.Split(
' ');
200 int year = 0;
int month = 0;
int day = 0;
201 int hour = 0;
int minute = 0;
int second = 0;
203 foreach (var part
in split)
207 timeZone = parsedTimeZone;
211 Identifier suffix =
string.Join(
"", part.Where(
char.IsLetter)).ToIdentifier();
212 if (!part.EndsWith(suffix.Value)) {
continue; }
214 part[..^suffix.Value.Length],
215 NumberStyles.Integer,
216 CultureInfo.InvariantCulture,
222 if (suffix ==
"Y") { year = value; }
223 else if (suffix ==
"M") { month = value; }
224 else if (suffix ==
"D") { day = value; }
225 else if (suffix ==
"HR") { hour = value; }
226 else if (suffix ==
"MIN") { minute = value; }
227 else if (suffix ==
"SEC") { second = value; }
230 if (year > 0 && month > 0 && day > 0)
232 return Option<SerializableDateTime>.Some(
234 new DateTime(year, month, day, hour, minute, second),
238 return Option<SerializableDateTime>.None();
242 =>
ToUtc().value.CompareTo(other.ToUtc().value);
245 => a.CompareTo(b) < 0;
248 => a.CompareTo(b) > 0;
251 => a.CompareTo(b) == 0;
263 => a.
ToUtc().value - b.ToUtc().value;
DateTime wrapper that tries to offer a reliable string representation that's also human-friendly
SerializableDateTime ToUtc()
static SerializableDateTime FromUtcUnixTime(long unixTime)
static SerializableDateTime UtcNow
static bool operator!=(in SerializableDateTime a, in SerializableDateTime b)
static bool operator==(in SerializableDateTime a, in SerializableDateTime b)
override int GetHashCode()
static bool operator>(in SerializableDateTime a, in SerializableDateTime b)
static SerializableDateTime FromLocalUnixTime(long unixTime)
SerializableDateTime ToLocal()
override bool Equals(object? obj)
int CompareTo(SerializableDateTime other)
string ToLocalUserString()
static SerializableDateTime LocalNow
readonly SerializableTimeZone TimeZone
SerializableDateTime(DateTime value, SerializableTimeZone timeZone)
bool Equals(SerializableDateTime other)
static SerializableDateTime operator-(in SerializableDateTime dt, in TimeSpan ts)
static bool operator<(in SerializableDateTime a, in SerializableDateTime b)
static SerializableDateTime operator+(in SerializableDateTime dt, in TimeSpan ts)
static Option< SerializableDateTime > Parse(string str)
SerializableDateTime(DateTime value)
override string ToString()
SerializableTimeZone(TimeSpan value)
static SerializableTimeZone FromDateTime(DateTime dateTime)
override int GetHashCode()
static SerializableTimeZone LocalTimeZone
static Option< SerializableTimeZone > Parse(string str)
override string ToString()
readonly TimeSpan Value
Diff from UTC