Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create ITimeSystem abstraction for date parsing/time zone logic #1488

Merged
merged 1 commit into from
Mar 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="MongoDB.Bson.signed" Version="2.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NodaTime" Version="3.1.6" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
</ItemGroup>
Expand Down
54 changes: 54 additions & 0 deletions Jint.Tests.PublicInterface/TimeSystemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
using System.Globalization;
using Jint.Runtime;
using NodaTime;

namespace Jint.Tests.PublicInterface;

public class TimeSystemTests
{
[Theory]
[InlineData("401, 0, 1, 0, 0, 0, 0", -49512821989000)]
[InlineData("1900, 0, 1, 0, 0, 0, 0", -2208994789000)]
[InlineData("1920, 0, 1, 0, 0, 0, 0", -1577929189000)]
[InlineData("1969, 0, 1, 0, 0, 0, 0", -31543200000)]
[InlineData("2000, 1, 1, 1, 1, 1, 1", 949359661001)]
public void CanProduceValidDatesUsingNodaTimeIntegration(string input, long expected)
{
var dateTimeZone = DateTimeZoneProviders.Tzdb["Europe/Helsinki"];
TimeZoneInfo timeZone;
try
{
timeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Helsinki");
}
catch (TimeZoneNotFoundException)
{
timeZone = TimeZoneInfo.FindSystemTimeZoneById("FLE Standard Time");
}

var engine = new Engine(options =>
{
options.TimeZone = timeZone;
options.TimeSystem = new NodaTimeSystem(dateTimeZone, timeZone);
});

Assert.Equal(expected, engine.Evaluate($"new Date({input}) * 1").AsNumber());
}
}

file sealed class NodaTimeSystem : DefaultTimeSystem
{
private readonly DateTimeZone _dateTimeZone;

public NodaTimeSystem(
DateTimeZone dateTimeZone,
TimeZoneInfo timeZoneInfo) : base(timeZoneInfo, CultureInfo.CurrentCulture)
{
_dateTimeZone = dateTimeZone;
}

public override TimeSpan GetUtcOffset(long epochMilliseconds)
{
var offset = _dateTimeZone.GetUtcOffset(Instant.FromUnixTimeMilliseconds(epochMilliseconds));
return offset.ToTimeSpan();
}
}
6 changes: 0 additions & 6 deletions Jint.Tests.Test262/Test262Harness.settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@
// Windows line ending differences
"built-ins/String/raw/special-characters.js",

// parsing of large/small years not implemented in .NET (-271821, +271821)
"built-ins/Date/parse/time-value-maximum-range.js",

// delete/add detection not implemented for map iterator during iteration
"built-ins/Map/prototype/forEach/iterates-values-deleted-then-readded.js",
"built-ins/MapIteratorPrototype/next/iteration-mutable.js",
Expand Down Expand Up @@ -183,9 +180,6 @@
// special casing data
"built-ins/**/special_casing*.js",

// negative years, we can partially handle but not values too big due to .NET parsing limitations
"built-ins/Date/prototype/*/negative-year.js",

// failing tests in new test suite (due to updating to latest and using whole set)
"language/arguments-object/mapped/nonconfigurable-descriptors-define-failure.js",
"language/eval-code/direct/arrow-fn-a-following-parameter-is-named-arguments-arrow-func-declare-arguments-assign-incl-def-param-arrow-arguments.js",
Expand Down
1 change: 0 additions & 1 deletion Jint.Tests/Jint.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="MongoDB.Bson.signed" Version="2.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NodaTime" Version="3.1.6" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5" />
</ItemGroup>
Expand Down
34 changes: 0 additions & 34 deletions Jint.Tests/Runtime/DateTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
using NodaTime;

namespace Jint.Tests.Runtime;

public class DateTests
Expand Down Expand Up @@ -100,38 +98,6 @@ public void CanParseLocaleString(string input, long expected)
Assert.Equal(expected, _engine.Evaluate($"new Date('{input}') * 1").AsNumber());
}

[Theory]
[InlineData("401, 0, 1, 0, 0, 0, 0", -49512821989000)]
[InlineData("1900, 0, 1, 0, 0, 0, 0", -2208994789000)]
[InlineData("1920, 0, 1, 0, 0, 0, 0", -1577929189000)]
[InlineData("1969, 0, 1, 0, 0, 0, 0", -31543200000)]
[InlineData("2000, 1, 1, 1, 1, 1, 1", 949359661001)]
public void CanProduceValidDatesUsingNodaTimeIntegration(string input, long expected)
{
var dateTimeZone = DateTimeZoneProviders.Tzdb["Europe/Helsinki"];
TimeZoneInfo timeZone;
try
{
timeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Helsinki");
}
catch (TimeZoneNotFoundException)
{
timeZone = TimeZoneInfo.FindSystemTimeZoneById("FLE Standard Time");
}

var engine = new Engine(options =>
{
options.TimeZone = timeZone;
options.GetUtcOffset = milliseconds =>
{
var offset = dateTimeZone.GetUtcOffset(Instant.FromUnixTimeMilliseconds(milliseconds));
return offset.ToTimeSpan();
};
});

Assert.Equal(expected, engine.Evaluate($"new Date({input}) * 1").AsNumber());
}

[Fact]
public void CanUseMoment()
{
Expand Down
102 changes: 7 additions & 95 deletions Jint/Native/Date/DateConstructor.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Globalization;
using Jint.Collections;
using Jint.Native.Function;
using Jint.Native.Object;
Expand All @@ -14,48 +13,8 @@ namespace Jint.Native.Date;
internal sealed class DateConstructor : Constructor
{
internal static readonly DateTime Epoch = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

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 static readonly JsString _functionName = new JsString("Date");
private readonly ITimeSystem _timeSystem;

internal DateConstructor(
Engine engine,
Expand All @@ -68,6 +27,7 @@ internal sealed class DateConstructor : Constructor
PrototypeObject = new DatePrototype(engine, this, objectPrototype);
_length = new PropertyDescriptor(7, PropertyFlag.Configurable);
_prototypeDescriptor = new PropertyDescriptor(PrototypeObject, PropertyFlag.AllForbidden);
_timeSystem = engine.Options.TimeSystem;
}

internal DatePrototype PrototypeObject { get; }
Expand Down Expand Up @@ -101,43 +61,13 @@ private JsValue Parse(JsValue thisObj, JsValue[] arguments)
/// </summary>
private DatePresentation ParseFromString(string date)
{
var negative = date.StartsWith("-");
if (negative)
{
date = date.Substring(1);
}

var startParen = date.IndexOf('(');
if (startParen != -1)
if (_timeSystem.TryParse(date, out var result))
{
// 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, Engine.Options.Culture, 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))
{
return FromDateTimeOffset(mimeKitResult);
}

// unrecognized dates should return NaN (15.9.4.2)
return DatePresentation.NaN;
}
}
}
return result;
}

return FromDateTime(result, negative);
// unrecognized dates should return NaN
return DatePresentation.NaN;
}

/// <summary>
Expand Down Expand Up @@ -262,25 +192,6 @@ private JsDate Construct(DatePresentation time)
static (engine, _, dateValue) => new JsDate(engine, dateValue), time);
}

private static long FromDateTimeOffset(DateTimeOffset dt, bool negative = false)
{
var dateAsUtc = dt.ToUniversalTime();

double result;
if (negative)
{
var zero = (Epoch - DateTime.MinValue).TotalMilliseconds;
result = zero - TimeSpan.FromTicks(dateAsUtc.Ticks).TotalMilliseconds;
result *= -1;
}
else
{
result = (dateAsUtc - Epoch).TotalMilliseconds;
}

return (long) System.Math.Floor(result);
}

internal DatePresentation FromDateTime(DateTime dt, bool negative = false)
{
if (dt == DateTime.MinValue)
Expand Down Expand Up @@ -319,4 +230,5 @@ internal DatePresentation FromDateTime(DateTime dt, bool negative = false)

return result;
}

}
36 changes: 24 additions & 12 deletions Jint/Native/Date/DatePrototype.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System.Globalization;
using System.Runtime.CompilerServices;
using Jint.Collections;
using Jint.Native.Object;
Expand All @@ -21,6 +20,7 @@ internal sealed class DatePrototype : Prototype
private const double MaxMonth = -MinMonth;

private readonly DateConstructor _constructor;
private readonly ITimeSystem _timeSystem;

internal DatePrototype(
Engine engine,
Expand All @@ -30,6 +30,7 @@ internal sealed class DatePrototype : Prototype
{
_prototype = objectPrototype;
_constructor = constructor;
_timeSystem = engine.Options.TimeSystem;
}

protected override void Initialize()
Expand Down Expand Up @@ -84,7 +85,7 @@ protected override void Initialize()
["setUTCFullYear"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "setUTCFullYear", SetUTCFullYear, 3, lengthFlags), propertyFlags),
["toUTCString"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "toUTCString", ToUtcString, 0, lengthFlags), propertyFlags),
["toISOString"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "toISOString", ToISOString, 0, lengthFlags), propertyFlags),
["toJSON"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "toJSON", ToJSON, 1, lengthFlags), propertyFlags)
["toJSON"] = new PropertyDescriptor(new ClrFunctionInstance(Engine, "toJSON", ToJson, 1, lengthFlags), propertyFlags)
};
SetProperties(properties);

Expand Down Expand Up @@ -772,10 +773,13 @@ private JsValue ToUtcString(JsValue thisObj, JsValue[] arguments)
return "Invalid Date";
}

var weekday = _dayNames[WeekDay(tv)];
var month = _monthNames[MonthFromTime(tv)];
var day = DateFromTime(tv).ToString("00");
var yv = YearFromTime(tv);
var universalTime = tv.ToDateTime().ToUniversalTime();
var paddedYear = yv.ToString().PadLeft(4, '0');
return universalTime.ToString($"ddd, dd MMM {paddedYear} HH:mm:ss 'GMT'", CultureInfo.InvariantCulture);
var paddedYear = yv.ToString("0000");

return $"{weekday}, {day} {month} {paddedYear} {TimeString(tv)}";
}

/// <summary>
Expand Down Expand Up @@ -807,10 +811,18 @@ private JsValue ToISOString(JsValue thisObj, JsValue[] arguments)
if (ms < 0) { ms += MsPerSecond; }

var (year, month, day) = YearMonthDayFromTime(t);
return $"{year:0000}-{month:00}-{day:00}T{h:00}:{m:00}:{s:00}.{ms:000}Z";
month++;

var formatted = $"{year:0000}-{month:00}-{day:00}T{h:00}:{m:00}:{s:00}.{ms:000}Z";
if (year > 9999)
{
formatted = "+" + formatted;
}

return formatted;
}

private JsValue ToJSON(JsValue thisObj, JsValue[] arguments)
private JsValue ToJson(JsValue thisObj, JsValue[] arguments)
{
var o = TypeConverter.ToObject(_realm, thisObj);
var tv = TypeConverter.ToPrimitive(o, Types.Number);
Expand Down Expand Up @@ -1094,7 +1106,7 @@ private static int WeekDay(DatePresentation t)

private DateTime ToLocalTime(DatePresentation t)
{
var utcOffset = _engine.Options.GetUtcOffset(t.Value).TotalMilliseconds;
var utcOffset = _timeSystem.GetUtcOffset(t.Value).TotalMilliseconds;
return (t + utcOffset).ToDateTime();
}

Expand All @@ -1108,13 +1120,13 @@ private DatePresentation LocalTime(DatePresentation t)
return DatePresentation.NaN;
}

var offset = Engine.Options.GetUtcOffset(t.Value).TotalMilliseconds;
var offset = _timeSystem.GetUtcOffset(t.Value).TotalMilliseconds;
return t + offset;
}

internal DatePresentation Utc(DatePresentation t)
{
var offset = Engine.Options.GetUtcOffset(t.Value).TotalMilliseconds;
var offset = _timeSystem.GetUtcOffset(t.Value).TotalMilliseconds;
return t - offset;
}

Expand Down Expand Up @@ -1376,7 +1388,7 @@ private static string TimeString(DatePresentation t)
/// </summary>
private string TimeZoneString(DatePresentation tv)
{
var offset = Engine.Options.GetUtcOffset(tv.Value).TotalMilliseconds;
var offset = _timeSystem.GetUtcOffset(tv.Value).TotalMilliseconds;

string offsetSign;
double absOffset;
Expand All @@ -1394,7 +1406,7 @@ private string TimeZoneString(DatePresentation tv)
var offsetMin = MinFromTime(absOffset).ToString("00");
var offsetHour = HourFromTime(absOffset).ToString("00");

var tzName = " (" + _engine.Options.TimeZone.StandardName + ")";
var tzName = " (" + _timeSystem.DefaultTimeZone.StandardName + ")";

return offsetSign + offsetHour + offsetMin + tzName;
}
Expand Down