On Sunday, I lazed around as usual, looking through Reddit. Scrolling through the puppy's fun and the bad humor of the programmers, my attention was drawn to one specific post. It was a
bug in calc.exe .
Invalid result of calculating the date range in the Windows Calculator"Well, it looks like a curious mistake, I wonder what could cause it," I thought to myself. The number of weeks certainly makes the bug look like some kind of overrun or range error, you know, typical reasons. But it can always be some kind of inverted bit by some high-energy beam from some friendly space neighbor.
Interested in the cause, I did what you do in such cases: I tried it on my machine to post “It works for me”. And the repetition of the situation from the post “July 31 - December 31” in my car gave the correct result “5 months”. But after testing a bit, I found that "July 31 - December 30" actually causes an error. The not quite correct value “5 months, 613566756 weeks, 3 days” is displayed.
')
I have not finished loosening the program, and then I remembered: “Oh, and is a calculator not one of those things for which Microsoft opened the source code?”
And indeed . This error could not be too complicated, so I thought that I would try to find it. Downloading the sources was easy enough, and adding the required UWP workload to Visual Studio also went without a hitch.
Navigating through code bases that 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 a 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 seemingly suitable for the name
DateDiff_FromDate to DateCalculatorViewModel.cpp
and, finally,
DateCalculator.cpp
.
By setting a breakpoint and looking at some variables, I saw that the final
DateDifference
value
DateDifference
already wrong. That is, it was not just a conversion error to a string, but an actual calculation error.
The actual calculation in the simplified pseudocode looks 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) {
It looks fine. There is no problem in logic. In essence, the function does the following:
- counts off full years from starting date
- since the date of the last full year counts the months
- since the date of the last full month, it counts weeks
- since the date of the last full week counts the remaining days
In fact, the problem lies in the assumption that the sequential launch
date = advance_date_by(date, month, somenumber) date = advance_date_by(date, month, 1)
equals
date = advance_date_by(date, month, somenumber + 1)
Usually this is the same thing. But the question arises:
"If you hit the 31st day of the month, next month 30 days, you add one month, then where will you go?"It seems that for
Windows.Globalization.Calendar.AddMonths (Int32) the answer will be “on the 30th number”.
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 neither
distributive (with AddMonth-multiplication), nor
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, does the range setting error result in such a huge number of weeks? As you might guess, this occurs because
days_diff
is an unsigned type. This turns -1 days into a huge amount, which is then passed to the next iteration of the loop with the weeks. Which then tries to correct the situation by reducing the
current_guess
, but not reducing the unsigned variable.
Well, it was an interesting way to spend a Sunday. I created a
pull request on Github with a minimal “fix”. I put the "correction" in quotes, because now the calculation looks like this:

I think 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 date difference. But in any case it is less wrong than it was.