Хабрахабр

[Перевод] Исправляя мелкий баг в calc.exe

В воскресенье я как обычно бездельничал, просматривая Reddit. Прокручивая щенячьи забавы и плохой юмор программистов, моё внимание привлёк один конкретный пост. Речь шла о баге в calc.exe.


Неверный результат вычисления диапазона дат в Калькуляторе Windows

Количество недель, безусловно, делает баг похожим на какую-то ошибку переполнения или задания диапазона, ну вы знаете, типичные причины. «Ну, это похоже на любопытную ошибку, интересно, что может её вызвать», — подумал я про себя. И повторение ситуации из поста «31 июля − 31 декабря» на моей машине дало правильный результат «5 месяцев». Но это всегда может быть какой-то перевёрнутый бит каким-то высокоэнергетическим лучом от какого-то дружественного космического соседа.
Заинтересовавшись причиной, я сделал то, что вы делаете в таких случаях: попробовал на своей машине, чтобы запостить «У меня всё работает». Выводится не совсем корректное значение «5 месяцев, 613566756 недель, 3 дня». Но немного потестировав, я обнаружил, что «31 июля – 30 декабря» на самом деле вызывает ошибку.

Эта ошибка не могла быть слишком сложной, поэтому я подумал, что попробую найти её. Я ещё не закончил расшатывать программу и тут вспомнил: «О, а разве калькулятор — не одна из тех вещей, для которых Microsoft открыла исходники?» И действительно. Скачать исходники было достаточно просто, и добавление требуемой рабочей нагрузки UWP в Visual Studio также прошло без сучка и задоринки.

Особенно когда вы хотите внести вклад в проекты с открытым исходным кодом, где находите баг. Навигация по кодовым базам, с которыми вы не знакомы, — это то, к чему привыкаешь со временем. Однако незнание XAML или WinRT, конечно, не облегчает дело.

Нашёл DateCalculator.xaml, затем вроде бы подходящий по названию DateDiff_FromDate to DateCalculatorViewModel.cpp и, наконец, DateCalculator.cpp. Я открыл файл solution и заглянул в проект “Calculator” в поисках любого файла, который должен иметь отношение к багу.

То есть это была не просто ошибка преобразования в строку, а ошибка фактического вычисления. Установив точку останова и посмотрев некоторые переменные, я увидел, что конечное значение DateDifference уже неверно.

Фактическое вычисление в упрощённом псевдокоде выглядит примерно так:

DateDifference calculate_difference(start_date, end_date) else if(diff_remaining > 0) { // pivot_date is still below the end date if(best_guess_hit) break; current_guess = current_guess + 1 pivot_date = advance_date_by(pivot_date, type, 1) } } while(diff_remaining!=0) temp_pivot_date = advance_date_by(temp_pivot_date, type, current_guess) pivot_date = temp_pivot_date calculated_difference[type] = current_guess days_diff = calculate_days_difference(pivot_date, end_date) } calculcated_difference[day] = days_diff return calculcated_difference
}

Выглядит нормально. В логике проблем нет. По сути, функция делает следующее:

  • отсчитывает полные годы от стартовой даты
  • с момента даты последнего полного года отсчитывает месяцы
  • с момента даты последнего полного месяца отсчитывает недели
  • с момента даты последней полной недели отсчитывает оставшиеся дни

На самом деле проблема заключается в предположении, что последовательный запуск

date = advance_date_by(date, month, somenumber)
date = advance_date_by(date, month, 1)

равен

date = advance_date_by(date, month, somenumber + 1)

Обычно это одно и то же. Но возникает вопрос: «Если вы попали на 31-е число месяца, в следующем месяце 30 дней, вы прибавляете один месяц, то куда попадёте?»

Globalization. Похоже, для Windows. AddMonths(Int32) ответ будет «на 30-е число». Calendar.

А это значит, что:
«31 июля + 4 месяца = 30 ноября»
«30 ноября + 1 месяц = 30 декабря»
«31 июля + 5 месяцев = 31 декабря»

Какой вообще-то должна быть операция «сложения». Таким образом, операция AddMonths не является ни дистрибутивной (с AddMonth-умножением), ни коммутативной, ни ассоциативной. Разве не весело работать со временем и календарями?

Как вы могли догадаться, это возникает из-за того, что days_diff является беззнаковым типом. Почему в данном случае ошибка задания диапазона приводит к такому огромному числу недель? Которая затем пытается исправить ситуацию, уменьшая current_guess, но не уменьшая беззнаковую переменную. Это превращает -1 дней в огромное количество, которое затем передаётся на следующую итерацию цикла с неделями.

Я создал пулл-запрос на Github с минимальным «исправлением». Что ж, это был интересный способ провести воскресенье. Я ставлю «исправление» в кавычки, потому что теперь вычисление выглядит так:

Хотя такой вариант не совсем согласуется с человеческой интуицией о разнице дат. Думаю, технически это правильный результат, если считать, что «31 июля + 4 месяца = 30 ноября». Но в любом случае это менее неправильно, чем было.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть