Skip to content

Commit 262c5ac

Browse files
spahnkeoliverbock
authored andcommitted
Correct date conversions
We need to be very careful with date conversions because C# does not use the IANA timezone database to determine offsets from UTC / daylight savings time. However, V8 does use the IANA timezone database which lead to discrepancies in the date conversion. Example: Germany did not observe daylight savings time from 1950-1979 and therefore observed UTC+1 throughout the whole year. However, C# thinks Germany did observe daylight savings time and assumes incorrectly that Germany observed UTC+2 during the summer time. V8 new Date(1978, 5, 15) // "Thu Jun 15 1978 00:00:00 GMT+0100 (Mitteleuropäische Normalzeit)" C# If we get the ticks since 1970-01-01 from V8 to construct a UTC DateTime object we get "1978-06-14 23:00:00" which is correct. A subsequent call to .ToLocalTime() on that date object yields "1978-06-15 01:00:00" which is wrong; it should be "1978-06-15 00:00:00"! The same problem exists in the other direction. We therefore construct date objects directly from the date components we get from V8 and vice versa.
1 parent 7763f82 commit 262c5ac

File tree

3 files changed

+134
-10
lines changed

3 files changed

+134
-10
lines changed

Source/Noesis.Javascript/JavascriptInterop.cpp

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ JavascriptInterop::ConvertFromV8(Handle<Value> iValue, ConvertedObjects &already
130130
if (iValue->IsArray())
131131
return ConvertArrayFromV8(iValue, already_converted);
132132
if (iValue->IsDate())
133-
return ConvertDateFromV8(iValue);
133+
return ConvertDateFromV8(iValue.As<Date>());
134134
if (iValue->IsRegExp())
135135
return ConvertRegexFromV8(iValue);
136136
if (iValue->IsFunction())
@@ -199,8 +199,8 @@ JavascriptInterop::ConvertToV8(System::Object^ iObject)
199199
return v8::Number::New(isolate, safe_cast<float>(iObject));
200200
if (type == System::Decimal::typeid)
201201
return v8::Number::New(isolate, (double)safe_cast<System::Decimal>(iObject));
202-
if (type == System::DateTime::typeid)
203-
return v8::Date::New(isolate->GetCurrentContext(), SystemInterop::ConvertFromSystemDateTime(safe_cast<System::DateTime^>(iObject))).ToLocalChecked();
202+
if (type == System::DateTime::typeid)
203+
return ConvertDateTimeToV8(safe_cast<System::DateTime^>(iObject));
204204
}
205205
}
206206
if (type == System::String::typeid)
@@ -357,13 +357,73 @@ JavascriptInterop::ConvertObjectFromV8(Handle<Object> iObject, ConvertedObjects
357357

358358
////////////////////////////////////////////////////////////////////////////////////////////////////
359359

360-
System::DateTime^
361-
JavascriptInterop::ConvertDateFromV8(Handle<Value> iValue)
360+
/*
361+
* We need to be very careful with date conversions because C# does not use the IANA timezone database
362+
* to determine offsets from UTC / daylight savings time. However, V8 does use the IANA timezone
363+
* database which lead to discrepancies in the date conversion.
364+
*
365+
* Example:
366+
* Germany did not observe daylight savings time from 1950-1979 and therefore observed UTC+1
367+
* throughout the whole year. However, C# thinks Germany did observe daylight savings time
368+
* and assumes incorrectly that Germany observed UTC+2 during the summer time.
369+
*
370+
* V8
371+
* new Date(1978, 5, 15) // "Thu Jun 15 1978 00:00:00 GMT+0100 (Mitteleuropäische Normalzeit)"
372+
*
373+
* C#
374+
* If we get the ticks since 1970-01-01 from V8 to construct a UTC DateTime object we get
375+
* "1978-06-14 23:00:00" which is correct. A subsequent call to .ToLocalTime() on that date object
376+
* yields "1978-06-15 01:00:00" which is wrong; it should be "1978-06-15 00:00:00"!
377+
*
378+
* The same problem exists in the other direction.
379+
*
380+
* We therefore construct date objects directly from the date components we get from V8 and vice versa.
381+
*/
382+
383+
double GetDateComponent(Isolate* isolate, Handle<Date> date, const char* component)
362384
{
363-
System::DateTime^ startDate = gcnew System::DateTime(1970, 1, 1, 0, 0, 0, 0, System::DateTimeKind::Utc);
364-
double milliseconds = iValue->NumberValue(JavascriptContext::GetCurrentIsolate()->GetCurrentContext()).ToChecked();
365-
System::TimeSpan^ timespan = System::TimeSpan::FromMilliseconds(milliseconds);
366-
return System::DateTime(timespan->Ticks + startDate->Ticks).ToLocalTime();
385+
auto getComponent = date->Get(isolate->GetCurrentContext(), String::NewFromUtf8(isolate, component)).ToLocalChecked().As<Function>();
386+
auto componentValue = getComponent->Call(date, 0, nullptr);
387+
return componentValue->NumberValue(isolate->GetCurrentContext()).ToChecked();
388+
}
389+
390+
void SetDateComponent(Isolate* isolate, Handle<Date> date, const char* component, double value)
391+
{
392+
auto getComponent = date->Get(isolate->GetCurrentContext(), String::NewFromUtf8(isolate, component)).ToLocalChecked().As<Function>();
393+
Handle<Value> parameters[] = { JavascriptInterop::ConvertToV8(value) };
394+
getComponent->Call(date, 1, parameters);
395+
}
396+
397+
System::DateTime^ JavascriptInterop::ConvertDateFromV8(Handle<Date> date)
398+
{
399+
auto isolate = JavascriptContext::GetCurrentIsolate();
400+
auto year = GetDateComponent(isolate, date, "getFullYear");
401+
auto month = GetDateComponent(isolate, date, "getMonth") + 1;
402+
auto day = GetDateComponent(isolate, date, "getDate");
403+
auto hour = GetDateComponent(isolate, date, "getHours");
404+
auto minute = GetDateComponent(isolate, date, "getMinutes");
405+
auto second = GetDateComponent(isolate, date, "getSeconds");
406+
auto millisecond = GetDateComponent(isolate, date, "getMilliseconds");
407+
return gcnew System::DateTime(year, month, day, hour, minute, second, millisecond, System::DateTimeKind::Local);
408+
}
409+
410+
Handle<Date> JavascriptInterop::ConvertDateTimeToV8(System::DateTime^ dateTime)
411+
{
412+
auto isolate = JavascriptContext::GetCurrentIsolate();
413+
EscapableHandleScope handleScope(isolate);
414+
415+
auto date = v8::Date::New(isolate->GetCurrentContext(), SystemInterop::ConvertFromSystemDateTime(dateTime)).ToLocalChecked().As<Date>();
416+
if (dateTime->Kind == System::DateTimeKind::Utc)
417+
return handleScope.Escape(date);
418+
419+
SetDateComponent(isolate, date, "setFullYear", dateTime->Year);
420+
SetDateComponent(isolate, date, "setMonth", dateTime->Month - 1);
421+
SetDateComponent(isolate, date, "setDate", dateTime->Day);
422+
SetDateComponent(isolate, date, "setHours", dateTime->Hour);
423+
SetDateComponent(isolate, date, "setMinutes", dateTime->Minute);
424+
SetDateComponent(isolate, date, "setSeconds", dateTime->Second);
425+
SetDateComponent(isolate, date, "setMilliseconds", dateTime->Millisecond);
426+
return handleScope.Escape(date);
367427
}
368428

369429
////////////////////////////////////////////////////////////////////////////////////////////////////

Source/Noesis.Javascript/JavascriptInterop.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,9 @@ class JavascriptInterop
9090

9191
static System::Object^ ConvertObjectFromV8(Handle<Object> iObject, ConvertedObjects &already_converted);
9292

93-
static System::DateTime^ ConvertDateFromV8(Handle<Value> iValue);
93+
static System::DateTime^ ConvertDateFromV8(Handle<Date> iValue);
94+
95+
static Handle<Date> ConvertDateTimeToV8(System::DateTime^ dateTime);
9496

9597
static System::Text::RegularExpressions::Regex^ ConvertRegexFromV8(Handle<Value> iValue);
9698

Tests/Noesis.Javascript.Tests/DateTest.cs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,5 +88,67 @@ public void CreateFixedDateInJavaScript()
8888
DateTime dateAsReportedByV8 = (DateTime)_context.Run("new Date(2010, 9, 10)");
8989
dateAsReportedByV8.Should().Be(new DateTime(2010, 10, 10));
9090
}
91+
92+
[TestMethod]
93+
public void SetDateTimeUtc_DateWhereTimezoneDatabaseIsImportant()
94+
{
95+
_context.SetParameter("val", new DateTime(1978, 6, 15, 0, 0, 0, DateTimeKind.Utc));
96+
97+
_context.Run("val.getUTCFullYear()").Should().BeOfType<int>().Which.Should().Be(1978);
98+
_context.Run("val.getUTCMonth()").Should().BeOfType<int>().Which.Should().Be(5);
99+
_context.Run("val.getUTCDate()").Should().BeOfType<int>().Which.Should().Be(15);
100+
_context.Run("val.getUTCHours()").Should().BeOfType<int>().Which.Should().Be(0);
101+
_context.Run("val.getUTCMinutes()").Should().BeOfType<int>().Which.Should().Be(0);
102+
_context.Run("val.getUTCSeconds()").Should().BeOfType<int>().Which.Should().Be(0);
103+
}
104+
105+
[TestMethod]
106+
public void SetDateTimeLocal_DateWhereTimezoneDatabaseIsImportant()
107+
{
108+
_context.SetParameter("val", new DateTime(1978, 6, 15, 0, 0, 0, DateTimeKind.Local));
109+
110+
_context.Run("val.getFullYear()").Should().BeOfType<int>().Which.Should().Be(1978);
111+
_context.Run("val.getMonth()").Should().BeOfType<int>().Which.Should().Be(5);
112+
_context.Run("val.getDate()").Should().BeOfType<int>().Which.Should().Be(15);
113+
_context.Run("val.getHours()").Should().BeOfType<int>().Which.Should().Be(0);
114+
_context.Run("val.getMinutes()").Should().BeOfType<int>().Which.Should().Be(0);
115+
_context.Run("val.getSeconds()").Should().BeOfType<int>().Which.Should().Be(0);
116+
}
117+
118+
[TestMethod]
119+
[Ignore]
120+
public void SetAndReadDateTimeUtc_DateWhereTimezoneDatabaseIsImportant()
121+
{
122+
var dateTime = new DateTime(1978, 6, 15, 0, 0, 0, DateTimeKind.Utc);
123+
_context.SetParameter("val", dateTime);
124+
125+
var dateFromV8 = (DateTime) _context.Run("val");
126+
dateFromV8.ToUniversalTime().Should().Be(dateTime); // this cannot work without an external dependency like NodaTime
127+
}
128+
129+
[TestMethod]
130+
public void SetAndReadDateTimeLocal_DateWhereTimezoneDatabaseIsImportant()
131+
{
132+
var dateTime = new DateTime(1978, 6, 15, 0, 0, 0, DateTimeKind.Local);
133+
_context.SetParameter("val", dateTime);
134+
135+
_context.Run("val").Should().Be(dateTime);
136+
}
137+
138+
[TestMethod]
139+
public void SetAndReadDateTimeUnspecified_DateWhereTimezoneDatabaseIsImportant()
140+
{
141+
var dateTime = new DateTime(1978, 6, 15);
142+
_context.SetParameter("val", dateTime);
143+
144+
_context.Run("val").Should().Be(dateTime);
145+
}
146+
147+
[TestMethod]
148+
public void CreateFixedDateInJavaScript_DateWhereTimezoneDatabaseIsImportant()
149+
{
150+
DateTime dateAsReportedByV8 = (DateTime) _context.Run("new Date(1978, 5, 15)");
151+
dateAsReportedByV8.Should().Be(new DateTime(1978, 6, 15));
152+
}
91153
}
92154
}

0 commit comments

Comments
 (0)