JSON.Net десериализация значений DateTime не удается в некоторых случаях? - PullRequest
1 голос
/ 05 ноября 2011

Используя JSON.Net, из следующих 5 тестов, первый и последний проход, в то время как остальные терпят неудачу:

[Test, Sequential]
public void WhyCantIDeserializeThisDateWhen2011Works(
    [Values(1980, 1980, 1980, 1980, 1980, 1980, 1980)] Int32 year,
    [Values(10,   10,   10,   10,   11,   11,   11)] Int32 month,
    [Values(26,   27,   30,   31,   1,    2,    3)] Int32 day)
{
    var obj = new {
        Title = "Will this be able to serialize the DateTime field?",
        Timestamp = new DateTime(year, month, day)
    };

    var type = obj.GetType();
    var serialized = Newtonsoft.Json.JsonConvert.SerializeObject(obj);
    dynamic deserialized = Newtonsoft.Json.JsonConvert.DeserializeObject(serialized, type);

    Assert.AreEqual(obj.Title, deserialized.Title);
    Assert.AreEqual(obj.Timestamp, deserialized.Timestamp);
}

Вот некоторые из выводов:

[snip]

Test 'Rds.Infrastructure.Serializers.Tests.JsonSerializerTests.WhyCantIDeserializeThisDateWhen2011Works(1980,11,2)' failed:
Expected: 1980-11-02 00:00:00.000
But was: 1980-11-01 23:00:00.000
at CallSite.Target(Closure , CallSite , Type , DateTime , Object )
    UnitTests\Rds.Infrastructure\Serializers\JsonSerializerTests.cs(141,0): at     Rds.Infrastructure.Serializers.Tests.JsonSerializerTests.WhyCantIDeserializeThisDateWhen2011Works(Int32 year, Int32 month, Int32 day)

2 passed, 5 failed, 0 skipped, took 627.55 seconds (NUnit 2.5.5).

Ошибка типична для всех ошибок - когда она перезагружает дату, а не является указанным днем, она появляется как предыдущий день в 23:00. Это особенно странно, если я изменю год на 2011 год, все эти тесты пройдут.

Я копался в коде JSON.Net - метод ParseDate класса JsonTextReader считывает значение. Используя 27 октября 2011 года в качестве примера, мои комментарии:

private void ParseDate(string text)
{
  string value = text.Substring(6, text.Length - 8);
  DateTimeKind kind = DateTimeKind.Utc;

  int index = value.IndexOf('+', 1);

  if (index == -1)
    index = value.IndexOf('-', 1);

  TimeSpan offset = TimeSpan.Zero;

  if (index != -1)
  {
    kind = DateTimeKind.Local;
    offset = ReadOffset(value.Substring(index));
    value = value.Substring(0, index);
  }

  long javaScriptTicks = long.Parse(value, NumberStyles.Integer, CultureInfo.InvariantCulture);

  // The date time gets loaded here, as Oct 27 2011 3am
  DateTime utcDateTime = JsonConvert.ConvertJavaScriptTicksToDateTime(javaScriptTicks);

#if !NET20
  if (_readType == ReadType.ReadAsDateTimeOffset)
  {
    SetToken(JsonToken.Date, new DateTimeOffset(utcDateTime.Add(offset).Ticks, offset));
  }
  else
#endif
  {
    DateTime dateTime;

    switch (kind)
    {
      case DateTimeKind.Unspecified:
        dateTime = DateTime.SpecifyKind(utcDateTime.ToLocalTime(), DateTimeKind.Unspecified);
        break;
      case DateTimeKind.Local:
        // Here, it gets converted to local time, Oct 26 2011 at 11pm!
        dateTime = utcDateTime.ToLocalTime();
        break;
      default:
        dateTime = utcDateTime;
        break;
    }

    SetToken(JsonToken.Date, dateTime);
  }
}

Как отмечалось выше, ошибка возникает только для дат с 27 октября 1980 года по 2 ноября 1980 года. Я не проводил тесты, чтобы определить, какие годы являются проблемами, но тесты проходят, если вы используете 2011.

Полагаю, это связано с изменениями в летнем времени?

Кто-нибудь имеет представление о том, что здесь происходит?

1 Ответ

2 голосов
/ 07 ноября 2011

Я углубился в это гораздо дальше.

Когда JSON.Net сериализует экземпляр DateTime, он вызывает TimeZone.CurrentTimeZone.GetUtcOffset(dt). Когда он перезагружает DateTime, он принимает дату в формате UTC и для преобразования в локальную дату вызывает utcDateTime.ToLocalTime() для преобразования в местное время. Похоже, что эти два метода не всегда используют одно и то же смещение:

(Примечание: я нахожусь в часовом поясе Атлантики.)

[Test, Sequential]
public void AnotherTest(
    [Values(2006, 2006, 2006, 2006, 2006, 2006, 2006)] Int32 year,
    [Values(10, 10, 10, 10, 11, 11, 11)] Int32 month,
    [Values(26, 27, 30, 31, 1, 2, 3)] Int32 day)
{
    var dt = new DateTime(year, month, day, 0, 0, 0, DateTimeKind.Local);

    var utcOffset1 = TimeZone.CurrentTimeZone.GetUtcOffset(dt);
    var utcOffset2 = dt.Subtract(dt.ToUniversalTime());

    Assert.AreEqual(utcOffset1, utcOffset2);
}

Они проходят 26 октября и 3 ноября и не действуют в период между. Я также проводил тесты в разные более ранние годы с такими же результатами. Для дат с 2007 по 2011 годы все это проходит. Для всех сбоев, которые я обнаружил, utcOffset1 составлял -3 часа, а utcOffset2 - -4 часа. (Согласно http://www.timeanddate.com/library/abbreviations/timezones/, атлантическое стандартное время должно быть UTC-4, а атлантическое летнее время должно быть UTC-3.) Несколько быстрых тестов показали мне, что есть также проблема в начале летнего времени а также до 2007 года.

(я открыл проблему на https://connect.microsoft.com/VisualStudio/feedback/details/699491/timezone-getutcoffset-and-datetime-touniversaltime-not-consistent-for-brief-period-for-atlantic-time-zone.)

В то же время, чтобы устранить эту проблему, нужно лишь сделать согласованным смещение UTC при сериализации и десериализации даты, что означает избавление от вызова на TimeZone.CurrentTimeZone.GetUtcOffset.

UPDATE

JamesNK обновил JSON.Net 1 ноября 2011 года, чтобы преобразования часового пояса использовали TimeZoneInfo вместо TimeZone, что, похоже, решило проблему.

ОБНОВЛЕНИЕ 2

Спасибо @derekhat за следующую дополнительную информацию:

У меня есть несколько свободных минут сегодня вечером. Все тесты прошли для меня с использованием Windows 7 64-bit и скомпилированы в командной строке с .NET 2.0 SDK (пришлось изменить var на явные объявления типов).

5 из 7 тестов не пройдены в Visual Studio 2010 и .NET 4.

Затем я нашел следующую документацию .

Метод GetUtcOffset распознает только текущее летнее время правило корректировки для местного часового пояса. В результате это гарантированно точно вернет смещение UTC только по местному времени в течение периода, в котором действует последнее правило корректировки. Это может вернуть неточные результаты, если время является исторической датой и временем значение, на которое распространялось предыдущее правило корректировки.

Ситуация еще более усложняется другой частью документации, которая гласит: «В системах Windows XP метод ToUniversalTime распознает только текущее правило корректировки при преобразовании из местного времени в UTC». И комментарий гласит, что поведение WinXP также существует в Windows Server 2003. Смысл в том, что ToUniversalTime правильно работает с историческими датами в более новых версиях Windows, что, кажется, соответствует вашим результатам.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...