グラフ電卓のMicroPythonで曜日・日数を求める

## 動機
fx-9750GIII や fx-CG50 には Finance アプリがあり、この中に日付・曜日計算機能がある。
一方、 Graph Math+ には今のところ Finance アプリが搭載されていない（2025年のアップデートで搭載予定とする情報もあるが）。
そこで MicroPython で曜日を求めるスクリプトを入れておこうと思い立った。

## 曜日判定はツェラーの公式を使う
Graph Math+ に搭載されている MicroPython では datetime モジュールなどを利用することができない。
そこでツェラーの公式を使って計算で求めるのが一般的な解決法。実際に Python コードを公開している人がいた。

https://stabucky.com/wp/archives/13945

そこでこれを電卓で入力しやすい形に微修正して…
```zeller.py
def zeller(year, month, day):
  if month <= 2:
    year -= 1
    month += 10
  else:
    month -= 2
  w = day + int((13 * month - 1) / 5) + year + int(year / 4) - int(year / 100) + int(year / 400)
  x = w % 7
  return x

ws = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]

date = input('YYYYMMDD? ')

year = int(date[0:4])
month = int(date[4:6])
day = int(date[6:8])

x = zeller(year, month, day)

print('%d/%d/%d(%s)' % (year, month, day, ws[x]))
```
こんな感じに実行できる。
```:zeller.py 実行画面
YYYYMMDD? 20241201
2024/12/1(Sun)
>>>
```

## 日数計算
下記ブログ記事にあった「VB6のプログラムにあった計算方法」の式を使ってみる。

https://ameblo.jp/ysama2021/entry-12804574660.html

このやり方が秀逸なのは、3月を起点にした月数に30.59を掛けて整数化しているところ。
3月から前月までの累積日数と30違いで一致する。

```Python: 日数計算
def datediff(y1, m1, d1, y2, m2, d2):
  if m1 == 1 or m1 == 2:
    m1 = m1 + 12
    y1 = y1 - 1
  
  if m2 == 1 or m2 == 2:
    m2 = m2 + 12
    y2 = y2 - 1
  
  days1 = int(365.25 * y1) + int(y1 / 400) - int(y1 / 100) + int(30.59 * (m1 - 2)) + d1
  days2 = int(365.25 * y2) + int(y2 / 400) - int(y2 / 100) + int(30.59 * (m2 - 2)) + d2
  days = days2 - days1
  return days
```
こんな感じで日数が求められる。
```: 日数計算
YYYYMMDD? 20241127
2024/11/27(Wed)
Target? 20250101
20241127Wed-20250101Wed
Diff: 35
```

## 何日後・何日前も求めよう
上記の整数化された年月日は、0年3月1日を31としたシリアル値となっている。
このシリアル値から日付を求めることができれば、何日後・何日前の日付を求めることが可能だ。
400年、100年、4年、年、月、日と順にたどっていけば復元できるはず。
…が、簡単にはいかない。
かなりトリッキーに処理をした。もっとよい方法があると思う。

## 完成品
- Graph Math+ が未着の段階で作成したので、 Google Colab と fx-CG50 のエミュレータで動作確認をした。 fx-CG50 でも使えるが、文字サイズが大きいため、一部の文字ははみ出して表示される。
- エラー処理はしていない。
- グレゴリオ暦以前（1582年以前）の日付は正しくない。
```date.py
def ymd(date):
  year = int(date[0:4])
  month = int(date[4:6])
  day = int(date[6:8])
  return year, month, day

def zeller(year, month, day):
  if month <= 2:
    year -= 1
    month += 10
  else:
    month -= 2
  w = day + int((13 * month - 1) / 5) + year + int(year / 4) - int(year / 100) + int(year / 400)
  x = w % 7
  return x

def date_to_serial(year, month, day):
  if month == 1 or month == 2:
    month = month + 12
    year = year - 1
  s_day = int(365.25 * year) + int(year / 400) - int(year / 100) + int(30.59 * (month - 2)) + day
  return s_day

def serial_to_date(s_day):
  s_day = s_day - 31
  y400 = int(s_day / 146097)
  s_day = s_day - y400 * 146097
  y100 = int(s_day / 36524)
  if y100 == 4:
    y100 = 3; y4 = 0; y1 = 99; month = 11; day = 29
  else:
    s_day = s_day - y100 * 36524
    y4 = int(s_day / 1461)
    s_day = s_day - y4 * 1461
    y1 = int(s_day / 365)
    if y1 == 4:
      y1 = 3; month = 11; day = 29
    else:
      s_day = s_day - y1 * 365
      for i in range(12):
        if s_day < round((i+1)*30.6):
          month = i ; day = s_day - round(i*30.6) + 1 ; break
  year = y400*400 + y100*100 + y4*4 + y1
  month = month + 3
  if month > 12:
    year = year + 1
    month = month - 12
  return year, month, day

ws = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]

while True:
  date = input('YYYYMMDD? ')
  if len(date) != 8:
    break
  
  else:
    year, month, day = ymd(date)
    x = zeller(year, month, day)
    s_day1 = date_to_serial(year, month, day)
    print('{:04}/{:02}/{:02}({})'.format(year, month, day, ws[x]))
    date2 = input('Target/Diff? ')

    if len(date2) == 8:
      year_t, month_t, day_t = ymd(date2)
      y = zeller(year_t, month_t, day_t)
      s_day2 = date_to_serial(year_t, month_t, day_t)
      days = s_day2 - s_day1
      print('{:04}{:02}{:02}{}-{:04}{:02}{:02}{}'.format(year, month, day, ws[x], year_t, month_t, day_t, ws[y]))
      print('Diff: %d' % days)

    else:
      days = int(date2)
      s_day2 = s_day1 + days
      year_t, month_t, day_t = serial_to_date(s_day2)
      y = zeller(year_t, month_t, day_t)
      print('{:04}{:02}{:02}{}[{:+}d]'.format(year, month, day, ws[x], days))
      print('{:04}/{:02}/{:02}({})'.format(year_t, month_t, day_t, ws[y]))
```

## 使い方
- 曜日だけ求めたい時は最初のInputで終わらせればよい（ACキーで止められる）。
- 次のInputを8桁で入力すると、日数が表示される。
![python_date_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/8562137e-ba66-c358-8aac-a79180f4ecd1.png)

- 次のInputを8桁以外で入力すると、日付が表示される。負の値も入力可。
![python_date_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/3504736c-d74f-a25f-6342-0eef96df70aa.png)

- 日数or日付の答を出力した後、最初の入力（ `YYYYMMDD?` ）に戻る。ここで8桁以外の入力（EXEキーのみも可）で正常終了する。
  　

- fx-CG50 や fx-9750GIII で動作させると以下のようになる（いずれもエミュレータ）。
![python_date_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/bc080acb-1c78-b7ff-d215-3d6823d62299.png)
![python_date_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3943774/5290b839-eb28-714e-a686-72b8edcc8d70.png)