見出し画像

今年ももう M/D か……

一年で数回、「日付」と「経過日数の割合」がつりあう日があります。
それが 1/19、 3/15、 4/14、 8/13 の4日、直近では 4/14 です。

どういうことか。
もう少し噛みくだくと 4/14 は、年初 1/1 を一番目として一年間で 104 番目に当たります。(英語では day number と言うそう)
経過日数の割合 104/365 と日付表示の 4/14 それぞれを分数として見たとき、ほぼ一致する──そういう話です。
※ およそ 0.28493150684931506 と 0.2857142857142857 です。

「今年ももう 4/14 か……」という何気ないつぶやきは、 4 月 14 日であるという事実と、一年間のうち 4/14 が経過したという事実のふたつを同時に表示できるのです!
※ これを言うなら 8/13 が一番しっくりくるでしょう。日付として約分できず、 1/19 ほど分母が小さすぎず、そして半分以上過ぎてしまったという実感がともなう。

* * *

さて。
これら日付を導きだすのに手計算はいくらなんでも面倒。当然プログラム、今回は Python を使いました。
365 日分の日付のリストをつくり、リスト長に対するインデックス(つまり経過日数の割合)と、日付の分数の値が近いもの──1日未満としました──を選択する。
以下、解説。

まず 365 日に亘る日付のリストを作ります。

import itertools
concat = itertools.chain.from_iterable
days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
m_and_d = list(concat( ((m+1, d+1) for d in range(ds))
                       for m, ds in enumerate(days)))

各月の日数をリスト days に入れ、月と日のタプルのリスト m_and_d を組み立てる。
enumerate と range のジェネレーターふたつを内包表記で動かしてできる二重のリスト(のジェネレーター)を concat 関数(itertools.chain.from_iterable 関数につけた別名)で平たくし、さらに list コンストラクターを通して m_and_d のリストとします。

これだけでは少々わかりにくいので、もう少し具体的に説明します。
days = [31, 28, 31, …, 31] に対して enumerate(days) は iterable なオブジェクト ((0, 31), (1, 28), (2, 31), …, (11, 31)) を返します。このペアを m と ds で受けてループを回すのが for m, ds in enumerate(days)。
内包表記で、この m と ds から ((m+1, d+1) for d in range(ds)) を要素とする列を生成する。ちょっとしんどいですが、この要素がまた内包表記。先に「二重のリスト(のジェネレーター)」と表現したところで、これも列になります。
具体的には、まず (0, 31) を m と ds で受ける。 ds は for m in range(ds) で受けられ (0, 1, …, 30) の列を生成し、この列の要素それぞれを d で受けて (m+1, d+1) の組を生む。つまり ((1, 1), (1, 2), …, (1, 31)) になります。(m は 0、 d は 0 から 30 の列。 m と d を一つずつ増やした組をつくる)
続いて (1, 28) は ((2, 1), (2, 2), …, (2, 28)) に、 (2, 31) は ((3, 1), (3, 2), …, (3, 31)) に、……、そして (11, 31) は ((12, 1), (12, 2), …, (12, 31)) に。結果、各月の日付のリストを要素とするリストができるわけです。

このリストのリストから順番に要素を取りだすのが itertools.chain.from_iterable。(見方を変えるとリストを連結しているように見えますので concat と別名をつけました)
これはジェネレーターですので list コンストラクター関数を通してリストに変えて──結果、リスト m_and_d は:

>>> m_and_d
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (1, 6), (1, 7), (1, 8), (1, 9), (1, 10), (1, 11), (1, 12), (1, 13), (1, 14), (1, 15), (1, 16), (1, 17), (1, 18), (1, 19), (1, 20), (1, 21), (1, 22), (1, 23), (1, 24), (1, 25), (1, 26), (1, 27), (1, 28), (1, 29), (1, 30), (1, 31), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (2, 6), (2, 7), (2, 8), (2, 9), (2, 10), (2, 11), (2, 12), (2, 13), (2, 14), (2, 15), (2, 16), (2, 17), (2, 18), (2, 19), (2, 20), (2, 21), (2, 22), (2, 23), (2, 24), (2, 25), (2, 26), (2, 27), (2, 28), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5), (3, 6), (3, 7), (3, 8), (3, 9), (3, 10), (3, 11), (3, 12), (3, 13), (3, 14), (3, 15), (3, 16), (3, 17), (3, 18), (3, 19), (3, 20), (3, 21), (3, 22), (3, 23), (3, 24), …, (12, 31)]

さて、日付のリストができたので、この分数の値 m/d と経過日数 n の割合 n/365 が近くなる日付を求めます。
日付や経過日数は整数のため、割り算で端数が切りすてられないよう実数への変換を噛ませることに注意。(割り算に実数を忍びこませる)

triples = [((i+1)/365. - m/float(d), i+1, (m, d))
           for i, (m, d) in enumerate(m_and_d)]

割合と日付の差、日数、日付からなる三つ組のリスト triples を上述のとおり定義します。
そして割合と日付の差が1日未満になるものを抽出すると:

>>> [t[2] for t in triples if t[0] > -(1/365.) and t[0] < 1/365.]
[(1, 19), (3, 15), (4, 14), (8, 13)]

はい、出ました。冒頭に掲げた4日、 1/19、 3/15、 4/14、 8/13。

なお閏年の場合の計算は読者への課題として残しておきます。

* * *

見出し画像についての注意。
あたかも 1/19、 3/15、 4/14、 8/13 それぞれできれいな長方形ができるように描いていますが、嘘をついています。

365 は 19、 15、 14、 13 のいずれでも割りきれません。次のような微調整(数ラインの凸凹)が必要です。

1月は 19 日ごとに区切って 19 x 19 = 361 の箱をつくり、最終月ちかくの末尾4ラインに1日ずつ追加、
3月は 24 日ごとに区切って 24 x 15 = 360 の箱をつくり、年初の2ラインと年末の3ラインに1日ずつ追加、
4月は 26 日ごとに区切って 26 x 14 = 364 の箱をつくり、年末の最終ラインに1日を追加(はみ出す 12/31)、
8月は 28 日ごとに区切って 28 x 13 = 364 の箱をつくり、足りない1日を8番目のライン、 8/13 の当日として追加。

* * *

ところで満を持してつぶやいた 3/15 のツイートは、まったくバズりませんでした。寂しい😅

付録:日付番号


この記事が気に入ったらサポートをしてみませんか?