日常2(3か月カレンダー可変曜日)
このページはマス旅の一部です。
カレンダーといえば1か月が多いですね。
でも月末になると、翌月が気になったり、前月を思い出したくなったりしませんか。
1.3か月表示のカレンダーを作ろう
前月と翌月の分がいっしょに見れるカレンダーがあると便利ですね。
日曜スタートと月曜スタートを切り替えられたらうれしいかも。
課題:3か月カレンダー
「西暦AD:yyyy 月:mm」
を入力すると、
「yyyyのMMを中心に3か月カレンダー」
月スタートか日スタートを切り替えらえる。
を表示してくれる。祭日表示、休日表示はない。
2.数学外の知識
今の日本の暦は1582年にローマ教皇によって制定されたグレゴリオ暦(太陽暦)です。
(グレゴリウス13世は、ユリウス暦の1582年10月4日木曜日の翌日を、グレゴリオ暦の10月15日金曜日として実施した。)
カレンダーおたくではないので、現在から126年前の年1900年以降のカレンダーで十分とする。
ちなみに、1900年1月1日は月曜日です。
うるう年はうるう月(2月)にうるう日(29日)があるだけです。
月の日数のリストを1から12まで作ると、
平年の月の日数のリスト={31,28,31,30,31,30,31,31,30,31,30,31}
閏年の月の日数のリスト={31,29,31,30,31,30,31,31,30,31,30,31}の2種類がある。
うるう年(LeapYear)は、西暦年数が4で割り切れる年が原則ですが、
100で割り切れて400で割り切れない年は平年となります(例: 2100年は平年、2000年はうるう年)
#最大表示行数はいくつあるか?
もしも西暦ADのyyyy=2026、月をmm=09と入れると、
2026の8月、9月、10月の3か月分の日付が表示されればよい。
でも、もっとカレンダーをリアルに考えよう。
3か月とも31日ということはないね。かならず30日が入る。3か月合計で31×2+30=92日。
前前月の末が最大6日めり込むと6+92=98日=7×14から、14週ピッタリ。
前前月の末のめり込みを減らすと、その分翌々月の分が入り込むから、
3か月を完全に表示するためには14週あればよいとわかる。
3.数学・言語・プログラミングの支援
#西暦の日付から曜日を計算する「ツェラーの公式」がありますね。
使うための準備が必要です。
月(M)1月、2月は前年の13月、14月として計算する。2026年1月は2025年の13月
年(Y)1月、2月は西暦から1引く。
結果(W)は日=0、月=1、…土=6という法7の
剰余で曜日を表す。
YY=Y+floor(Y/4)-floor(Y/100)+floor(Y/100)
MM=floor((13 M+8)/5)
と変換とします。
W = YY+MM+D ( mod 7)
#前月(初月)の1日の曜日W(0~6)を求める。
入力された、yyyy,mmから、前月の年(prevY),前月の月(prevM)
1月の前月は12月で前年だから、
prevY = If(mm == 1, yyyy - 1)
prevM = If(mm == 1, 12)
2月の前月は1月で前年の13月
prevY = If(mm == 2, yyyy - 1)
prevM = If(mm == 2, 13)
3月の前月は2月で前年の14月
prevY = If(mm == 3, yyyy - 1)
prevM = If(mm == 3, 13)
4月からの前月は年はそのままで月だけ1引く
prevY = If(それ以外, yyyy)
prevM = If(それ以外,mm-1)
まとめると、
prevY = If(mm < 4, yyyy - 1, yyyy)
prevM = If(mm < 4, 12, mm - 1)
YY=prevY+floor(prevY/4)-floor(prevY/100)+floor(prevY/400)
MM=floor((13 prevM+8)/5)
D=1として、
W = Mod(YY + MM +1 ,7)
を出す。
#前月(初月)の初日(1)の表示位置(colとする)が左から何番目になるか。
startMonに日曜スタートなら0を入れ、月用スタートなら1を入れる。
たとえば、W=1(月)だとしよう。
startMon=1ならカレンダーの1番目にくるし、startMon=0ならば、カレンダーの2番目に来る。
つまり,colはstartMon=1ならWのまま入り、そうでないならW+1が入ります。
でも、W=0は例外です。startMon=1で、col=W=0となり範囲外です。W=0つまり日曜日は月曜スタートの週の最後、7番目にくるのです。
まとめると、
col=if(startMon==1,if(W==0,7,W),W+1)
とすればいいですね。
#カレンダーは長方形
カレンダーは長方形ですが、日付は直線的に単調増加して、次の月になると1から始まる。
これを繰り返す1本の線のようにデータcellsを作り、
それをビュー(表示)のときに7列に区切ればよいね。
その1本のデータを作る1つ1つの入れ物をセルと呼ぼう。
セルは全部で14週あるから7×14=98ある。
#cellsデータを作る
うるう年かどうかのチェックをしないと、2月の月末日が決まりません。
うるう年は、
ベン図でかくと4の倍数の中の100の倍数の中の400の倍数というように3重輪になった図で考えます。
基本4の倍数ですが、100の倍数から400の倍数をぬいた部分だけ平年ドーナツができてしまいます。
言い換えると、4の倍数から100の倍数をぬいドーナツに、400の倍数を追加したものですね。
r4=Mod(yyyy,4)==0
r100=Mod(yyyy,100)==0
r400=Mod(yyyy,400)==0
真理値がr4,r100,r400に入るので、うるう年判定結果は一行で表せますね。
isLeap= (r4∧not(r100)) ∨ r400
m=1,2,3のときはyyyyでconsを調べることで、2月を28日にするか29日までにするかが決めます。
それ以外のときはcons=falseつまり平年としてもカレンダー上問題ありませんね。
LastDaysN={31,28,31,30,31,30,31,31,30,31,30,31}+{31,28,31,30,31,30,31,31,30,31,30,31}
LastDaysL={31,29,31,30,31,30,31,31,30,31,30,31}+{31,29,31,30,31,30,31,31,30,31,30,31}
isLeap=Trueで、mm=1,2,3のときはLastDays=LastDaysLとします。
それ以外では、LastDays=LastDaysNとしましょう。
geogebraはリストを作ってから、データの更新できないので、一気に作ってつなぎましょう。
前前月データはcol番の手前までを空白にします。
prepre:""をcol-1(個)ならべたもの
M=mm+12
preMonth:LastDaysリストのM-1番目を末日にした自然数を1から順にならべます。
targetMonth:LastDaysリストのM番目を末日にした自然数を1から順にならべます。
nextMonth:LastDaysリストのM+1番目を末日にした自然数を1から順にならべます。
nextnextは、98セルにまだ余裕があれば「""」で埋めます。
cells =prepre + preMonth + targetMonth + nextMonth + nextnext
のようにリストを連結します。連結は+かJoinなどを使いましょう。
#cellsの表示をカレンダー形式にする
98セル以下の線状のデータcellsを整形して表示すればいいね。
startMon=0なら曜日0を、=1なら曜日1をタイトル行にします。
曜日0={日、月、火、水、木、金、土}
曜日1={月、火、水、木、金、土、日}
タイトル行に曜日
表示は98セルのセル番号を1から7がリスト1、8から14がリスト2、。。。とリスト14を14個作ります。
geogebraならば、
それをTableText(曜日0か1、リスト1、リスト2、…、リスト14)
として、(14+1)行7列の行列表
として表示できるでしょう。
あとは、選択した当月の年と月を強調したければ、
年と月をテキストにオブジェクトとして読み込んだものを適当に貼り付ければよいでしょう。
4.コード化
<geogebra>
# 入力部分
yyyy = slider(1900,2050,1) #見出し:西暦AD
mm = slider(1,12,1) #当月
startMon = slider(0,1,1) #開始曜日
text0="開始曜日(1:月曜, 0:日曜)"
#計算部分
prevY= If(mm < 4, yyyy - 1, yyyy)
prevM= If(mm < 4, 12, mm - 1)
YY= prevY +floor(prevY/4) -floor(prevY/100) +floor(prevY/400)
MM= floor((13 prevM+8)/5)
W= Mod(YY + MM +1 , 7)
col= If(startMon == 1, If(W == 0, 7, W), W + 1)
# &&は∧に自動変換されますが、||はAbsに誤変換されるので、∧∨で貼り付けます。
isLeap= ( Mod(yyyy,4)==0 ∧ Mod(yyyy,100)!=0 )∨( Mod(yyyy,400)==0 )
LastDaysN= Join({31,28,31,30,31,30,31,31,30,31,30,31},{31,28,31,30,31,30,31,31,30,31,30,31})
LastDaysL= Join({31,29,31,30,31,30,31,31,30,31,30,31},{31,29,31,30,31,30,31,31,30,31,30,31})
LastDays= if(isLeap && mm < 4, LastDaysL ,LastDaysN)
M= if( mm <3,mm + 12,mm)
prepre= Sequence("",k,1,col-1)
preMonth= Sequence(k,k,1,Element(LastDays,M-1))
targetMonth= Sequence(k,k,1,Element(LastDays,M))
nextMonth= Sequence(k,k,1,Element(LastDays,M+1))
left= 98 -(col-1+ Element(LastDays,M-1)+Element(LastDays,M)+Element(LastDays,M+1))
nextnext= Sequence("",k,1,left)
cells= Join(prepre,preMonth,targetMonth,nextMonth,nextnext)
#表示部分
headers = If(startMon == 1, {"月", "火", "水", "木", "金", "土", "日"}, {"日", "月", "火", "水", "木", "金", "土"})
rs = Sequence( Take(cells,7k - 6,7k), k, 1,14)
#"c|_"のスタイルはコピペすると、"c|_")"に自動変換されて長い閉じカッコが付加されるので、手動で削ってください。バグです。
calTable= TableText( headers, rs(1),rs(2),rs(3),rs(4),rs(5),rs(6),rs(7),rs(8),rs(9),rs(10),rs(11),rs(12),rs(13),rs(14),"c|_")
#リストrsの番号指定はElemnt(rs,番号)とかくのが正式です。
#しかし、冗長でありマニュアルにも部分的にrs(番号)という書き方が載っている場合もあるので、
#視認性を優先しています。
3か月カレンダーで開始曜日が変えられるものを作ろう
<Pythonに移植しよう>
import math
# 入力部分
yyyy = int(input("西暦AD(yyyy): "))
mm = int(input("当月(mm): "))
startMon = int(input("開始曜日 (1:月曜, 0:日曜): "))
# 計算部分
prevY = yyyy - 1 if mm < 4 else yyyy
prevM = 12 if mm < 4 else mm - 1
YY = prevY + (prevY // 4) - (prevY // 100) + (prevY // 400)
MM = (13 * prevM + 8) // 5
W = (YY + MM + 1) % 7
# 列位置 col (Pythonは1-indexedではなく1〜7の数値として計算)
if startMon == 1:
col = 7 if W == 0 else W
else:
col = W + 1
# うるう年判定
isLeap = (yyyy % 4 == 0 and yyyy % 100 != 0) or (yyyy % 400 == 0)
# 月末日リスト(平年・閏年)
LastDaysN = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] * 2
LastDaysL = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] * 2
LastDays = LastDaysL if (isLeap and mm < 4) else LastDaysN
M = mm + 12 if mm < 3 else mm
# 1次元リスト cells の構築
prepre = [""] * (col - 1)
preMonth = [str(k) for k in range(1, LastDays[M - 2] + 1)] # M-1のインデックスはM-2
targetMonth = [str(k) for k in range(1, LastDays[M - 1] + 1)] # MのインデックスはM-1
nextMonth = [str(k) for k in range(1, LastDays[M] + 1)] # M+1のインデックスはM
# 残りセルを埋める
left = 98 - (len(prepre) + len(preMonth) + len(targetMonth) + len(nextMonth))
nextnext = [""] * left
# リストの連結
cells = prepre + preMonth + targetMonth + nextMonth + nextnext
# 表示部分
headers = (
["月", "火", "水", "木", "金", "土", "日"]
if startMon == 1
else ["日", "月", "火", "水", "木", "金", "土"]
)
# 7要素ずつ14行に分割(GeoGebraの Sequence(Take(...)) に相当)
rs = [cells[7 * k : 7 * k + 7] for k in range(14)]
# コンソールへの整形出力
print(f"\n===== {yyyy}年{mm}月を中心とする3か月カレンダー =====")
print(" | ".join(f"{h:2s}" for h in headers))
print("-" * 38)
for row in rs:
print(" | ".join(f"{cell:>2s}" for cell in row))
Pythonの出力例
