Fixing a minor bug in calc.exe

Original author: Peter Tissen
  • Transfer
On Sunday, I was idle as usual, browsing through Reddit. Scrolling through puppy fun and the programmers' bad humor, one particular post attracted my attention. It was a bug in calc.exe .


Incorrect result of calculating the date range in the Windows Calculator

"Well, this seems like a curious error, I wonder what might cause it," I thought to myself. The number of weeks, of course, makes the bug look like some kind of overflow or range error, you know, typical reasons. But it can always be some inverted bit by some high-energy ray from some friendly cosmic neighbor.

Being interested in the reason, I did what you do in such cases: I tried it on my machine to post "Everything works for me." And the repetition of the situation from the post “July 31 - December 31” on my machine gave the correct result of “5 months”. But after testing a bit, I found that "July 31 - December 30" actually causes an error. The incorrect value “5 months, 613566756 weeks, 3 days” is displayed.

I had not finished shaking the program, and then I remembered: “Oh, isn't the calculator one of those things for which Microsoft has opened the source?” And really . This mistake could not be too complicated, so I thought that I would try to find it. Downloading the sources was quite simple, and adding the required UWP workload to Visual Studio also went without a hitch.

Navigating codebases you are not familiar with is something you get used to with time. Especially when you want to contribute to open source projects where you find the bug. However, ignorance of XAML or WinRT, of course, does not make things easier.

I opened the solution file and looked into the “Calculator” project in search of any file that should be related to the bug. Found DateCalculator.xaml, then it seems to be appropriate for the title DateDiff_FromDate to DateCalculatorViewModel.cppand finally DateCalculator.cpp.

Having set a breakpoint and looking at some variables, I saw that the final value is DateDifferencealready incorrect. That is, it was not just a conversion error into a string, but an error of the actual calculation.

The actual calculation in a simplified pseudo-code looks something like this:

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
}

It looks fine. There are no problems in logic. Essentially, the function does the following:

  • counts full years from the start date
  • from the date of the last full year counts months
  • counts weeks from the date of the last full month
  • from the date of the last full week counts the remaining days

Actually, the problem is the assumption that sequential start

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

is equal to

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

This is usually the same thing. But the question arises: “If you hit the 31st day of the month, the next month 30 days, you add one month, then where will you go?”

It seems like for Windows.Globalization.Calendar.AddMonths (Int32) the answer will be "on the 30th."

And this means that:
“July 31 + 4 months = November 30”
“November 30 + 1 month = December 30”
“July 31 + 5 months = December 31”

Thus, the AddMonths operation is not distributive (with AddMonth-multiplication ), neither commutative nor associative . What actually should be the operation of "addition". Isn't it fun to work with time and calendars?

Why in this case, the error of setting the range leads to such a huge number of weeks? As you might have guessed, this arises from being days_diffan unsigned type. This turns -1 days into a huge amount, which is then passed on to the next iteration of the cycle with weeks. Which then tries to correct the situation by decreasing current_guessbut not decreasing the unsigned variable.

Well, that was an interesting way to spend Sunday. I created a pull request on Github with a minimal “fix”. I put the “correction” in quotation marks, because now the calculation looks like this:



I think that technically this is the correct result, if we assume that "July 31 + 4 months = November 30." Although this option is not entirely consistent with human intuition about the difference in dates. But in any case, this is less wrong than it was.

Also popular now: