using System.Globalization; using Jint.Native; using Jint.Native.Date; namespace Jint.Runtime; public class DefaultTimeSystem : ITimeSystem { private static readonly string[] _defaultFormats = ["yyyy-MM-dd", "yyyy-MM", "yyyy"]; private static readonly string[] _secondaryFormats = [ "yyyy-MM-ddTHH:mm:ss.FFF", "yyyy-MM-ddTHH:mm:ss", "yyyy-MM-ddTHH:mm", // Formats used in DatePrototype toString methods "ddd MMM dd yyyy HH:mm:ss 'GMT'K", "ddd MMM dd yyyy", "HH:mm:ss 'GMT'K", // standard formats "yyyy-M-dTH:m:s.FFFK", "yyyy/M/dTH:m:s.FFFK", "yyyy-M-dTH:m:sK", "yyyy/M/dTH:m:sK", "yyyy-M-dTH:mK", "yyyy/M/dTH:mK", "yyyy-M-d H:m:s.FFFK", "yyyy/M/d H:m:s.FFFK", "yyyy-M-d H:m:sK", "yyyy/M/d H:m:sK", "yyyy-M-d H:mK", "yyyy/M/d H:mK", "yyyy-M-dK", "yyyy/M/dK", "yyyy-MK", "yyyy/MK", "yyyyK", "THH:mm:ss.FFFK", "THH:mm:ssK", "THH:mmK", "THHK" ]; private readonly CultureInfo _parsingCulture; public DefaultTimeSystem(TimeZoneInfo timeZoneInfo, CultureInfo parsingCulture) { _parsingCulture = parsingCulture; DefaultTimeZone = timeZoneInfo; } public virtual DateTimeOffset GetUtcNow() { return DateTimeOffset.UtcNow; } public TimeZoneInfo DefaultTimeZone { get; } public virtual bool TryParse(string date, out long epochMilliseconds) { epochMilliseconds = long.MinValue; if (string.IsNullOrEmpty(date)) { return false; } // special check for large years that always require + or - in front and have 6 digit year if ((date[0] == '+' || date[0] == '-') && date.IndexOf('-', 1) == 7) { return TryParseLargeYear(date, out epochMilliseconds); } var startParen = date.IndexOf('('); if (startParen != -1) { // informative text date = date.Substring(0, startParen); } date = date.Trim(); if (!DateTime.TryParseExact(date, _defaultFormats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result)) { if (!DateTime.TryParseExact(date, _secondaryFormats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)) { if (!DateTime.TryParse(date, _parsingCulture, DateTimeStyles.AdjustToUniversal, out result)) { if (!DateTime.TryParse(date, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)) { // fall back to trying with MimeKit if (DateUtils.TryParse(date, out var mimeKitResult)) { var dateAsUtc = mimeKitResult.ToUniversalTime(); #pragma warning disable MA0132 epochMilliseconds = (long) Math.Floor((dateAsUtc - DateConstructor.Epoch).TotalMilliseconds); #pragma warning restore MA0132 return true; } return false; } } } } var convertToUtcAfter = result.Kind == DateTimeKind.Unspecified; var dateAsUtc1 = result.Kind == DateTimeKind.Local ? result.ToUniversalTime() : DateTime.SpecifyKind(result, DateTimeKind.Utc); DatePresentation datePresentation = (long) (dateAsUtc1 - DateConstructor.Epoch).TotalMilliseconds; if (convertToUtcAfter) { var offset = GetUtcOffset(datePresentation.Value).TotalMilliseconds; datePresentation -= offset; } epochMilliseconds = datePresentation.Value; return true; } /// /// Supports parsing of large (> 10 000) and negative years, should not be needed that often... /// private static bool TryParseLargeYear(string date, out long epochMilliseconds) { epochMilliseconds = long.MinValue; var yearString = date.Substring(0, 7); if (!int.TryParse(yearString, NumberStyles.Integer, CultureInfo.InvariantCulture, out var year)) { return false; } if (year == 0 && date[0] == '-') { // cannot be negative zero ever return false; } // create replacement string #pragma warning disable CA1845 var dateToParse = "2000" + date.Substring(7); #pragma warning restore CA1845 if (!DateTime.TryParse(dateToParse, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out var parsed)) { return false; } var dateTime = parsed.ToUniversalTime(); var datePresentation = DatePrototype.MakeDate( DatePrototype.MakeDay(year, dateTime.Month - 1, dateTime.Day), DatePrototype.MakeTime(dateTime.Hour, dateTime.Minute, dateTime.Second, dateTime.Millisecond) ); epochMilliseconds = datePresentation.Value; return true; } public virtual TimeSpan GetUtcOffset(long epochMilliseconds) { // we have limited capabilities without addon like NodaTime if (epochMilliseconds is < -62135596800000L or > 253402300799999L) { return this.DefaultTimeZone.BaseUtcOffset; } return this.DefaultTimeZone.GetUtcOffset(DateTimeOffset.FromUnixTimeMilliseconds(epochMilliseconds)); } }