Skip to content

CSHARP-3268: Investigate Military Time Failures #1687

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 27 additions & 25 deletions src/MongoDB.Bson/IO/JsonReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1373,7 +1373,9 @@ private DateTime ParseJavaScriptDateTimeString(string dateTimeString)
{
// if DateTime.TryParse succeeds we're done, otherwise assume it's an RFC 822 formatted DateTime string
DateTime dateTime;
if (DateTime.TryParse(dateTimeString, out dateTime))

// DateTime.Parse doesn't understand military time zones, so don't call it when a military time zone is present
if (!Regex.IsMatch(dateTimeString, " [A-Y]$") && DateTime.TryParse(dateTimeString, out dateTime))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's my suggestion:

Either we support military time zones or we don't. And if we do support them, then we need to support ALL military time zones reliably.

Since DateTime.TryParse does not understand military time zones, use a regex to detect the presence of a military time zone and do NOT call DateTime.TryParse in that case. The reason to NOT call DateTime.TryParse is that DateTime.TryParse gets confused by SOME military time zones and interprets them as something else.

I'm not sure why we call DateTime.TryParse at all here... since we have our own parsing for RFC 822 format date strings. I guess the idea here is to be "lenient" in what we accept, and that we will parse anything that DateTime.TryParse understands and fall back to our own RFC 822 datetime parser for anything DateTime.TryParse does not understand.

{
return dateTime;
}
Expand Down Expand Up @@ -1479,30 +1481,30 @@ private DateTime ParseJavaScriptDateTimeString(string dateTimeString)
case "MDT": offset = TimeSpan.FromHours(-6); break;
case "PST": offset = TimeSpan.FromHours(-8); break;
case "PDT": offset = TimeSpan.FromHours(-7); break;
case "A": offset = TimeSpan.FromHours(-1); break;
case "B": offset = TimeSpan.FromHours(-2); break;
case "C": offset = TimeSpan.FromHours(-3); break;
case "D": offset = TimeSpan.FromHours(-4); break;
case "E": offset = TimeSpan.FromHours(-5); break;
case "F": offset = TimeSpan.FromHours(-6); break;
case "G": offset = TimeSpan.FromHours(-7); break;
case "H": offset = TimeSpan.FromHours(-8); break;
case "I": offset = TimeSpan.FromHours(-9); break;
case "K": offset = TimeSpan.FromHours(-10); break;
case "L": offset = TimeSpan.FromHours(-11); break;
case "M": offset = TimeSpan.FromHours(-12); break;
case "N": offset = TimeSpan.FromHours(1); break;
case "O": offset = TimeSpan.FromHours(2); break;
case "P": offset = TimeSpan.FromHours(3); break;
case "Q": offset = TimeSpan.FromHours(4); break;
case "R": offset = TimeSpan.FromHours(5); break;
case "S": offset = TimeSpan.FromHours(6); break;
case "T": offset = TimeSpan.FromHours(7); break;
case "U": offset = TimeSpan.FromHours(8); break;
case "V": offset = TimeSpan.FromHours(9); break;
case "W": offset = TimeSpan.FromHours(10); break;
case "X": offset = TimeSpan.FromHours(11); break;
case "Y": offset = TimeSpan.FromHours(12); break;
case "A": offset = TimeSpan.FromHours(1); break;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sign of all the offsets for A to Y was backwards.

case "B": offset = TimeSpan.FromHours(2); break;
case "C": offset = TimeSpan.FromHours(3); break;
case "D": offset = TimeSpan.FromHours(4); break;
case "E": offset = TimeSpan.FromHours(5); break;
case "F": offset = TimeSpan.FromHours(6); break;
case "G": offset = TimeSpan.FromHours(7); break;
case "H": offset = TimeSpan.FromHours(8); break;
case "I": offset = TimeSpan.FromHours(9); break;
case "K": offset = TimeSpan.FromHours(10); break;
case "L": offset = TimeSpan.FromHours(11); break;
case "M": offset = TimeSpan.FromHours(12); break;
case "N": offset = TimeSpan.FromHours(-1); break;
case "O": offset = TimeSpan.FromHours(-2); break;
case "P": offset = TimeSpan.FromHours(-3); break;
case "Q": offset = TimeSpan.FromHours(-4); break;
case "R": offset = TimeSpan.FromHours(-5); break;
case "S": offset = TimeSpan.FromHours(-6); break;
case "T": offset = TimeSpan.FromHours(-7); break;
case "U": offset = TimeSpan.FromHours(-8); break;
case "V": offset = TimeSpan.FromHours(-9); break;
case "W": offset = TimeSpan.FromHours(-10); break;
case "X": offset = TimeSpan.FromHours(-11); break;
case "Y": offset = TimeSpan.FromHours(-12); break;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you mean that we should add a case for "Z" here?

I don't see how we would ever reach here with a "Z" because DateTime.TryParse would have already handled it.

default:
var offsetSign = zone.Substring(0);
var offsetHours = zone.Substring(1, 2);
Expand Down
213 changes: 99 additions & 114 deletions tests/MongoDB.Bson.Tests/Jira/CSharp275Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,125 +22,110 @@ namespace MongoDB.Bson.Tests.Jira
{
public class CSharp275Tests
{
private class Test
{
public string Json;
public string Iso;
public Test(string json, string iso)
{
this.Json = json;
this.Iso = iso;
}
}

private Test[] _tests = new Test[]
{
public static object[][] TestParseDatesData => [
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Convert to supply data to a Theory.

// note: use EST/EDT in all Json values to ensure DateTime.Parse doesn't work
// test with dayOfWeek
new Test("Mon, 10 Oct 2011 11:22:33 EDT", "2011-10-10T11:22:33-04:00"),
new Test("Tue, 11 Oct 2011 11:22:33 EDT", "2011-10-11T11:22:33-04:00"),
new Test("Wed, 12 Oct 2011 11:22:33 EDT", "2011-10-12T11:22:33-04:00"),
new Test("Thu, 13 Oct 2011 11:22:33 EDT", "2011-10-13T11:22:33-04:00"),
new Test("Fri, 14 Oct 2011 11:22:33 EDT", "2011-10-14T11:22:33-04:00"),
new Test("Sat, 15 Oct 2011 11:22:33 EDT", "2011-10-15T11:22:33-04:00"),
new Test("Sun, 16 Oct 2011 11:22:33 EDT", "2011-10-16T11:22:33-04:00"),
// test without dayOfWeek
new Test("10 Oct 2011 11:22:33 EDT", "2011-10-10T11:22:33-04:00"),
new Test("11 Oct 2011 11:22:33 EDT", "2011-10-11T11:22:33-04:00"),
new Test("12 Oct 2011 11:22:33 EDT", "2011-10-12T11:22:33-04:00"),
new Test("13 Oct 2011 11:22:33 EDT", "2011-10-13T11:22:33-04:00"),
new Test("14 Oct 2011 11:22:33 EDT", "2011-10-14T11:22:33-04:00"),
new Test("15 Oct 2011 11:22:33 EDT", "2011-10-15T11:22:33-04:00"),
new Test("16 Oct 2011 11:22:33 EDT", "2011-10-16T11:22:33-04:00"),
// test monthName
new Test("1 Jan 2011 11:22:33 EST", "2011-01-01T11:22:33-05:00"),
new Test("1 Feb 2011 11:22:33 EST", "2011-02-01T11:22:33-05:00"),
new Test("1 Mar 2011 11:22:33 EST", "2011-03-01T11:22:33-05:00"),
new Test("1 Apr 2011 11:22:33 EDT", "2011-04-01T11:22:33-04:00"),
new Test("1 May 2011 11:22:33 EDT", "2011-05-01T11:22:33-04:00"),
new Test("1 Jun 2011 11:22:33 EDT", "2011-06-01T11:22:33-04:00"),
new Test("1 Jul 2011 11:22:33 EDT", "2011-07-01T11:22:33-04:00"),
new Test("1 Aug 2011 11:22:33 EDT", "2011-08-01T11:22:33-04:00"),
new Test("1 Sep 2011 11:22:33 EDT", "2011-09-01T11:22:33-04:00"),
new Test("1 Oct 2011 11:22:33 EDT", "2011-10-01T11:22:33-04:00"),
new Test("1 Nov 2011 11:22:33 EDT", "2011-11-01T11:22:33-04:00"),
new Test("1 Dec 2011 11:22:33 EST", "2011-12-01T11:22:33-05:00"),
// test 2-digit year
new Test("Mon, 1 Jan 01 11:22:33 EST", "2001-01-01T11:22:33-5:00"),
new Test("Mon, 1 Jan 29 11:22:33 EST", "2029-01-01T11:22:33-5:00"),
new Test("Tue, 1 Jan 30 11:22:33 EST", "2030-01-01T11:22:33-5:00"),
new Test("Wed, 1 Jan 31 11:22:33 EST", "2031-01-01T11:22:33-5:00"),
new Test("Thu, 1 Jan 32 11:22:33 EST", "2032-01-01T11:22:33-5:00"),
new Test("Fri, 1 Jan 99 11:22:33 EST", "1999-01-01T11:22:33-5:00"),
// test time zones
new Test("Mon, 10 Oct 2011 11:22:33 UT", "2011-10-10T11:22:33-00:00"),
new Test("Mon, 10 Oct 2011 11:22:33 GMT", "2011-10-10T11:22:33-00:00"),
new Test("Mon, 10 Oct 2011 11:22:33 EST", "2011-10-10T11:22:33-05:00"),
new Test("Mon, 10 Oct 2011 11:22:33 EDT", "2011-10-10T11:22:33-04:00"),
new Test("Mon, 10 Oct 2011 11:22:33 CST", "2011-10-10T11:22:33-06:00"),
new Test("Mon, 10 Oct 2011 11:22:33 CDT", "2011-10-10T11:22:33-05:00"),
new Test("Mon, 10 Oct 2011 11:22:33 MST", "2011-10-10T11:22:33-07:00"),
new Test("Mon, 10 Oct 2011 11:22:33 MDT", "2011-10-10T11:22:33-06:00"),
new Test("Mon, 10 Oct 2011 11:22:33 PST", "2011-10-10T11:22:33-08:00"),
new Test("Mon, 10 Oct 2011 11:22:33 PDT", "2011-10-10T11:22:33-07:00"),
// TODO: Investigate Military Time Failures
// https://jira.mongodb.org/browse/CSHARP-3268
//new Test("Mon, 10 Oct 2011 11:22:33 A", "2011-10-10T11:22:33+01:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 B", "2011-10-10T11:22:33+02:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 C", "2011-10-10T11:22:33+03:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 D", "2011-10-10T11:22:33+04:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 E", "2011-10-10T11:22:33+05:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 F", "2011-10-10T11:22:33+06:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 G", "2011-10-10T11:22:33+07:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 H", "2011-10-10T11:22:33+08:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 I", "2011-10-10T11:22:33+09:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 K", "2011-10-10T11:22:33+10:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 L", "2011-10-10T11:22:33+11:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 M", "2011-10-10T11:22:33+12:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 N", "2011-10-10T11:22:33-01:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 O", "2011-10-10T11:22:33-02:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 P", "2011-10-10T11:22:33-03:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 Q", "2011-10-10T11:22:33-04:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 R", "2011-10-10T11:22:33-05:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 S", "2011-10-10T11:22:33-06:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 T", "2011-10-10T11:22:33-07:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 U", "2011-10-10T11:22:33-08:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 V", "2011-10-10T11:22:33-09:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 W", "2011-10-10T11:22:33-10:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 X", "2011-10-10T11:22:33-11:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 Y", "2011-10-10T11:22:33-12:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 Z", "2011-10-10T11:22:33-00:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 +0000", "2011-10-10T11:22:33+00:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 -0000", "2011-10-10T11:22:33-00:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 +0100", "2011-10-10T11:22:33+01:00"),
//new Test("Mon, 10 Oct 2011 11:22:33 -0100", "2011-10-10T11:22:33-01:00")
};
["Mon, 10 Oct 2011 11:22:33 EDT", "2011-10-10T11:22:33-04:00"],
["Tue, 11 Oct 2011 11:22:33 EDT", "2011-10-11T11:22:33-04:00"],
["Wed, 12 Oct 2011 11:22:33 EDT", "2011-10-12T11:22:33-04:00"],
["Thu, 13 Oct 2011 11:22:33 EDT", "2011-10-13T11:22:33-04:00"],
["Fri, 14 Oct 2011 11:22:33 EDT", "2011-10-14T11:22:33-04:00"],
["Sat, 15 Oct 2011 11:22:33 EDT", "2011-10-15T11:22:33-04:00"],
["Sun, 16 Oct 2011 11:22:33 EDT", "2011-10-16T11:22:33-04:00"],
// // test without dayOfWeek
["10 Oct 2011 11:22:33 EDT", "2011-10-10T11:22:33-04:00"],
["11 Oct 2011 11:22:33 EDT", "2011-10-11T11:22:33-04:00"],
["12 Oct 2011 11:22:33 EDT", "2011-10-12T11:22:33-04:00"],
["13 Oct 2011 11:22:33 EDT", "2011-10-13T11:22:33-04:00"],
["14 Oct 2011 11:22:33 EDT", "2011-10-14T11:22:33-04:00"],
["15 Oct 2011 11:22:33 EDT", "2011-10-15T11:22:33-04:00"],
["16 Oct 2011 11:22:33 EDT", "2011-10-16T11:22:33-04:00"],
// // test monthName
["1 Jan 2011 11:22:33 EST", "2011-01-01T11:22:33-05:00"],
["1 Feb 2011 11:22:33 EST", "2011-02-01T11:22:33-05:00"],
["1 Mar 2011 11:22:33 EST", "2011-03-01T11:22:33-05:00"],
["1 Apr 2011 11:22:33 EDT", "2011-04-01T11:22:33-04:00"],
["1 May 2011 11:22:33 EDT", "2011-05-01T11:22:33-04:00"],
["1 Jun 2011 11:22:33 EDT", "2011-06-01T11:22:33-04:00"],
["1 Jul 2011 11:22:33 EDT", "2011-07-01T11:22:33-04:00"],
["1 Aug 2011 11:22:33 EDT", "2011-08-01T11:22:33-04:00"],
["1 Sep 2011 11:22:33 EDT", "2011-09-01T11:22:33-04:00"],
["1 Oct 2011 11:22:33 EDT", "2011-10-01T11:22:33-04:00"],
["1 Nov 2011 11:22:33 EDT", "2011-11-01T11:22:33-04:00"],
["1 Dec 2011 11:22:33 EST", "2011-12-01T11:22:33-05:00"],
// // test 2-digit year
["Mon, 1 Jan 01 11:22:33 EST", "2001-01-01T11:22:33-5:00"],
["Mon, 1 Jan 29 11:22:33 EST", "2029-01-01T11:22:33-5:00"],
["Tue, 1 Jan 30 11:22:33 EST", "2030-01-01T11:22:33-5:00"],
["Wed, 1 Jan 31 11:22:33 EST", "2031-01-01T11:22:33-5:00"],
["Thu, 1 Jan 32 11:22:33 EST", "2032-01-01T11:22:33-5:00"],
["Fri, 1 Jan 99 11:22:33 EST", "1999-01-01T11:22:33-5:00"],
// // test time zones
["Mon, 10 Oct 2011 11:22:33 UT", "2011-10-10T11:22:33-00:00"],
["Mon, 10 Oct 2011 11:22:33 GMT", "2011-10-10T11:22:33-00:00"],
["Mon, 10 Oct 2011 11:22:33 EST", "2011-10-10T11:22:33-05:00"],
["Mon, 10 Oct 2011 11:22:33 EDT", "2011-10-10T11:22:33-04:00"],
["Mon, 10 Oct 2011 11:22:33 CST", "2011-10-10T11:22:33-06:00"],
["Mon, 10 Oct 2011 11:22:33 CDT", "2011-10-10T11:22:33-05:00"],
["Mon, 10 Oct 2011 11:22:33 MST", "2011-10-10T11:22:33-07:00"],
["Mon, 10 Oct 2011 11:22:33 MDT", "2011-10-10T11:22:33-06:00"],
["Mon, 10 Oct 2011 11:22:33 PST", "2011-10-10T11:22:33-08:00"],
["Mon, 10 Oct 2011 11:22:33 PDT", "2011-10-10T11:22:33-07:00"],
// military time zones
["Mon, 10 Oct 2011 11:22:33 A", "2011-10-10T11:22:33+01:00"],
["Mon, 10 Oct 2011 11:22:33 B", "2011-10-10T11:22:33+02:00"],
["Mon, 10 Oct 2011 11:22:33 C", "2011-10-10T11:22:33+03:00"],
["Mon, 10 Oct 2011 11:22:33 D", "2011-10-10T11:22:33+04:00"],
["Mon, 10 Oct 2011 11:22:33 E", "2011-10-10T11:22:33+05:00"],
["Mon, 10 Oct 2011 11:22:33 F", "2011-10-10T11:22:33+06:00"],
["Mon, 10 Oct 2011 11:22:33 G", "2011-10-10T11:22:33+07:00"],
["Mon, 10 Oct 2011 11:22:33 H", "2011-10-10T11:22:33+08:00"],
["Mon, 10 Oct 2011 11:22:33 I", "2011-10-10T11:22:33+09:00"],
["Mon, 10 Oct 2011 11:22:33 K", "2011-10-10T11:22:33+10:00"],
["Mon, 10 Oct 2011 11:22:33 L", "2011-10-10T11:22:33+11:00"],
["Mon, 10 Oct 2011 11:22:33 M", "2011-10-10T11:22:33+12:00"],
["Mon, 10 Oct 2011 11:22:33 N", "2011-10-10T11:22:33-01:00"],
["Mon, 10 Oct 2011 11:22:33 O", "2011-10-10T11:22:33-02:00"],
["Mon, 10 Oct 2011 11:22:33 P", "2011-10-10T11:22:33-03:00"],
["Mon, 10 Oct 2011 11:22:33 Q", "2011-10-10T11:22:33-04:00"],
["Mon, 10 Oct 2011 11:22:33 R", "2011-10-10T11:22:33-05:00"],
["Mon, 10 Oct 2011 11:22:33 S", "2011-10-10T11:22:33-06:00"],
["Mon, 10 Oct 2011 11:22:33 T", "2011-10-10T11:22:33-07:00"],
["Mon, 10 Oct 2011 11:22:33 U", "2011-10-10T11:22:33-08:00"],
["Mon, 10 Oct 2011 11:22:33 V", "2011-10-10T11:22:33-09:00"],
["Mon, 10 Oct 2011 11:22:33 W", "2011-10-10T11:22:33-10:00"],
["Mon, 10 Oct 2011 11:22:33 X", "2011-10-10T11:22:33-11:00"],
["Mon, 10 Oct 2011 11:22:33 Y", "2011-10-10T11:22:33-12:00"],
["Mon, 10 Oct 2011 11:22:33 Z", "2011-10-10T11:22:33-00:00"],
["Mon, 10 Oct 2011 11:22:33 +0000", "2011-10-10T11:22:33+00:00"],
["Mon, 10 Oct 2011 11:22:33 -0000", "2011-10-10T11:22:33-00:00"],
["Mon, 10 Oct 2011 11:22:33 +0100", "2011-10-10T11:22:33+01:00"],
["Mon, 10 Oct 2011 11:22:33 -0100", "2011-10-10T11:22:33-01:00"]
];

[Fact]
public void TestParseDates()
[Theory]
[MemberData(nameof(TestParseDatesData))]
public void TestParseDates(string dateString, string isoString)
{
foreach (var test in _tests)
var json = string.Format("{{ date : new Date('{0}') }}", dateString);
BsonDocument document = null;
try
{
document = BsonDocument.Parse(json);
}
catch (Exception ex)
{
var message = string.Format("Error parsing: new Date(\"{0}\"). Message: {1}.", dateString, ex.Message);
throw new AssertionException(message); // note: the test data for 2-digit years needs to be adjusted at the beginning of each year
}
var dateTime = document["date"].ToUniversalTime();
var expected = DateTime.Parse(isoString).ToUniversalTime();
Assert.Equal(DateTimeKind.Utc, dateTime.Kind);
Assert.Equal(DateTimeKind.Utc, expected.Kind);
if (dateTime != expected)
{
var json = string.Format("{{ date : new Date('{0}') }}", test.Json);
BsonDocument document = null;
try
{
document = BsonDocument.Parse(json);
}
catch (Exception ex)
{
var message = string.Format("Error parsing: new Date(\"{0}\"). Message: {1}.", test.Json, ex.Message);
throw new AssertionException(message); // note: the test data for 2-digit years needs to be adjusted at the beginning of each year
}
var dateTime = document["date"].ToUniversalTime();
var expected = DateTime.Parse(test.Iso).ToUniversalTime();
Assert.Equal(DateTimeKind.Utc, dateTime.Kind);
Assert.Equal(DateTimeKind.Utc, expected.Kind);
if (dateTime != expected)
{
var message = string.Format("Parsing new Date(\"{0}\") did not yield expected result {1}.", test.Json, expected.ToString("o"));
throw new AssertionException(message);
}
var message = string.Format("Parsing new Date(\"{0}\") did not yield expected result {1}.", dateString, expected.ToString("o"));
throw new AssertionException(message);
}
}
}
Expand Down