X Tutup
using System; using System.Collections; using System.Collections.Generic; using JetBrains.Annotations; using Npgsql; #pragma warning disable 1591 // ReSharper disable once CheckNamespace namespace NpgsqlTypes { /// /// A struct similar to .NET DateTime but capable of storing PostgreSQL's timestamp and timestamptz types. /// DateTime is capable of storing values from year 1 to 9999 at 100-nanosecond precision, /// while PostgreSQL's timestamps store values from 4713BC to 5874897AD with 1-microsecond precision. /// [Serializable] public readonly struct NpgsqlDateTime : IEquatable, IComparable, IComparable, IComparer, IComparer { #region Fields readonly NpgsqlDate _date; readonly TimeSpan _time; readonly InternalType _type; #endregion #region Constants public static readonly NpgsqlDateTime Epoch = new NpgsqlDateTime(NpgsqlDate.Epoch); public static readonly NpgsqlDateTime Era = new NpgsqlDateTime(NpgsqlDate.Era); public static readonly NpgsqlDateTime Infinity = new NpgsqlDateTime(InternalType.Infinity, NpgsqlDate.Era, TimeSpan.Zero); public static readonly NpgsqlDateTime NegativeInfinity = new NpgsqlDateTime(InternalType.NegativeInfinity, NpgsqlDate.Era, TimeSpan.Zero); // 9999-12-31 private const int MaxDateTimeDay = 3652058; #endregion #region Constructors NpgsqlDateTime(InternalType type, NpgsqlDate date, TimeSpan time) { if (!date.IsFinite && type != InternalType.Infinity && type != InternalType.NegativeInfinity) throw new ArgumentException("Can't construct an NpgsqlDateTime with a non-finite date, use Infinity and NegativeInfinity instead", nameof(date)); _type = type; _date = date; _time = time; } public NpgsqlDateTime(NpgsqlDate date, TimeSpan time, DateTimeKind kind = DateTimeKind.Unspecified) : this(KindToInternalType(kind), date, time) {} public NpgsqlDateTime(NpgsqlDate date) : this(date, TimeSpan.Zero) {} public NpgsqlDateTime(int year, int month, int day, int hours, int minutes, int seconds, DateTimeKind kind=DateTimeKind.Unspecified) : this(new NpgsqlDate(year, month, day), new TimeSpan(0, hours, minutes, seconds), kind) {} public NpgsqlDateTime(int year, int month, int day, int hours, int minutes, int seconds, int milliseconds, DateTimeKind kind = DateTimeKind.Unspecified) : this(new NpgsqlDate(year, month, day), new TimeSpan(0, hours, minutes, seconds, milliseconds), kind) { } public NpgsqlDateTime(DateTime dateTime) : this(new NpgsqlDate(dateTime.Date), dateTime.TimeOfDay, dateTime.Kind) {} public NpgsqlDateTime(long ticks, DateTimeKind kind) : this(new DateTime(ticks, kind)) { } public NpgsqlDateTime(long ticks) : this(new DateTime(ticks, DateTimeKind.Unspecified)) { } #endregion #region Public Properties public NpgsqlDate Date => _date; public TimeSpan Time => _time; public int DayOfYear => _date.DayOfYear; public int Year => _date.Year; public int Month => _date.Month; public int Day => _date.Day; public DayOfWeek DayOfWeek => _date.DayOfWeek; public bool IsLeapYear => _date.IsLeapYear; public long Ticks => _date.DaysSinceEra * NpgsqlTimeSpan.TicksPerDay + _time.Ticks; public int Millisecond => _time.Milliseconds; public int Second => _time.Seconds; public int Minute => _time.Minutes; public int Hour => _time.Hours; public bool IsInfinity => _type == InternalType.Infinity; public bool IsNegativeInfinity => _type == InternalType.NegativeInfinity; public bool IsFinite { get { switch (_type) { case InternalType.FiniteUnspecified: case InternalType.FiniteUtc: case InternalType.FiniteLocal: return true; case InternalType.Infinity: case InternalType.NegativeInfinity: return false; default: throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {_type} of enum {nameof(NpgsqlDateTime)}.{nameof(InternalType)}. Please file a bug."); } } } public DateTimeKind Kind { get { switch (_type) { case InternalType.FiniteUtc: return DateTimeKind.Utc; case InternalType.FiniteLocal: return DateTimeKind.Local; case InternalType.FiniteUnspecified: case InternalType.Infinity: case InternalType.NegativeInfinity: return DateTimeKind.Unspecified; default: throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {_type} of enum {nameof(DateTimeKind)}. Please file a bug."); } } } /// /// Cast of an to a . /// /// An equivalent . public DateTime ToDateTime() { if (!IsFinite) throw new InvalidCastException("Can't convert infinite timestamp values to DateTime"); if (_date.DaysSinceEra < 0 || _date.DaysSinceEra > MaxDateTimeDay) throw new InvalidCastException("Out of the range of DateTime (year must be between 1 and 9999)"); return new DateTime(Ticks, Kind); } /// /// Converts the value of the current object to Coordinated Universal Time (UTC). /// /// /// See the MSDN documentation for DateTime.ToUniversalTime(). /// Note: this method only takes into account the time zone's base offset, and does /// not respect daylight savings. See https://github.com/npgsql/npgsql/pull/684 for more /// details. /// public NpgsqlDateTime ToUniversalTime() { switch (_type) { case InternalType.FiniteUnspecified: // Treat as Local case InternalType.FiniteLocal: if (_date.DaysSinceEra >= 1 && _date.DaysSinceEra <= MaxDateTimeDay - 1) { // Day between 0001-01-02 and 9999-12-30, so we can use DateTime and it will always succeed return new NpgsqlDateTime(Subtract(TimeZoneInfo.Local.GetUtcOffset(new DateTime(ToDateTime().Ticks, DateTimeKind.Local))).Ticks, DateTimeKind.Utc); } // Else there are no DST rules available in the system for outside the DateTime range, so just use the base offset return new NpgsqlDateTime(Subtract(TimeZoneInfo.Local.BaseUtcOffset).Ticks, DateTimeKind.Utc); case InternalType.FiniteUtc: case InternalType.Infinity: case InternalType.NegativeInfinity: return this; default: throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {_type} of enum {nameof(NpgsqlDateTime)}.{nameof(InternalType)}. Please file a bug."); } } /// /// Converts the value of the current object to local time. /// /// /// See the MSDN documentation for DateTime.ToLocalTime(). /// Note: this method only takes into account the time zone's base offset, and does /// not respect daylight savings. See https://github.com/npgsql/npgsql/pull/684 for more /// details. /// public NpgsqlDateTime ToLocalTime() { switch (_type) { case InternalType.FiniteUnspecified: // Treat as UTC case InternalType.FiniteUtc: if (_date.DaysSinceEra >= 1 && _date.DaysSinceEra <= MaxDateTimeDay - 1) { // Day between 0001-01-02 and 9999-12-30, so we can use DateTime and it will always succeed return new NpgsqlDateTime(TimeZoneInfo.ConvertTime(new DateTime(ToDateTime().Ticks, DateTimeKind.Utc), TimeZoneInfo.Local)); } // Else there are no DST rules available in the system for outside the DateTime range, so just use the base offset return new NpgsqlDateTime(Add(TimeZoneInfo.Local.BaseUtcOffset).Ticks, DateTimeKind.Local); case InternalType.FiniteLocal: case InternalType.Infinity: case InternalType.NegativeInfinity: return this; default: throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {_type} of enum {nameof(NpgsqlDateTime)}.{nameof(InternalType)}. Please file a bug."); } } public static NpgsqlDateTime Now => new NpgsqlDateTime(DateTime.Now); #endregion #region String Conversions public override string ToString() { switch (_type) { case InternalType.Infinity: return "infinity"; case InternalType.NegativeInfinity: return "-infinity"; default: return $"{_date} {_time}"; } } public static NpgsqlDateTime Parse(string str) { if (str == null) { throw new NullReferenceException(); } switch (str = str.Trim().ToLowerInvariant()) { case "infinity": return Infinity; case "-infinity": return NegativeInfinity; default: try { var idxSpace = str.IndexOf(' '); var datePart = str.Substring(0, idxSpace); if (str.Contains("bc")) { datePart += " BC"; } var idxSecond = str.IndexOf(' ', idxSpace + 1); if (idxSecond == -1) { idxSecond = str.Length; } var timePart = str.Substring(idxSpace + 1, idxSecond - idxSpace - 1); return new NpgsqlDateTime(NpgsqlDate.Parse(datePart), TimeSpan.Parse(timePart)); } catch (OverflowException) { throw; } catch { throw new FormatException(); } } } #endregion #region Comparisons public bool Equals(NpgsqlDateTime other) { switch (_type) { case InternalType.Infinity: return other._type == InternalType.Infinity; case InternalType.NegativeInfinity: return other._type == InternalType.NegativeInfinity; default: return other._type == _type && _date.Equals(other._date) && _time.Equals(other._time); } } public override bool Equals([CanBeNull] object obj) => obj is NpgsqlDateTime && Equals((NpgsqlDateTime)obj); public override int GetHashCode() { switch (_type) { case InternalType.Infinity: return int.MaxValue; case InternalType.NegativeInfinity: return int.MinValue; default: return _date.GetHashCode() ^ PGUtil.RotateShift(_time.GetHashCode(), 16); } } public int CompareTo(NpgsqlDateTime other) { switch (_type) { case InternalType.Infinity: return other._type == InternalType.Infinity ? 0 : 1; case InternalType.NegativeInfinity: return other._type == InternalType.NegativeInfinity ? 0 : -1; default: switch (other._type) { case InternalType.Infinity: return -1; case InternalType.NegativeInfinity: return 1; default: var cmp = _date.CompareTo(other._date); return cmp == 0 ? _time.CompareTo(other._time) : cmp; } } } public int CompareTo([CanBeNull] object o) { if (o == null) return 1; if (o is NpgsqlDateTime) return CompareTo((NpgsqlDateTime)o); throw new ArgumentException(); } public int Compare(NpgsqlDateTime x, NpgsqlDateTime y) => x.CompareTo(y); public int Compare([CanBeNull] object x, [CanBeNull] object y) { if (x == null) return y == null ? 0 : -1; if (y == null) return 1; if (!(x is IComparable) || !(y is IComparable)) throw new ArgumentException(); return ((IComparable)x).CompareTo(y); } #endregion #region Arithmetic /// /// Returns a new that adds the value of the specified TimeSpan to the value of this instance. /// /// A positive or negative time interval. /// An object whose value is the sum of the date and time represented by this instance and the time interval represented by value. public NpgsqlDateTime Add(NpgsqlTimeSpan value) { return AddTicks(value.Ticks); } /// /// Returns a new that adds the value of the specified to the value of this instance. /// /// A positive or negative time interval. /// An object whose value is the sum of the date and time represented by this instance and the time interval represented by value. public NpgsqlDateTime Add(TimeSpan value) { return AddTicks(value.Ticks); } /// /// Returns a new that adds the specified number of years to the value of this instance. /// /// A number of years. The value parameter can be negative or positive. /// An object whose value is the sum of the date and time represented by this instance and the number of years represented by value. public NpgsqlDateTime AddYears(int value) { switch (_type) { case InternalType.Infinity: case InternalType.NegativeInfinity: return this; default: return new NpgsqlDateTime(_type, _date.AddYears(value), _time); } } /// /// Returns a new that adds the specified number of months to the value of this instance. /// /// A number of months. The months parameter can be negative or positive. /// An object whose value is the sum of the date and time represented by this instance and months. public NpgsqlDateTime AddMonths(int value) { switch (_type) { case InternalType.Infinity: case InternalType.NegativeInfinity: return this; default: return new NpgsqlDateTime(_type, _date.AddMonths(value), _time); } } /// /// Returns a new that adds the specified number of days to the value of this instance. /// /// A number of whole and fractional days. The value parameter can be negative or positive. /// An object whose value is the sum of the date and time represented by this instance and the number of days represented by value. public NpgsqlDateTime AddDays(double value) { return Add(TimeSpan.FromDays(value)); } /// /// Returns a new that adds the specified number of hours to the value of this instance. /// /// A number of whole and fractional hours. The value parameter can be negative or positive. /// An object whose value is the sum of the date and time represented by this instance and the number of hours represented by value. public NpgsqlDateTime AddHours(double value) { return Add(TimeSpan.FromHours(value)); } /// /// Returns a new that adds the specified number of minutes to the value of this instance. /// /// A number of whole and fractional minutes. The value parameter can be negative or positive. /// An object whose value is the sum of the date and time represented by this instance and the number of minutes represented by value. public NpgsqlDateTime AddMinutes(double value) { return Add(TimeSpan.FromMinutes(value)); } /// /// Returns a new that adds the specified number of minutes to the value of this instance. /// /// A number of whole and fractional minutes. The value parameter can be negative or positive. /// An object whose value is the sum of the date and time represented by this instance and the number of minutes represented by value. public NpgsqlDateTime AddSeconds(double value) { return Add(TimeSpan.FromSeconds(value)); } /// /// Returns a new that adds the specified number of milliseconds to the value of this instance. /// /// A number of whole and fractional milliseconds. The value parameter can be negative or positive. Note that this value is rounded to the nearest integer. /// An object whose value is the sum of the date and time represented by this instance and the number of milliseconds represented by value. public NpgsqlDateTime AddMilliseconds(double value) { return Add(TimeSpan.FromMilliseconds(value)); } /// /// Returns a new that adds the specified number of ticks to the value of this instance. /// /// A number of 100-nanosecond ticks. The value parameter can be positive or negative. /// An object whose value is the sum of the date and time represented by this instance and the time represented by value. public NpgsqlDateTime AddTicks(long value) { switch (_type) { case InternalType.Infinity: case InternalType.NegativeInfinity: return this; default: return new NpgsqlDateTime(Ticks + value, Kind); } } public NpgsqlDateTime Subtract(NpgsqlTimeSpan interval) { return Add(-interval); } public NpgsqlTimeSpan Subtract(NpgsqlDateTime timestamp) { switch (_type) { case InternalType.Infinity: case InternalType.NegativeInfinity: throw new InvalidOperationException("You cannot subtract infinity timestamps"); } switch (timestamp._type) { case InternalType.Infinity: case InternalType.NegativeInfinity: throw new InvalidOperationException("You cannot subtract infinity timestamps"); } return new NpgsqlTimeSpan(0, _date.DaysSinceEra - timestamp._date.DaysSinceEra, _time.Ticks - timestamp._time.Ticks); } #endregion #region Operators public static NpgsqlDateTime operator +(NpgsqlDateTime timestamp, NpgsqlTimeSpan interval) => timestamp.Add(interval); public static NpgsqlDateTime operator +(NpgsqlTimeSpan interval, NpgsqlDateTime timestamp) => timestamp.Add(interval); public static NpgsqlDateTime operator -(NpgsqlDateTime timestamp, NpgsqlTimeSpan interval) => timestamp.Subtract(interval); public static NpgsqlTimeSpan operator -(NpgsqlDateTime x, NpgsqlDateTime y) => x.Subtract(y); public static bool operator ==(NpgsqlDateTime x, NpgsqlDateTime y) => x.Equals(y); public static bool operator !=(NpgsqlDateTime x, NpgsqlDateTime y) => !(x == y); public static bool operator <(NpgsqlDateTime x, NpgsqlDateTime y) => x.CompareTo(y) < 0; public static bool operator >(NpgsqlDateTime x, NpgsqlDateTime y) => x.CompareTo(y) > 0; public static bool operator <=(NpgsqlDateTime x, NpgsqlDateTime y) => x.CompareTo(y) <= 0; public static bool operator >=(NpgsqlDateTime x, NpgsqlDateTime y) => x.CompareTo(y) >= 0; #endregion #region Casts /// /// Implicit cast of a to an /// /// A /// An equivalent . public static implicit operator NpgsqlDateTime(DateTime dateTime) => ToNpgsqlDateTime(dateTime); public static NpgsqlDateTime ToNpgsqlDateTime(DateTime dateTime) => new NpgsqlDateTime(dateTime); /// /// Explicit cast of an to a . /// /// An . /// An equivalent . public static explicit operator DateTime(NpgsqlDateTime npgsqlDateTime) => npgsqlDateTime.ToDateTime(); #endregion public NpgsqlDateTime Normalize() => Add(NpgsqlTimeSpan.Zero); static InternalType KindToInternalType(DateTimeKind kind) { switch (kind) { case DateTimeKind.Unspecified: return InternalType.FiniteUnspecified; case DateTimeKind.Utc: return InternalType.FiniteUtc; case DateTimeKind.Local: return InternalType.FiniteLocal; default: throw new InvalidOperationException($"Internal Npgsql bug: unexpected value {kind} of enum {nameof(NpgsqlDateTime)}.{nameof(InternalType)}. Please file a bug."); } } enum InternalType { FiniteUnspecified, FiniteUtc, FiniteLocal, Infinity, NegativeInfinity } } }
X Tutup