Fixing a minor bug in calc.exe
- 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
Having set a breakpoint and looking at some variables, I saw that the final value is
The actual calculation in a simplified pseudo-code looks something like this:
It looks fine. There are no problems in logic. Essentially, the function does the following:
Actually, the problem is the assumption that sequential start
is equal to
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
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.
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.cpp
and finally DateCalculator.cpp
. Having set a breakpoint and looking at some variables, I saw that the final value is
DateDifference
already 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_diff
an 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_guess
but 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.