我為Windows 10 修復了一個bug
據這名開發者(下用Peter代稱)介紹,他某日在Reddit閒逛時,一個位於Windows 10子版塊下的帖子引起了他的注意。帖子內容如下:和大家一樣,在計算兩個日期之間的相隔天數時,Peter也發現了關於週數的描述明顯是錯誤的,如此大的數值看起來應該是上溢或者下溢之類的問題,要不就是差一錯誤(off-by-one)等常見的邏輯錯誤。
本著對這個bug 的好奇心,再加上 Windows 10 計算器是開源項目,Peter 認為解決這個問題應該不會太複雜,所以他希望親自找到bug 並進行修復。
他先在自己的電腦上測試看是否能複現,按照帖子的示例,在測試7.31-12.31 的間隔天數時,計算器返回的結果是正確的—— “5個月”。接著Peter 稍微改了一下日期,改成 7.31-12.30 時,bug 復現了,計算器顯示的值為:“5 months, 613566756 weeks, 3 days”,這明顯是錯誤的。
確定了bug的存在,Peter決定從 Windows計算器的GitHub倉庫下載源碼來研究一番。從repo把源碼下載到本地後,由於在IDE運行Windows計算器項目需要UWP workload,所以Peter還為 Visual Studio添加了UWP workload。不過Peter表示搭建開發環境也十分順利,修bug第一步至此完成。
接著Peter打開了解決方案文件(solution file),查看“Calculator”項目,並蒐索看似相關的任何文件。他找到了界面文件DateCalculator.xaml
,接著從相關的文件DateDiff_FromDate
追踪到了DateCalculatorViewModel.cpp
,最後找到DateCalculator.cpp
。
然後Peter開始設置斷點並觀察相關變量的變化,他發現final變量DateDifference
的值有誤。因此他判斷這個bug不是由轉換為字符串存在錯誤而導致的,而是實實在在的計算錯誤。
既然計算存在問題,那就看看它的計算邏輯是如何實現的。
Windows計算器對間隔日期的計算邏輯用偽代碼表示如下:
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 }
上面的代碼主要做了這些事:先算出相差的年數、然後計算相差的月數、接著計算相差的周數、最後計算相差的天數。
Peter 表示這看起來很正常,他沒發現其中的邏輯存在錯誤。
問題正是在於此,寫這段代碼的人以為代碼會按預料中執行:
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) 來說,它的答案顯然是“在第30天”。
具體而言,這就意味著:
“July 31st + 4 Months = November 30th”
“November 30th + 1 Month = December 30th”
然而實際情況是:
“July 31st + 5 Months = December 31st”
這就引起了“差一錯誤”。逐步調用Window::Globalization::Calendar::AddMonths
會導致GetDifferenceInDays
出現負值,然後將其分配給無符號變量daysDiff
,經過後面的循環迭代後,daysDiff
會將這個負值變為更大的數字。
接著Peter在Windows計算器的GitHub倉庫提交了一個PR 以進行最小化“修復”。
Peter 為修復加上了引號,是因為它最後計算出的結果如下:
Peter 表示,如果各位認可“7月31日+ 4個月= 11月30日”這樣的結果,他認為這在技術上是正確的。雖然完整的結果不符合大眾對日期間隔天數的閱讀習慣,但至少不會出錯。
不過這件事中,最令人深刻的是微軟最後合併了Peter 提交的PR 以修復這個問題。
這說明微軟的開源項目不僅僅是將代碼託管在GitHub 而已,而是會聽取來自社區用戶的建議和改進。
那麼問題來了,如果是你,你會怎樣解決這個錯誤呢?