В первой редакции говорилось о двадцатилетнем баге. На самом деле ему 30 лет. Спасибо Sidnekin.
Сегодня, считывая какие-то данные, моя программа обработала 36'916 возможных дат. Две из этих 36'916 не прошли проверку. Я не придал этому значения, потому что эти даты были из данных предоставленных клиентом, а такие данные часто удивляют. Однако, взглянув на исходные данные, выяснилось, что проверку не прошли 1 января 2011 и 1 января 2007. В программе, написанной мной месяц назад, был баг. Но оказалось, что этому багу 30 лет.
Любому человеку, который не очень понимает экосистему программного обеспечения, написанное ниже покажется странным, но в этом есть смысл. Из-за решения, принятого давным-давно, чтобы принести деньги одной компании, мой $клиент потратил деньги на оплату мне, чтобы я исправил баг, внесённый одной компанией случайно, а другая специально. Чтобы объяснить это, мне придётся рассказать о третьей компании, добавившей особенность, ставшую в конечном счёте багом, и ещё о нескольких фактах, повлиявших на непонятный баг, который я исправил сегодня.
В старые добрые времена компьютеры Apple иногда сбрасывали дату на 1 января 1904 года. Причина проста: в те времена компьютеры Apple использовали питаемое от батареек системное время, чтобы следить за датой и временем. Что случалось, когда батарейка садилась? Компьютеры Apple считали свои даты, как количество секунд, прошедших с начала эпохи. Эпоха в данном случае — всего лишь дата начала отсчёта. И для компьютеров Apple такой датой было 1 января 1904 года. Когда батарейка садилась это число становилось новой датой. Но почему так случалось на самом деле?
В те времена Apple использовала 32 бита для хранения количества секунд со стартовой даты. Один бит может содержать два значения: 0 или 1. Два бита — четыре значения: 00, 01, 10, 11. Три бита — восемь значений: 000, 001, 010, 011, 100, 101, 110, 111. И так далее. Сколько значений содержится в 32 битах? 32 бита содержат 232 (или 4'294'967'296) значений. Для дат Apple это равнялось примерно 136 годам, поэтому старые компьютеры Apple не могут работать с датами после 2040 года и, если батарейка в системных часах садилась, то дата опять была равна 0 секундам после эпохи и приходилось вручную устанавливать текущее число каждый раз при включении компьютера (до покупки новой батарейки).
Однако, решение Apple для хранения дат как количества секунд после эпохи значило, что невозможно хранить даты до эпохи. Как мы увидим, это имело далеко идущие последствия. Это была особенность, а не баг, добавленный Apple. Кроме всего прочего, это означало, что операционная система Macintosh имела иммунитет к проблеме Y2K (хотя многие программы на Mac не имели, так как по-своему представляли даты, чтобы обойти ограничения Mac'ов).
Двигаясь дальше, встречаем Lotus 1-2-3, убойное приложение IBM, способствовавшее запуску PC-революции, хотя на самом деле VisiCalc на Apple дало начало персональным компьютерам. Можно сказать, что, если бы не 1-2-3, то PC скорее всего не покинули свою нишу и компьютерные технологии развивались совсем по-другому. Однако, Lotus 1-2-3 неправильно считал 1900-й високосным годом. Когда Microsoft выпустила Multiplan, свою первую программу для электронных таблиц, она не смогла завоевать рынок. Так что при разработке Excel было решено не только скопировать у Lotus 1-2-3 правила именования столбцов, но и сделать продукты полностью, с точностью до багов, совместимыми, включая намеренное обращение с 1900-м как с високосным годом, проблема актуальная до сих пор. Так что для 1-2-3 это было багом, но для Excel — особенностью, гарантирующей всем пользователям 1-2-3 возможность импорта электронных таблиц в Excel без различий в датах, даже если они были неправильны.
Со временем Microsoft решила выпустить версию Excel для Apple Macintosh, но была проблема. Как уже упоминалось, Macintosh не понимала даты до 1 января 1904 года, а для Excel'я эпохой было 1 января 1900 года. Так что Excel подправили, чтобы распознавать эпоху и хранить дату относительно соответствующей эпохи. В статье поддержки Microsoft эта проблема описана довольно ясно. И это приводит к моему багу.
Мой нынешний $клиент получает электронные таблицы от многих своих клиентов. Эти таблицы могли быть сделаны на Windows, а могли быть сделаны на Mac. В результате, эпохой в этих таблицах может быть 1 января 1900 или 1 января 1904 года. Как узнать какая именно? Формат файла в Excel'е хранит такую информацию, но парсер, который я использую, её не предоставляет и считает, что вы сами знаете с какой эпохой имеете дело в данном файле. Мне, наверное, следовало потратить кучу времени в попытках разобраться как прочитать бинарный формат Excel и отправить патч разработчику парсера, но у меня были другие дела для моего $клиента и я набросал эвристику для определения к какой эпохе относится данный файл. Она была простая.
В Excel может хранится, например, 5 июля 1998, но это число может быть отформатировано как «07-05-98» (бесполезный американский формат), «Июл 5, 98», «Июль 5, 1998», «5-ИЮЛ-98» и ещё множеством других бесполезных вариантов (по иронии, единственный формат, который моя версия Excel не предлагает — ISO 8601). Внутри же, неформатированное значение, это либо 35981 для эпохи 1900, или 34519 для эпохи 1904 (эти числа соответствуют количеству дней прошедших с эпохи). Я использовал устойчивый парсер, чтобы извлечь год из отформатированной даты. а затем парсер Excel, чтобы извлечь год из неотформатированного значения. Если они различались на четыре, значит даты в файле считались от 1904 года.
Почему сразу не использовать форматированные даты? Потому что 5 июля 1998 года может быть отформатировано как «Июль 1998», теряя день. Мы получаем электронные таблицы от стольких компаний и они создают их столькими разными способами, что они ожидают от нас (от меня в данном случае) способности разобраться. Excel понимает что к чему, значит и я должен!
Тут-то 39082 и дало мне толчок. Помните, как Lotus 1-2-3 считал 1900й високосным годом, и как это по-честному скопировали в Excel? Поскольку это добавляет один день к 1900, многие функции расчёта дат могут ошибаться на один день. Это значит, что 39082 может быть 1 января 2011 года (на Mac), а может быть 31 декабря 2006 (на Windows). Здорово конечно, что мой парсер извлекает 2011 из форматированного значения. Но поскольку парсер Excel не знает от какой эпохи рассчитываются даты в данном файле, то считает по умолчанию, что от 1900, возвращает год 2006, моя программа видит, что разница пять лет, считает, что это ошибка, пишет её в лог и возвращает неотформатированное значение.
Чтобы обойти это я придумал следующее (псевдокод):
difference = formatted_year - parsed_year
if ( 0 == difference )
assume 1900 date system
if ( 4 == difference )
assume 1904 date system
if ( 5 == difference and parsed month is December and parsed day is 31 )
assume 1900 date system
Теперь все 36'916 даты парсятся правильно.
Замечание: для прикола, если у вас есть Mac с Excel, можете попробовать ввести дату до 1904 года и отформатировать её в другой формат. Ввести её вы сможете, но не сможете отформатировать, потому что Excel будет считать, что это обычный текст. В то же время для Microsoft Excel все дни недели до 1 марта 1900 неправильны из-за бага в программе, выпущенной в января 1983 года.
Update: Мне сказали, что Spreadseet::ParseExcel понимает флаг 1904. К сожалению я использую Spreadsheet::ParseExcel::Stream, который не понимает. Даже на огромных машинах нам не хватает памяти при использовании стандартного парсера, так что мы используем потоковый. Мои попытки обойти это ограничение натолкнулись на ещё один баг.
Update 2: Оказывается, Microsoft сначала выпустил Excel для Macintosh.
Update 3: Если верить Джоэлу Спольски, баг в Lotus 1-2-3 мог быть намеренной попыткой упростить программу. Я и раньше читал намёки, что Lotus сделал это преднамеренно, но поскольку я не уверен на все 100, я не стал про это писать.
Автор: adaptun