MyWebsite Bootstrap

如何做出如windows右下角的月曆?

04 Apr 2022 - Gengar

如何做出如windows右下角的月曆?

calculate calendar


這幾天看到了好幾年前練習用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/02

我們可以發現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)

到這邊基本上公式的解析也完成了,接下來就是把他寫成電腦看得懂的語言即可。

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


Reference

  1. 格里曆wiki
  2. 儒略日wiki
  3. 星期的計算wiki
Tags: #python