2021-03-31 11:22:02 +08:00
|
|
|
|
#region License
|
|
|
|
|
// Copyright (c) 2007 James Newton-King
|
|
|
|
|
//
|
|
|
|
|
// Permission is hereby granted, free of charge, to any person
|
|
|
|
|
// obtaining a copy of this software and associated documentation
|
|
|
|
|
// files (the "Software"), to deal in the Software without
|
|
|
|
|
// restriction, including without limitation the rights to use,
|
|
|
|
|
// copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
|
|
|
// copies of the Software, and to permit persons to whom the
|
|
|
|
|
// Software is furnished to do so, subject to the following
|
|
|
|
|
// conditions:
|
|
|
|
|
//
|
|
|
|
|
// The above copyright notice and this permission notice shall be
|
|
|
|
|
// included in all copies or substantial portions of the Software.
|
|
|
|
|
//
|
|
|
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
|
|
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
|
|
|
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
|
|
|
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
|
|
|
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
|
|
|
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
|
|
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
|
|
|
// OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
using System;
|
|
|
|
|
using System.IO;
|
|
|
|
|
using System.Xml;
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
|
|
|
|
namespace LC.Newtonsoft.Json.Utilities
|
|
|
|
|
{
|
|
|
|
|
internal static class DateTimeUtils
|
|
|
|
|
{
|
|
|
|
|
internal static readonly long InitialJavaScriptDateTicks = 621355968000000000;
|
|
|
|
|
private const string IsoDateFormat = "yyyy-MM-ddTHH:mm:ss.FFFFFFFK";
|
|
|
|
|
|
|
|
|
|
private const int DaysPer100Years = 36524;
|
|
|
|
|
private const int DaysPer400Years = 146097;
|
|
|
|
|
private const int DaysPer4Years = 1461;
|
|
|
|
|
private const int DaysPerYear = 365;
|
|
|
|
|
private const long TicksPerDay = 864000000000L;
|
|
|
|
|
private static readonly int[] DaysToMonth365;
|
|
|
|
|
private static readonly int[] DaysToMonth366;
|
|
|
|
|
|
|
|
|
|
static DateTimeUtils()
|
|
|
|
|
{
|
|
|
|
|
DaysToMonth365 = new[] { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
|
|
|
|
|
DaysToMonth366 = new[] { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public static TimeSpan GetUtcOffset(this DateTime d)
|
|
|
|
|
{
|
|
|
|
|
#if !HAVE_TIME_ZONE_INFO
|
|
|
|
|
return TimeZone.CurrentTimeZone.GetUtcOffset(d);
|
|
|
|
|
#else
|
|
|
|
|
return TimeZoneInfo.Local.GetUtcOffset(d);
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if !(PORTABLE40 || PORTABLE) || NETSTANDARD1_3
|
|
|
|
|
public static XmlDateTimeSerializationMode ToSerializationMode(DateTimeKind kind)
|
|
|
|
|
{
|
|
|
|
|
switch (kind)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeKind.Local:
|
|
|
|
|
return XmlDateTimeSerializationMode.Local;
|
|
|
|
|
case DateTimeKind.Unspecified:
|
|
|
|
|
return XmlDateTimeSerializationMode.Unspecified;
|
|
|
|
|
case DateTimeKind.Utc:
|
|
|
|
|
return XmlDateTimeSerializationMode.Utc;
|
|
|
|
|
default:
|
|
|
|
|
throw MiscellaneousUtils.CreateArgumentOutOfRangeException(nameof(kind), kind, "Unexpected DateTimeKind value.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
public static string ToDateTimeFormat(DateTimeKind kind)
|
|
|
|
|
{
|
|
|
|
|
switch (kind)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeKind.Local:
|
|
|
|
|
return IsoDateFormat;
|
|
|
|
|
case DateTimeKind.Unspecified:
|
|
|
|
|
return "yyyy-MM-ddTHH:mm:ss.FFFFFFF";
|
|
|
|
|
case DateTimeKind.Utc:
|
|
|
|
|
return "yyyy-MM-ddTHH:mm:ss.FFFFFFFZ";
|
|
|
|
|
default:
|
|
|
|
|
throw MiscellaneousUtils.CreateArgumentOutOfRangeException(nameof(kind), kind, "Unexpected DateTimeKind value.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
internal static DateTime EnsureDateTime(DateTime value, DateTimeZoneHandling timeZone)
|
|
|
|
|
{
|
|
|
|
|
switch (timeZone)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeZoneHandling.Local:
|
|
|
|
|
value = SwitchToLocalTime(value);
|
|
|
|
|
break;
|
|
|
|
|
case DateTimeZoneHandling.Utc:
|
|
|
|
|
value = SwitchToUtcTime(value);
|
|
|
|
|
break;
|
|
|
|
|
case DateTimeZoneHandling.Unspecified:
|
|
|
|
|
value = new DateTime(value.Ticks, DateTimeKind.Unspecified);
|
|
|
|
|
break;
|
|
|
|
|
case DateTimeZoneHandling.RoundtripKind:
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
throw new ArgumentException("Invalid date time handling value.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static DateTime SwitchToLocalTime(DateTime value)
|
|
|
|
|
{
|
|
|
|
|
switch (value.Kind)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeKind.Unspecified:
|
|
|
|
|
return new DateTime(value.Ticks, DateTimeKind.Local);
|
|
|
|
|
|
|
|
|
|
case DateTimeKind.Utc:
|
|
|
|
|
return value.ToLocalTime();
|
|
|
|
|
|
|
|
|
|
case DateTimeKind.Local:
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static DateTime SwitchToUtcTime(DateTime value)
|
|
|
|
|
{
|
|
|
|
|
switch (value.Kind)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeKind.Unspecified:
|
|
|
|
|
return new DateTime(value.Ticks, DateTimeKind.Utc);
|
|
|
|
|
|
|
|
|
|
case DateTimeKind.Utc:
|
|
|
|
|
return value;
|
|
|
|
|
|
|
|
|
|
case DateTimeKind.Local:
|
|
|
|
|
return value.ToUniversalTime();
|
|
|
|
|
}
|
|
|
|
|
return value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static long ToUniversalTicks(DateTime dateTime)
|
|
|
|
|
{
|
|
|
|
|
if (dateTime.Kind == DateTimeKind.Utc)
|
|
|
|
|
{
|
|
|
|
|
return dateTime.Ticks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ToUniversalTicks(dateTime, dateTime.GetUtcOffset());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static long ToUniversalTicks(DateTime dateTime, TimeSpan offset)
|
|
|
|
|
{
|
|
|
|
|
// special case min and max value
|
|
|
|
|
// they never have a timezone appended to avoid issues
|
|
|
|
|
if (dateTime.Kind == DateTimeKind.Utc || dateTime == DateTime.MaxValue || dateTime == DateTime.MinValue)
|
|
|
|
|
{
|
|
|
|
|
return dateTime.Ticks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
long ticks = dateTime.Ticks - offset.Ticks;
|
|
|
|
|
if (ticks > 3155378975999999999L)
|
|
|
|
|
{
|
|
|
|
|
return 3155378975999999999L;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ticks < 0L)
|
|
|
|
|
{
|
|
|
|
|
return 0L;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ticks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static long ConvertDateTimeToJavaScriptTicks(DateTime dateTime, TimeSpan offset)
|
|
|
|
|
{
|
2021-03-31 11:49:13 +08:00
|
|
|
|
long universialTicks = ToUniversalTicks(dateTime, offset);
|
2021-03-31 11:22:02 +08:00
|
|
|
|
|
2021-03-31 11:49:13 +08:00
|
|
|
|
return UniversialTicksToJavaScriptTicks(universialTicks);
|
2021-03-31 11:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static long ConvertDateTimeToJavaScriptTicks(DateTime dateTime)
|
|
|
|
|
{
|
|
|
|
|
return ConvertDateTimeToJavaScriptTicks(dateTime, true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static long ConvertDateTimeToJavaScriptTicks(DateTime dateTime, bool convertToUtc)
|
|
|
|
|
{
|
|
|
|
|
long ticks = (convertToUtc) ? ToUniversalTicks(dateTime) : dateTime.Ticks;
|
|
|
|
|
|
2021-03-31 11:49:13 +08:00
|
|
|
|
return UniversialTicksToJavaScriptTicks(ticks);
|
2021-03-31 11:22:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
2021-03-31 11:49:13 +08:00
|
|
|
|
private static long UniversialTicksToJavaScriptTicks(long universialTicks)
|
2021-03-31 11:22:02 +08:00
|
|
|
|
{
|
2021-03-31 11:49:13 +08:00
|
|
|
|
long javaScriptTicks = (universialTicks - InitialJavaScriptDateTicks) / 10000;
|
2021-03-31 11:22:02 +08:00
|
|
|
|
|
|
|
|
|
return javaScriptTicks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static DateTime ConvertJavaScriptTicksToDateTime(long javaScriptTicks)
|
|
|
|
|
{
|
|
|
|
|
DateTime dateTime = new DateTime((javaScriptTicks * 10000) + InitialJavaScriptDateTicks, DateTimeKind.Utc);
|
|
|
|
|
|
|
|
|
|
return dateTime;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#region Parse
|
|
|
|
|
internal static bool TryParseDateTimeIso(StringReference text, DateTimeZoneHandling dateTimeZoneHandling, out DateTime dt)
|
|
|
|
|
{
|
|
|
|
|
DateTimeParser dateTimeParser = new DateTimeParser();
|
|
|
|
|
if (!dateTimeParser.Parse(text.Chars, text.StartIndex, text.Length))
|
|
|
|
|
{
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTime d = CreateDateTime(dateTimeParser);
|
|
|
|
|
|
|
|
|
|
long ticks;
|
|
|
|
|
|
|
|
|
|
switch (dateTimeParser.Zone)
|
|
|
|
|
{
|
|
|
|
|
case ParserTimeZone.Utc:
|
|
|
|
|
d = new DateTime(d.Ticks, DateTimeKind.Utc);
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
case ParserTimeZone.LocalWestOfUtc:
|
|
|
|
|
{
|
|
|
|
|
TimeSpan offset = new TimeSpan(dateTimeParser.ZoneHour, dateTimeParser.ZoneMinute, 0);
|
|
|
|
|
ticks = d.Ticks + offset.Ticks;
|
|
|
|
|
if (ticks <= DateTime.MaxValue.Ticks)
|
|
|
|
|
{
|
|
|
|
|
d = new DateTime(ticks, DateTimeKind.Utc).ToLocalTime();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ticks += d.GetUtcOffset().Ticks;
|
|
|
|
|
if (ticks > DateTime.MaxValue.Ticks)
|
|
|
|
|
{
|
|
|
|
|
ticks = DateTime.MaxValue.Ticks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d = new DateTime(ticks, DateTimeKind.Local);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case ParserTimeZone.LocalEastOfUtc:
|
|
|
|
|
{
|
|
|
|
|
TimeSpan offset = new TimeSpan(dateTimeParser.ZoneHour, dateTimeParser.ZoneMinute, 0);
|
|
|
|
|
ticks = d.Ticks - offset.Ticks;
|
|
|
|
|
if (ticks >= DateTime.MinValue.Ticks)
|
|
|
|
|
{
|
|
|
|
|
d = new DateTime(ticks, DateTimeKind.Utc).ToLocalTime();
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
ticks += d.GetUtcOffset().Ticks;
|
|
|
|
|
if (ticks < DateTime.MinValue.Ticks)
|
|
|
|
|
{
|
|
|
|
|
ticks = DateTime.MinValue.Ticks;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d = new DateTime(ticks, DateTimeKind.Local);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = EnsureDateTime(d, dateTimeZoneHandling);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if HAVE_DATE_TIME_OFFSET
|
|
|
|
|
internal static bool TryParseDateTimeOffsetIso(StringReference text, out DateTimeOffset dt)
|
|
|
|
|
{
|
|
|
|
|
DateTimeParser dateTimeParser = new DateTimeParser();
|
|
|
|
|
if (!dateTimeParser.Parse(text.Chars, text.StartIndex, text.Length))
|
|
|
|
|
{
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTime d = CreateDateTime(dateTimeParser);
|
|
|
|
|
|
|
|
|
|
TimeSpan offset;
|
|
|
|
|
|
|
|
|
|
switch (dateTimeParser.Zone)
|
|
|
|
|
{
|
|
|
|
|
case ParserTimeZone.Utc:
|
|
|
|
|
offset = new TimeSpan(0L);
|
|
|
|
|
break;
|
|
|
|
|
case ParserTimeZone.LocalWestOfUtc:
|
|
|
|
|
offset = new TimeSpan(-dateTimeParser.ZoneHour, -dateTimeParser.ZoneMinute, 0);
|
|
|
|
|
break;
|
|
|
|
|
case ParserTimeZone.LocalEastOfUtc:
|
|
|
|
|
offset = new TimeSpan(dateTimeParser.ZoneHour, dateTimeParser.ZoneMinute, 0);
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
offset = TimeZoneInfo.Local.GetUtcOffset(d);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
long ticks = d.Ticks - offset.Ticks;
|
|
|
|
|
if (ticks < 0 || ticks > 3155378975999999999)
|
|
|
|
|
{
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = new DateTimeOffset(d, offset);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
private static DateTime CreateDateTime(DateTimeParser dateTimeParser)
|
|
|
|
|
{
|
|
|
|
|
bool is24Hour;
|
|
|
|
|
if (dateTimeParser.Hour == 24)
|
|
|
|
|
{
|
|
|
|
|
is24Hour = true;
|
|
|
|
|
dateTimeParser.Hour = 0;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
is24Hour = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTime d = new DateTime(dateTimeParser.Year, dateTimeParser.Month, dateTimeParser.Day, dateTimeParser.Hour, dateTimeParser.Minute, dateTimeParser.Second);
|
|
|
|
|
d = d.AddTicks(dateTimeParser.Fraction);
|
|
|
|
|
|
|
|
|
|
if (is24Hour)
|
|
|
|
|
{
|
|
|
|
|
d = d.AddDays(1);
|
|
|
|
|
}
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static bool TryParseDateTime(StringReference s, DateTimeZoneHandling dateTimeZoneHandling, string? dateFormatString, CultureInfo culture, out DateTime dt)
|
|
|
|
|
{
|
|
|
|
|
if (s.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
int i = s.StartIndex;
|
|
|
|
|
if (s[i] == '/')
|
|
|
|
|
{
|
|
|
|
|
if (s.Length >= 9 && s.StartsWith("/Date(") && s.EndsWith(")/"))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeMicrosoft(s, dateTimeZoneHandling, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (s.Length >= 19 && s.Length <= 40 && char.IsDigit(s[i]) && s[i + 10] == 'T')
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeIso(s, dateTimeZoneHandling, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!StringUtils.IsNullOrEmpty(dateFormatString))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeExact(s.ToString(), dateTimeZoneHandling, dateFormatString, culture, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static bool TryParseDateTime(string s, DateTimeZoneHandling dateTimeZoneHandling, string? dateFormatString, CultureInfo culture, out DateTime dt)
|
|
|
|
|
{
|
|
|
|
|
if (s.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
if (s[0] == '/')
|
|
|
|
|
{
|
|
|
|
|
if (s.Length >= 9 && s.StartsWith("/Date(", StringComparison.Ordinal) && s.EndsWith(")/", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeMicrosoft(new StringReference(s.ToCharArray(), 0, s.Length), dateTimeZoneHandling, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (s.Length >= 19 && s.Length <= 40 && char.IsDigit(s[0]) && s[10] == 'T')
|
|
|
|
|
{
|
|
|
|
|
if (DateTime.TryParseExact(s, IsoDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dt))
|
|
|
|
|
{
|
|
|
|
|
dt = EnsureDateTime(dt, dateTimeZoneHandling);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!StringUtils.IsNullOrEmpty(dateFormatString))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeExact(s, dateTimeZoneHandling, dateFormatString, culture, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if HAVE_DATE_TIME_OFFSET
|
|
|
|
|
internal static bool TryParseDateTimeOffset(StringReference s, string? dateFormatString, CultureInfo culture, out DateTimeOffset dt)
|
|
|
|
|
{
|
|
|
|
|
if (s.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
int i = s.StartIndex;
|
|
|
|
|
if (s[i] == '/')
|
|
|
|
|
{
|
|
|
|
|
if (s.Length >= 9 && s.StartsWith("/Date(") && s.EndsWith(")/"))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeOffsetMicrosoft(s, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (s.Length >= 19 && s.Length <= 40 && char.IsDigit(s[i]) && s[i + 10] == 'T')
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeOffsetIso(s, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!StringUtils.IsNullOrEmpty(dateFormatString))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeOffsetExact(s.ToString(), dateFormatString, culture, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static bool TryParseDateTimeOffset(string s, string? dateFormatString, CultureInfo culture, out DateTimeOffset dt)
|
|
|
|
|
{
|
|
|
|
|
if (s.Length > 0)
|
|
|
|
|
{
|
|
|
|
|
if (s[0] == '/')
|
|
|
|
|
{
|
|
|
|
|
if (s.Length >= 9 && s.StartsWith("/Date(", StringComparison.Ordinal) && s.EndsWith(")/", StringComparison.Ordinal))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeOffsetMicrosoft(new StringReference(s.ToCharArray(), 0, s.Length), out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (s.Length >= 19 && s.Length <= 40 && char.IsDigit(s[0]) && s[10] == 'T')
|
|
|
|
|
{
|
|
|
|
|
if (DateTimeOffset.TryParseExact(s, IsoDateFormat, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out dt))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeOffsetIso(new StringReference(s.ToCharArray(), 0, s.Length), out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!StringUtils.IsNullOrEmpty(dateFormatString))
|
|
|
|
|
{
|
|
|
|
|
if (TryParseDateTimeOffsetExact(s, dateFormatString, culture, out dt))
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
private static bool TryParseMicrosoftDate(StringReference text, out long ticks, out TimeSpan offset, out DateTimeKind kind)
|
|
|
|
|
{
|
|
|
|
|
kind = DateTimeKind.Utc;
|
|
|
|
|
|
|
|
|
|
int index = text.IndexOf('+', 7, text.Length - 8);
|
|
|
|
|
|
|
|
|
|
if (index == -1)
|
|
|
|
|
{
|
|
|
|
|
index = text.IndexOf('-', 7, text.Length - 8);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (index != -1)
|
|
|
|
|
{
|
|
|
|
|
kind = DateTimeKind.Local;
|
|
|
|
|
|
|
|
|
|
if (!TryReadOffset(text, index + text.StartIndex, out offset))
|
|
|
|
|
{
|
|
|
|
|
ticks = 0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
offset = TimeSpan.Zero;
|
|
|
|
|
index = text.Length - 2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (ConvertUtils.Int64TryParse(text.Chars, 6 + text.StartIndex, index - 6, out ticks) == ParseResult.Success);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TryParseDateTimeMicrosoft(StringReference text, DateTimeZoneHandling dateTimeZoneHandling, out DateTime dt)
|
|
|
|
|
{
|
|
|
|
|
if (!TryParseMicrosoftDate(text, out long ticks, out _, out DateTimeKind kind))
|
|
|
|
|
{
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTime utcDateTime = ConvertJavaScriptTicksToDateTime(ticks);
|
|
|
|
|
|
|
|
|
|
switch (kind)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeKind.Unspecified:
|
|
|
|
|
dt = DateTime.SpecifyKind(utcDateTime.ToLocalTime(), DateTimeKind.Unspecified);
|
|
|
|
|
break;
|
|
|
|
|
case DateTimeKind.Local:
|
|
|
|
|
dt = utcDateTime.ToLocalTime();
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
dt = utcDateTime;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = EnsureDateTime(dt, dateTimeZoneHandling);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TryParseDateTimeExact(string text, DateTimeZoneHandling dateTimeZoneHandling, string dateFormatString, CultureInfo culture, out DateTime dt)
|
|
|
|
|
{
|
|
|
|
|
if (DateTime.TryParseExact(text, dateFormatString, culture, DateTimeStyles.RoundtripKind, out DateTime temp))
|
|
|
|
|
{
|
|
|
|
|
temp = EnsureDateTime(temp, dateTimeZoneHandling);
|
|
|
|
|
dt = temp;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if HAVE_DATE_TIME_OFFSET
|
|
|
|
|
private static bool TryParseDateTimeOffsetMicrosoft(StringReference text, out DateTimeOffset dt)
|
|
|
|
|
{
|
|
|
|
|
if (!TryParseMicrosoftDate(text, out long ticks, out TimeSpan offset, out _))
|
|
|
|
|
{
|
|
|
|
|
dt = default(DateTime);
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DateTime utcDateTime = ConvertJavaScriptTicksToDateTime(ticks);
|
|
|
|
|
|
|
|
|
|
dt = new DateTimeOffset(utcDateTime.Add(offset).Ticks, offset);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static bool TryParseDateTimeOffsetExact(string text, string dateFormatString, CultureInfo culture, out DateTimeOffset dt)
|
|
|
|
|
{
|
|
|
|
|
if (DateTimeOffset.TryParseExact(text, dateFormatString, culture, DateTimeStyles.RoundtripKind, out DateTimeOffset temp))
|
|
|
|
|
{
|
|
|
|
|
dt = temp;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dt = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
private static bool TryReadOffset(StringReference offsetText, int startIndex, out TimeSpan offset)
|
|
|
|
|
{
|
|
|
|
|
bool negative = (offsetText[startIndex] == '-');
|
|
|
|
|
|
|
|
|
|
if (ConvertUtils.Int32TryParse(offsetText.Chars, startIndex + 1, 2, out int hours) != ParseResult.Success)
|
|
|
|
|
{
|
|
|
|
|
offset = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int minutes = 0;
|
|
|
|
|
if (offsetText.Length - startIndex > 5)
|
|
|
|
|
{
|
|
|
|
|
if (ConvertUtils.Int32TryParse(offsetText.Chars, startIndex + 3, 2, out minutes) != ParseResult.Success)
|
|
|
|
|
{
|
|
|
|
|
offset = default;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
offset = TimeSpan.FromHours(hours) + TimeSpan.FromMinutes(minutes);
|
|
|
|
|
if (negative)
|
|
|
|
|
{
|
|
|
|
|
offset = offset.Negate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
#region Write
|
|
|
|
|
internal static void WriteDateTimeString(TextWriter writer, DateTime value, DateFormatHandling format, string? formatString, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
if (StringUtils.IsNullOrEmpty(formatString))
|
|
|
|
|
{
|
|
|
|
|
char[] chars = new char[64];
|
|
|
|
|
int pos = WriteDateTimeString(chars, 0, value, null, value.Kind, format);
|
|
|
|
|
writer.Write(chars, 0, pos);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
writer.Write(value.ToString(formatString, culture));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static int WriteDateTimeString(char[] chars, int start, DateTime value, TimeSpan? offset, DateTimeKind kind, DateFormatHandling format)
|
|
|
|
|
{
|
|
|
|
|
int pos = start;
|
|
|
|
|
|
|
|
|
|
if (format == DateFormatHandling.MicrosoftDateFormat)
|
|
|
|
|
{
|
|
|
|
|
TimeSpan o = offset ?? value.GetUtcOffset();
|
|
|
|
|
|
|
|
|
|
long javaScriptTicks = ConvertDateTimeToJavaScriptTicks(value, o);
|
|
|
|
|
|
|
|
|
|
@"\/Date(".CopyTo(0, chars, pos, 7);
|
|
|
|
|
pos += 7;
|
|
|
|
|
|
|
|
|
|
string ticksText = javaScriptTicks.ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
ticksText.CopyTo(0, chars, pos, ticksText.Length);
|
|
|
|
|
pos += ticksText.Length;
|
|
|
|
|
|
|
|
|
|
switch (kind)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeKind.Unspecified:
|
|
|
|
|
if (value != DateTime.MaxValue && value != DateTime.MinValue)
|
|
|
|
|
{
|
|
|
|
|
pos = WriteDateTimeOffset(chars, pos, o, format);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case DateTimeKind.Local:
|
|
|
|
|
pos = WriteDateTimeOffset(chars, pos, o, format);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@")\/".CopyTo(0, chars, pos, 3);
|
|
|
|
|
pos += 3;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
pos = WriteDefaultIsoDate(chars, pos, value);
|
|
|
|
|
|
|
|
|
|
switch (kind)
|
|
|
|
|
{
|
|
|
|
|
case DateTimeKind.Local:
|
|
|
|
|
pos = WriteDateTimeOffset(chars, pos, offset ?? value.GetUtcOffset(), format);
|
|
|
|
|
break;
|
|
|
|
|
case DateTimeKind.Utc:
|
|
|
|
|
chars[pos++] = 'Z';
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return pos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static int WriteDefaultIsoDate(char[] chars, int start, DateTime dt)
|
|
|
|
|
{
|
|
|
|
|
int length = 19;
|
|
|
|
|
|
|
|
|
|
GetDateValues(dt, out int year, out int month, out int day);
|
|
|
|
|
|
|
|
|
|
CopyIntToCharArray(chars, start, year, 4);
|
|
|
|
|
chars[start + 4] = '-';
|
|
|
|
|
CopyIntToCharArray(chars, start + 5, month, 2);
|
|
|
|
|
chars[start + 7] = '-';
|
|
|
|
|
CopyIntToCharArray(chars, start + 8, day, 2);
|
|
|
|
|
chars[start + 10] = 'T';
|
|
|
|
|
CopyIntToCharArray(chars, start + 11, dt.Hour, 2);
|
|
|
|
|
chars[start + 13] = ':';
|
|
|
|
|
CopyIntToCharArray(chars, start + 14, dt.Minute, 2);
|
|
|
|
|
chars[start + 16] = ':';
|
|
|
|
|
CopyIntToCharArray(chars, start + 17, dt.Second, 2);
|
|
|
|
|
|
|
|
|
|
int fraction = (int)(dt.Ticks % 10000000L);
|
|
|
|
|
|
|
|
|
|
if (fraction != 0)
|
|
|
|
|
{
|
|
|
|
|
int digits = 7;
|
|
|
|
|
while ((fraction % 10) == 0)
|
|
|
|
|
{
|
|
|
|
|
digits--;
|
|
|
|
|
fraction /= 10;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chars[start + 19] = '.';
|
|
|
|
|
CopyIntToCharArray(chars, start + 20, fraction, digits);
|
|
|
|
|
|
|
|
|
|
length += digits + 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return start + length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static void CopyIntToCharArray(char[] chars, int start, int value, int digits)
|
|
|
|
|
{
|
|
|
|
|
while (digits-- != 0)
|
|
|
|
|
{
|
|
|
|
|
chars[start + digits] = (char)((value % 10) + 48);
|
|
|
|
|
value /= 10;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
internal static int WriteDateTimeOffset(char[] chars, int start, TimeSpan offset, DateFormatHandling format)
|
|
|
|
|
{
|
|
|
|
|
chars[start++] = (offset.Ticks >= 0L) ? '+' : '-';
|
|
|
|
|
|
|
|
|
|
int absHours = Math.Abs(offset.Hours);
|
|
|
|
|
CopyIntToCharArray(chars, start, absHours, 2);
|
|
|
|
|
start += 2;
|
|
|
|
|
|
|
|
|
|
if (format == DateFormatHandling.IsoDateFormat)
|
|
|
|
|
{
|
|
|
|
|
chars[start++] = ':';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int absMinutes = Math.Abs(offset.Minutes);
|
|
|
|
|
CopyIntToCharArray(chars, start, absMinutes, 2);
|
|
|
|
|
start += 2;
|
|
|
|
|
|
|
|
|
|
return start;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if HAVE_DATE_TIME_OFFSET
|
|
|
|
|
internal static void WriteDateTimeOffsetString(TextWriter writer, DateTimeOffset value, DateFormatHandling format, string? formatString, CultureInfo culture)
|
|
|
|
|
{
|
|
|
|
|
if (StringUtils.IsNullOrEmpty(formatString))
|
|
|
|
|
{
|
|
|
|
|
char[] chars = new char[64];
|
|
|
|
|
int pos = WriteDateTimeString(chars, 0, (format == DateFormatHandling.IsoDateFormat) ? value.DateTime : value.UtcDateTime, value.Offset, DateTimeKind.Local, format);
|
|
|
|
|
|
|
|
|
|
writer.Write(chars, 0, pos);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
writer.Write(value.ToString(formatString, culture));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
private static void GetDateValues(DateTime td, out int year, out int month, out int day)
|
|
|
|
|
{
|
|
|
|
|
long ticks = td.Ticks;
|
|
|
|
|
// n = number of days since 1/1/0001
|
|
|
|
|
int n = (int)(ticks / TicksPerDay);
|
|
|
|
|
// y400 = number of whole 400-year periods since 1/1/0001
|
|
|
|
|
int y400 = n / DaysPer400Years;
|
|
|
|
|
// n = day number within 400-year period
|
|
|
|
|
n -= y400 * DaysPer400Years;
|
|
|
|
|
// y100 = number of whole 100-year periods within 400-year period
|
|
|
|
|
int y100 = n / DaysPer100Years;
|
|
|
|
|
// Last 100-year period has an extra day, so decrement result if 4
|
|
|
|
|
if (y100 == 4)
|
|
|
|
|
{
|
|
|
|
|
y100 = 3;
|
|
|
|
|
}
|
|
|
|
|
// n = day number within 100-year period
|
|
|
|
|
n -= y100 * DaysPer100Years;
|
|
|
|
|
// y4 = number of whole 4-year periods within 100-year period
|
|
|
|
|
int y4 = n / DaysPer4Years;
|
|
|
|
|
// n = day number within 4-year period
|
|
|
|
|
n -= y4 * DaysPer4Years;
|
|
|
|
|
// y1 = number of whole years within 4-year period
|
|
|
|
|
int y1 = n / DaysPerYear;
|
|
|
|
|
// Last year has an extra day, so decrement result if 4
|
|
|
|
|
if (y1 == 4)
|
|
|
|
|
{
|
|
|
|
|
y1 = 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
year = y400 * 400 + y100 * 100 + y4 * 4 + y1 + 1;
|
|
|
|
|
|
|
|
|
|
// n = day number within year
|
|
|
|
|
n -= y1 * DaysPerYear;
|
|
|
|
|
|
|
|
|
|
// Leap year calculation looks different from IsLeapYear since y1, y4,
|
|
|
|
|
// and y100 are relative to year 1, not year 0
|
|
|
|
|
bool leapYear = y1 == 3 && (y4 != 24 || y100 == 3);
|
|
|
|
|
int[] days = leapYear ? DaysToMonth366 : DaysToMonth365;
|
|
|
|
|
// All months have less than 32 days, so n >> 5 is a good conservative
|
|
|
|
|
// estimate for the month
|
|
|
|
|
int m = n >> 5 + 1;
|
|
|
|
|
// m = 1-based month number
|
|
|
|
|
while (n >= days[m])
|
|
|
|
|
{
|
|
|
|
|
m++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
month = m;
|
|
|
|
|
|
|
|
|
|
// Return 1-based day-of-month
|
|
|
|
|
day = n - days[m - 1] + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-31 11:49:13 +08:00
|
|
|
|
}
|