Внедрение DST в JavaScript, вызывающее проблемы при отправке на контроллер MVC

Мой клиент, находящийся в страховом полисе, требует дату рождения потенциального застрахованного. Он вводится в веб-форму с использованием jQuery datepicker, подключенного к свойству модели с использованием Knockout и отправленного через ajax в JSON в контроллер MVC 4.

Некоторые страхователи получили свою страховую документацию с неправильной датой рождения. Во время последующего расследования мы обнаружили, что в дополнение к ошибкам ввода данных, которые являются чрезвычайно маргинальными, неправильные даты сосредоточены в следующие два периода:

  • последние три недели с марта по начало апреля
  • последняя неделя октября до первой недели ноября.

Поскольку наш клиент находится в часовом поясе Америки/Монреаля, я сразу же подумал о проблеме DST. Правила DST изменились в 2007 году в этом часовом поясе.

Читая несколько статей и другие вопросы о переполнении стека, я узнал, что в стандарте ECMAScript 5 указано, что реализация должна учитывать текущие правила DST и не должна учитывать историю изменений DST. ES5 15.9.1.8 Единственный браузер, не соответствующий этой спецификации на данный момент, - IE10 (подробнее об этом позже).

Учитывая эту спецификацию, браузеры сообщают о датах следующим образом (проверено в Chrome):

(new Date(2006, 9, 31, 0, 0, 0)).toISOString()
// 2006-10-31T04:00:00.000Z

(new Date(2008, 9, 31, 0, 0, 0)).toISOString()
// 2008-10-31T04:00:00.000Z

В то время как платформа .NET сообщает о датах правильно:

DateTime dt = new DateTime(2006, 10, 31, 0, 0, 0);
Console.WriteLine("{0:MM/dd/yy H:mm:ss zzz}", dt);
// 10-31-06 0:00:00 -05:00
dt = new DateTime(2008, 10, 31, 0, 0, 0);
Console.WriteLine("{0:MM/dd/yy H:mm:ss zzz}", dt);
// 10-31-08 0:00:00 -04:00

Одна из причин этой проблемы заключается в том, что если застрахованный человек родился в последнюю неделю октября до 2007 года, время UTC будет сообщено моему контроллеру MVC неправильно, например:

Объект даты Javascript:

new DateTime(2006, 9, 31, 0, 0, 0);

.Net анализирует данные JSON и получает:

10-30-06 23:00:00 GMT

В ходе тестов я обнаружил, что внутреннее представление объекта Date в JavaScript несовместимо с внутренним DateTime.Net, и я не смог свернуть собственный парсер с использованием представления JavaScript:

Javascript:

(new Date(2006, 9, 31, 0, 0, 0)).getTime()
// 1162267200000

.Net:

DateTime dt = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(1162267200000);
Console.WriteLine("{0}", dt);
// 2006-10-31 04:00:00

(Это неправильно, потому что он был представлен 30 октября 2006 года в 23:00 по местному времени.)

Для моего клиента используйте случай, так как это интересующая меня часть даты, я мог бы просто установить временную часть объекта Date в полдень, что защитит меня от всех случаев неправильной интерпретации даты на части JavaScripts. К сожалению, это Ugly Patch, и он не решает других потенциальных ситуаций, например, если мой клиент хотел, чтобы мы реализовали функциональность, требуя, чтобы мы имели точную дату и время события, которое происходило в прошлом.

Другим подходом было бы написать алгоритм для обнаружения потенциально проблемных недель DST в моем контроллере и установить правильное время, если я получу дату в течение рассматриваемых недель. К сожалению, учитывая, что в стандарте ECMAScript 6 указано, что история изменений DST должна соблюдаться (так что все будущие браузеры должны правильно обрабатывать эту ситуацию), и учитывая, что IE10 уже работает правильно, я боюсь создать проблемную ситуацию в будущем, Подход также кажется неправильным, архитектурно говоря.

Еще один аспект, который следует учитывать, заключается в том, что если мне нужно передать модель от клиента к серверу, а затем вернуться к клиенту, мне нужно будет создать способ воссоздать "плохой" объект JavaScript JavaScript, чтобы не влиять на отображение на клиентской стороне приложения.

Никакое решение не кажется правильным и расширяемым. Я не могу поверить, что раньше никогда не приходилось сталкиваться с этой проблемой.

Как я могу исправить это навсегда и таким образом, который можно было бы применить ко всем другим проектам JavaScript/MVC?

РЕДАКТИРОВАТЬ # 1 - Дополнительная информация, не связанная напрямую с проблемой:

Как заметил @matt-johnson, редко используется случай, когда информация о часовом поясе должна использоваться. В этом случае, однако, застрахованная дата рождения относится к часовому поясу, в который входит мой клиент. Пусть возьмут, например, 17-летнего жителя Виктории-BC с днем ​​рождения 2 декабря. Если он отправит страховую котировку 1 декабря в 22:00, хотя он все равно 17, страховая котировка будет принята, потому что он уже 18 в моем часовом поясе клиента. Это же правило применяется для страхового ценообразования. Это юридическое требование.

Чтобы быть ясными, я ищу универсальное решение, которое может быть применено к любым другим проектам. Конкретная проблема: Javascript, всего за несколько конкретных недель в течение многих лет до 2007 года, сообщает время с 1-часовым смещением. Это смещение всегда присутствует независимо от того, как я представляю время (локальный часовой пояс или UTC), потому что это неверные данные (а не данные).

Я использовал метод "установить временную часть в полдень", чтобы обойти ошибку, но основная проблема все еще остается. Что произойдет, если мой клиент попросит нас разработать веб-приложение, требующее, чтобы мы получили дату и время определенного события в прошлом? Например: "Пожалуйста, укажите точную дату и время аварии: 31 октября 2005 года в 19:32". Контроллер MVC установил время в 18:32.

Ответы

Ответ 1

Вы игнорируете часть .Net DateTime. В частности, вы не считаете, что свойство .Kind любого DateTime является одним из трех возможных значений DateTimeKind. Подробнее о MSDN.

Если вы работаете с значениями UTC, вам нужно установить DateTimeKind.Utc. Поскольку числовые значения JavaScript основаны на UTC, вы должны использовать это для преобразования:

DateTime dt = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc)
                      .AddMilliseconds(1162267200000);

Кроме того, вы делаете что-то странное, используя форматер zzz на DateTime, который имеет DateTimeKind.Unspecified. В этом случае на самом деле нет никакого часового пояса, связанного со значением, но .Net предположит, что вы хотели, чтобы он вел себя так, как будто он был DateTimeKind.Local, так как форматер zzz не имеет смысла иначе.

Вам действительно нужно быть осторожным, чтобы не использовать значения DateTimeKind.Local (например, DateTime.Now) или что-либо, где неопределенные виды могут быть неверно истолкованы как локальные (например, zzz или .ToUniversalTime()). "Локальный" в этом контексте будет локальным для вашего сервера, что редко актуально в веб-приложении. Подробнее здесь.

Что касается реализации DST JavaScript, я сделал аналогичные замечания относительно спецификаций ECMAScript. Подробнее об этом можно прочитать здесь.

Теперь со всем сказанным вы попадаете в очень конкретную точку, а это значит, что ни JavaScript, ни .NET не предлагают тип, который представляет дату без времени. Оба придерживаются подхода, устанавливающего время до полуночи. Если вы не будете осторожны, вы можете столкнуться с проблемами с локальными датами, которые не имеют полночь, например 20 октября 2013 года в Бразилии. Если вы ограничены в США, тогда у вас может не быть этой конкретной проблемы, но она по-прежнему актуальна с точки зрения дизайна.

Лучшим подходом было бы использование истинного типа "дата без времени". В .Net вы можете найти это как LocalDate в Noda Time. Как строка ISO8601, она будет выглядеть как "2013-10-31" без какого-либо времени или часового пояса.

Если вы не хотите использовать Noda Time (хотя я очень рекомендую его), вы все равно можете использовать тип DateTime в .Net, просто будьте очень осторожны, чтобы никогда не использовать временную часть для чего-либо. Он должен иметь DateTimeKind.Unspecified.

К сожалению, в настоящий момент нет JavaScript-кода для JavaScript без даты. (Возможно, класс JavaScript Date был назван DateTime). Таким образом, вам нужно будет создать строку только для даты. Вы можете собрать его с помощью Date вручную, например:

var s = dt.getFullYear() + "-" + 
  (dt.getMonth() < 10 ? "0" : "") + dt.getMonth() + "-" + 
  (dt.getDate() < 10 ? "0" : "") + dt.getDate();

Но проще использовать библиотеку moment.js:

var s = moment(dt).format("YYYY-MM-DD");

Я просто предлагаю свое мнение относительно "лучшего подхода". Но если вы просто хотели бы знать, как предотвратить провал с тем, что вы сейчас делаете, то вы должны быть осторожны, чтобы не конвертировать в UTC в свой ответ JavaScript. Вы делаете это с помощью .toISOString(). Опять же, вы можете собрать полную дату и время вручную, или вы могли бы использовать момент, чтобы отформатировать ответ, относящийся к местному времени.

Если вы действительно думаете об этом, у Birthdate действительно нет связанного с ним времени или часового пояса. Большинство людей не отслеживают час/минуту/секунду своего рождения (или часовой пояс своего места рождения), когда они выясняют, сколько им лет. Вы можете сделать это для астрологии, но для повседневного использования бизнеса вы просто будете использовать начало дня в местном часовом поясе, где вы оцениваете возраст. Таким образом, дата действительно является единственной важной частью данных.