如何做出如windows右下角的月曆?
04 Apr 2022 - Gengar
如何做出如windows右下角的月曆?

這幾天看到了好幾年前練習用python寫月曆,當時的作法很直觀──直接選取某年某日當作第一天(未考慮曆法修正直接用了元年一月一號),並計算總共過了幾天以及閏年來算出想要的月曆。現在想想雖然這麼做是沒錯,但是是否有什麼可以快速得知任意一天是星期幾的方法呢? (徒法煉鋼的方式雖然可行,但總會有些神奇的公式可以快速算出的吧)
其實曆法修正的歷史是非常錯綜複雜的,對於公元元年一月一號到底是星期幾也是有許多爭議。 不過簡單來說就是有兩種主流曆法: (1)儒略曆, 格里曆的前身, 基本上在1582年10月5日之前都是使用儒略曆 (2)格里曆, 從1582年10月15日開始正式啟用, 正式啟用日同時也是儒略日的1582年10月5日 兩種曆法從1582年開始的比較可見wiki關於格里曆的這段

從上面的wiki可以發現2100年2月在格里曆沒有閏,只有28號,而儒略曆則有閏,會有29號。因為我們的目標是要做出跟windows右下角相同的月曆,所以先來看一下windows 10的2100年2月月曆是否有閏來得知是格里曆還是儒略曆

我們可以發現2100年2月只有28號沒有29號,表示windows預設的月曆為格里曆,不過如果要計算格里曆的月曆的話基本上只能算1582年10月15日之後才合理,在那之前的只能用外插的。
接著進入本篇文章的重點,到底要怎麼快速得到某年某月某日是星期幾呢?其實在wiki上面就有關於星期的計算,裡面有著用不同方式推導出來的許多公式,有興趣的可以參考參考。這邊我選了一個最容易理解跟計算的公式:
格里曆:
儒略曆:
其中: d=日, m=月-2, c和y分別為西元年的前兩位跟後兩位 (e.g., 2022 -> c=20, y=22), 但當月小於3時, 年-1 (e.g., 2022/02/02 -> 2021/12/02)
第一眼看到這個公式的時候一定會有一個想法,為什麼公式的變數不化到最簡,要有”m=月-2“這種奇怪的事情發生。事實上,這公式其實已經化到最簡了,只是跟一般大眾認知的年月不太一樣才會變這樣。
要計算某一天是星期幾只需要簡單的三個步驟 (1)找一個已知日期跟星期幾的基準日 (2)計算從基準日開始到目標日經過了幾天 (3)將(2)得知的經過天數除以7得到的餘數跟(1)中基準日的星期幾相加即可得到目標日是星期幾了。其中最困難的就是(2)了,原因是閏年也不是單純的4年一閏,他又有100年不閏且400年又閏的規則,如果人類歷史夠長的話可能還會出現1600百年不閏的規則吧。 而且這個閏日又在卡一年中的某一天(2/28之後的那天QQ),表示某年在2/28之前跟2/28之後相減的天數在有閏跟沒閏的年份會不一樣(例如: 3/1跟2/28在沒閏的時候只差一天,但是在有閏的時候卻差了兩天),這在計算上會變得很複雜。
簡化計算的最簡單方式就是把閏日放到一年的最後一天,這樣只有到了一年的最後才需要考慮(畢竟本來閏日就是一年365.2422後面的小數點產生的,理論上也是要累積了滿四年才有機會多補那一天)。但是我們也不能隨便就把12/32給弄出來,除非我們想要來創造一個新的曆法.. 所以我們只能把3/1當作是每一年的第一天,而2/28則是一年365天的最後一天,當有閏日2/29的時候將會接在一年365天結束後的第366天。
我們回到公式變數的討論,相信聰明的你已經發現為什麼剛剛的公式裡為什麼”m=月-2“了~ 原因就是我把要把3/1當作每一年的第一天,也就是3/1在計算中要當作是1/1,因此需要把我們一般認知的月份-2。另外以2022/02/02這天為例,在計算上他並不屬於2022年的年初,而是2021年的年末,這就是為什麼計算上要把2022/02/02當作是2021/12/02的原因了!
這邊我們不做公式的詳細推導,只做簡單的公式分析。一般去分析事情都可以從兩個方向去前進,一個是從因到果,另一個從果到因,以數學式子來說就是以前學的y(x)中的從x推到y或是從y推到x。從x推到y就是一般的公式推導,那是數學家在做的事情。對於數學不夠好的好奇心又旺盛的人,一般就是從數學好的人推導出來的公式去理解背後的意義即可~ 既然要從果推到因,就會跟數學的從括號裡面往外、從前面到後面的概念相反了。這邊我們會從括號的外面往內,從後面往前。
公式的概念如下 (註: a mod b指的是a除以b後取餘數,例如15 mod 7 = 15-7*2 = 1)
-
(一大坨) mod 7 –> 首先看到看到括號最外面的mod 7,這個的意思就是上面提到的三步驟的最後一個步驟(3),也就是要把所有經過的天數除以7找餘數來得到是星期幾,因此mod 7前面的那一大串裡面會包括中間經過的天數以及基準日是星期幾的資訊。選擇mod 7的原因是一星期就是七天,不論今天是星期X,每經過七天就會回到星期X。舉例來說,今天是星期一,18天後會是星期幾? 由1+18 mod 7 = 1+4 = 5就可以知道會是星期五啦。
-
5(c mod 4) –> 每100年會多出來的天數,mod 4會讓他變成4*100=400年一閏 (細看的話是每一百年多約5.22天,所以前面才會乘上5,而後面的mod 4會強制每四年進位一次,也就是本來的5*4 mod 7 = 6變成5*(4 mod 4) mod 7 = 0,如此一來每400年便會多一天)
-
5(y mod 4) + 3y –> 每1年會多出來的天數加上4年一閏的部分 (細看的話是每一年多約8.2422天,所以會是8*y的概念,並利用100年的那個小技巧,也就是5(y mod 4),造成每4年會強制進位一次,如此一來每4年便會多一天)
-
另外每次從99年到100年的時候只會多(5.22-4) mod 7 = 1.22天,也就是每100年不會閏一次(如果有閏的話應該要多(8.2422+1) mod 7 = 2.2422天),有興趣的可以自己算算看
-
[2.6m-0.2] –> 每個月會多出來的天數,中括號是指取小於括號內的最大整數,例如[12.8]=12 (細看的話是每一個月會比28天多2~3天。假設每個月多的天數是2跟3交替出現,那就會是[2.5m]->2+3+2+3…但實際上並不是只有2跟3交替出現,還會有連續兩個月都是3天的7跟8月,表示m前面乘的數字會大於2.5,可以用除了二月之外的11個月多出來的總天數除以11算出約略是29/11=2.63,並利用-0.2來控制哪個月是大月+3跟小月+2)
-
d –> 這個就是單純多的天數,應該不需要解釋了吧~
到這邊基本上公式的解析也完成了,接下來就是把他寫成電腦看得懂的語言即可。
Example 1 - 給出指定日期是星期幾
Input:
# Author: Gengar
weekday_ref = {'1': 'Mon', '2': 'Tue', '3': 'Wed', '4': 'Thu', '5': 'Fri', '6': 'Sat', '7': 'Sun'}
date = '2022-04-04' # date by your choose, try changing it to whatever you want in the format yyyy-mm-dd
# define d, m, y, c in Gauss's algorithm (transform March to the 1st month of a year)
yy, mm, d = [int(e) for e in date.split('-')]
if mm < 3:
m = mm+10
yy = yy -1
else:
m = mm-2
c, y = yy//100, yy%100
# define function of Gauss's algorithm
weekday_Gregorian = lambda d, m, y, c: int((d + (2.6*m-0.2)//1 + 5*(y%4) + 3*y + 5*(c%4)) % 7)
weekday_Julian = lambda d, m, y, c: int((d + (2.6*m-2.2)//1 + 5*(y%4) + 3*y + 6*(c%7)) % 7)
weekday_num = weekday_Gregorian(d, m, y,c) # find weekday of 1st day of the month (Gregorian)
print(f'{date} is {weekday_ref.get(f"{weekday_num}")}.')
Output:
2022-04-04 is Mon.
Example 2 - 輸出指定月份的月曆(格里曆)
Input:
# Author: Gengar
# month to days reference
month_days = {'1': '31', '2': '28', '3': '31', '4': '30', '5': '31', '6': '30',
'7': '31', '8': '31', '9': '30', '10': '31', '11': '30', '12': '31'}
date = '2022-04' # date by your choose, try changing it to whatever you want in the format yyyy-mm
# define d, m, y, c in Gauss's algorithm (transform March to the 1st month of a year)
d = 1
yy, mm = [int(e) for e in date.split('-')]
if mm < 3:
m = mm+10
yy = yy -1
else:
m = mm-2
c, y = yy//100, yy%100
# define function of Gauss's algorithm
weekday_Gregorian = lambda d, m, y, c: int((d + (2.6*m-0.2)//1 + 5*(y%4) + 3*y + 5*(c%4)) % 7)
weekday_Julian = lambda d, m, y, c: int((d + (2.6*m-2.2)//1 + 5*(y%4) + 3*y + 6*(c%7)) % 7)
# print calendar
first_weekday = weekday_Gregorian(d, m, y,c) # find weekday of 1st day of the month (Gregorian)
last_day = int(month_days.get(f'{mm}')) # get the last day numner of the selected month
calendar = ' Sun Mon Tue Wed Thu Fri Sat\n'
day = 1
for week in range(6):
for weekday in range(7):
if (weekday!=first_weekday and day==1) or day > last_day:
calendar += ' '*4
else:
calendar += f'{day:>4}'
day += 1
calendar += '\n'
print(calendar)
Output:
Sun Mon Tue Wed Thu Fri Sat
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
最後有個小提醒,其實python的library - datetime就有weekday的函數可以用
而他的星期一會是0,要注意一下
>>> from datetime import date >>> print(date(2022, 4, 4).weekday()) 0
附錄 - 比較上述公式算出來的跟datetime.weekday()的差別
自製weekday
# Author: Gengar
def my_weekday(date, algorithm='G'):
# define d, m, y, c in Gauss's algorithm (transform March to the 1st month of a year)
yy, mm, d = [int(e) for e in date.split('-')]
if mm < 3:
m = mm+10
yy = yy -1
else:
m = mm-2
c, y = yy//100, yy%100
# define function of Gauss's algorithm
weekday_Gregorian = lambda d, m, y, c: int((d + (2.6*m-0.2)//1 + 5*(y%4) + 3*y + 5*(c%4)) % 7)
weekday_Julian = lambda d, m, y, c: int((d + (2.6*m-2.2)//1 + 5*(y%4) + 3*y + 6*(c%7)) % 7)
if algorithm == 'G':
return weekday_Gregorian(d, m, y,c)
elif algorithm == 'J':
return weekday_Julian(d, m, y,c)
結果比較
>>> from datetime import datetime >>> target_date = '2022-04-04' >>> print(f'my weekday: {my_weekday(target_date)}') >>> print(f'datetime weekday: {datetime.strptime(target_date, "%Y-%m-%d").weekday()}') my weekday: 1 datetime weekday: 0