- PVSM.RU - https://www.pvsm.ru -

Исправляя мелкий баг в calc.exe

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

Исправляя мелкий баг в calc.exe - 1
Неверный результат вычисления диапазона дат в Калькуляторе Windows

«Ну, это похоже на любопытную ошибку, интересно, что может её вызвать», — подумал я про себя. Количество недель, безусловно, делает баг похожим на какую-то ошибку переполнения или задания диапазона, ну вы знаете, типичные причины. Но это всегда может быть какой-то перевёрнутый бит каким-то высокоэнергетическим лучом от какого-то дружественного космического соседа.

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

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

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

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

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

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

DateDifference calculate_difference(start_date, end_date) {
    uint[] diff_types = [year, month, week, day]
    uint[] typical_days_in_type = [365, 31, 7, 1]
    uint[] calculated_difference = [0, 0, 0, 0]
    date temp_pivot_date
    date pivot_date = start_date
    uint days_diff = calculate_days_difference(start_date, end_date)

    for(type in differenceTypes) {
        temp_pivot_date = pivot_date
        uint current_guess = days_diff /typicalDaysInType[type] 
        if(current_guess !=0)
            pivot_date = advance_date_by(pivot_date, type, current_guess)
        
        int diff_remaining
        bool best_guess_hit = false
        do{
            diff_remaining = calculate_days_difference(pivot_date, end_date)
            if(diff_remaining < 0) {
                // pivotDate has gone over the end date; start from the beginning of this unit
                current_guess = current_guess - 1
                pivot_date = temp_pivot_date
                pivot_date = advance_date_by(pivot_date, type, current_guess)
                best_guess_hit = true
            } 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 дней, вы прибавляете один месяц, то куда попадёте?»

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

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

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

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

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

Исправляя мелкий баг в calc.exe - 2

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

Автор: m1rko

Источник [9]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/open-source/321282

Ссылки в тексте:

[1] баге в calc.exe: https://www.reddit.com/r/Windows10/comments/c14fu5/wait_what/

[2] И действительно: https://blogs.windows.com/buildingapps/2019/03/06/announcing-the-open-sourcing-of-windows-calculator/#C3LDvZ93SHZwoW8D.97

[3] Фактическое вычисление: https://github.com/microsoft/calculator/blob/2826d370565092dfca9a983a5fc6ec0b8b1c62e1/src/CalcViewModel/Common/DateCalculator.cpp#L122-L234

[4] Windows.Globalization.Calendar.AddMonths(Int32): https://docs.microsoft.com/en-us/uwp/api/windows.globalization.calendar.addmonths#Windows_Globalization_Calendar_AddMonths_System_Int32_

[5] дистрибутивной: https://en.wikipedia.org/wiki/Distributive_property

[6] коммутативной: https://en.wikipedia.org/wiki/Commutative_property

[7] ассоциативной: https://en.wikipedia.org/wiki/Associative_property

[8] пулл-запрос: https://github.com/microsoft/calculator/pull/553

[9] Источник: https://habr.com/ru/post/456684/?utm_campaign=456684&utm_source=habrahabr&utm_medium=rss