16bit変数でやりくりしてエオルゼア時間を計算する

# はじめに 
FF14が2日間メンテナンスの今、やることがありません。
こんなときは、そうですね！エオルゼア時計です！！！(n回目)

でもせっかくなので、いつかマイコンボードに組み込むときのために16bitでやりくりして計算できるようなコードを用意しておきましょう。

# 前提のおさらい 

エオルゼア時間は日本時間の `1440 ÷ 70` 倍のスピードで進みます。
また、日本時間日曜日0時0分には、エオルゼア時間も0時0分になります。

これまでの私の記事では `日本時間が週内で経過した秒数` × `1440 ÷ 70` = `エオルゼア時間秒数` として算出した値から計算してエオルゼア時間の時と分を求めていました。

ただし`日本時間が週内で経過した秒数` は 最大で `604799` 秒になるため 少なくとも符号なし20bitの変数が必要です。

またこの際の `エオルゼア時間秒数` の最大は `12441599` 秒ですので、こちらは少なくとも符号なし24bitの変数が必要です。

とはいえ32bitより少ないbit数の環境だと、次は基本的に16bitになってしまいますので何とかしてその16bit数で計算する方法を考えてみましょう。

それ以前に24時間の最大秒数 `86399` を格納しようとすると符号なし17bitの変数が必要です。あと1bit少なければ……。

嘆いていてもしょうがないので、時刻は区分である午前・午後と12時間分の秒数に分けて格納することにしましょう。
そうすると区分がbool変数に、秒数が符号なし16bit変数に収まります。

# ルックアップテーブルの作成 

周期性がないか確認するためにGoogleスプレッドシートであれこれ計算させていると次のような日曜日の時刻計算結果が出てきました


|No.|JST時|JST分|ET区分|ET秒(区分毎)|
|-:|-:|-:|-:|----:|
| 1|00|00|AM|    0|
| 2|00|10|AM|12342|
| 3|00|20|AM|24685|
| 4|00|30|AM|37028|
| 5|00|40|PM| 6171|
| 6|00|50|PM|18514|
| 7|01|00|PM|30857|
| 8|01|10|PM|43199|
| 9|01|20|AM|12342|
|10|01|30|AM|24685|

※10番以降省略。JST秒数は0とした場合。

この表から、2番～8番のJST 70分単位で周期性があることが分かります。
さらにこの表を基に次のようなルックアップテーブルを作成しました。

|Index|is_pm|et_sec|
|-:|-:|----:|
| 0|false|12342|
| 1|false|24685|
| 2|false|37028|
| 3|true| 6171|
| 4|true|18514|
| 5|true|30857|
| 6|true|43199|

`JST 10分経過回数 - 1` の `7` との余算でこのテーブルを引くと、10分単位時点でのエオルゼア秒数を取得できます。
1回も10分単位で経過していない場合は、例外としてエオルゼア秒数は0とします。

# プログラムコード

先ほどのルックアップテーブルを使うC++コードは下記の通りです。

```cpp:et2.cpp
// ============================================================ #
//  Copyright (c) 2024 CIB-MC
//  Released under the MIT license
//  https://opensource.org/licenses/mit-license.php
// ============================================================ #

export module et2;

#include <inttypes.h>

namespace et2 {
        struct LT_ITEM {
                bool is_pm;
                uint16_t et_sec;
        };

        const LT_ITEM LOOKUP_TABLE[] = {
                {false, 12342},
                {false, 24685},
                {false, 37028},
                {true,   6171},
                {true,  18514},
                {true,  30857},
                {true,  43199}
        };

        export struct TIME {
                bool is_pm;
                uint16_t hour;
                uint16_t min;
                uint16_t half_day_sec;
        };

        export TIME get_time(uint16_t day_of_week, bool is_pm, uint16_t hour, uint16_t min, uint16_t sec) {
                uint16_t count_offset = is_pm ? 12 : 0;
                uint16_t count = day_of_week * 144 + (hour + count_offset) * 6 + min / 10;
                int16_t  lt_index = count == 0 ? -1 : (count - 1) % 7;
                uint16_t base_is_pm  = (lt_index == -1 ? false : LOOKUP_TABLE[lt_index].is_pm);
                uint16_t base_et_sec = (lt_index == -1 ? 0 : LOOKUP_TABLE[lt_index].et_sec);

                uint16_t diff_from = (min / 10) * 10;
                uint16_t diff = (min - diff_from) * 60 + sec;
                uint16_t sec_of_et = base_et_sec + (diff * 20) + ((diff * 57) / 100) ; 
                uint16_t half_day_sec = sec_of_et % 43200; 
                uint16_t et_hour = sec_of_et / 3600 % 24;
                bool     et_is_pm = base_is_pm ^ (et_hour >= 12);
                uint16_t et_hour_12h = et_hour % 12;
                uint16_t et_min = (sec_of_et / 60) % 60;
                TIME time = {
                        et_is_pm,
                        et_hour_12h,
                        et_min,
                        half_day_sec
                };
                return time;
        }
}
```

`base_is_pm ^ (et_hour >= 12)` として、ルックアップテーブルで想定していた午前と午後の区分を超えてしまった場合にも区分が正しく出力されるようにしています。

併せて、下記のように従来手法の計算をモジュール化しました。

```cpp:et1.cpp
// ============================================================ #
//  Copyright (c) 2024 CIB-MC
//  Released under the MIT license
//  https://opensource.org/licenses/mit-license.php
// ============================================================ #

export module et1;

namespace et1 {
        const double TO_ET_MULTIPLIER = 1440.0d / 70.0d; 

        export struct TIME {
                bool is_pm;
                int hour;
                int min;
                int half_day_sec;
        };

        export TIME get_time(int day_of_week, bool is_pm, int hour, int min, int sec) {
                int sec_offset  = is_pm ? 43200 : 0;
                int sec_of_week = day_of_week * 86400 + sec_offset + hour * 3600 + min * 60 + sec;
                int sec_of_et   = sec_of_week * TO_ET_MULTIPLIER;
                int sec_of_et_day = sec_of_et  % 86400;
                int half_day_sec  = sec_of_et_day % 43200;
                int et_hour = int(sec_of_et_day / 3600) % 24;
                bool et_is_pm = (et_hour >= 12);
                int et_hour_12h = int(et_hour % 12);
                int et_min = int(sec_of_et_day / 60) % 60;
                TIME time = {
                        et_is_pm,
                        et_hour_12h,
                        et_min,
                        half_day_sec
                };
                return time;
        }
}
```

これらのファイルを都合のよい作業ディレクトリ下の `./modules/` ディレクトリに格納しておきます。

また、これらのモジュールをテストで動かすための、下記cppファイルを用意します。
従来手法で求めたエオルゼア時刻と新手法のそれを比較しています。
また従来手法と新手法の時刻が合わない場合は、エオルゼア秒の差が `±2` 以内であれば `Fuzzy OK` として取り扱います。

~~ゲームクライアント内時計でない時点で残念ながら、おもちゃでしかないので。~~

ついでに実行時間もそれぞれ計測しておきましょう。

```cpp:main.cpp
// ============================================================ #
//  Copyright (c) 2024 CIB-MC
//  Released under the MIT license
//  https://opensource.org/licenses/mit-license.php
// ============================================================ #

#include <cstdio>
#include <inttypes.h>
#include <ctime>

using namespace std;

import et1;
import et2;

int main() {
        int sec_till = 604800;
        int fuzzy_ok_count =0;
        int ng_count = 0;

        clock_t et1_time = 0;
        clock_t et2_time = 0;
        for (int i = 0; i < sec_till; i++) {
                uint16_t day_of_week = (uint16_t)(i / 86400);
                bool     is_pm       = ((i / 3600 % 24) >= 12);
                uint16_t hour        = (uint16_t)(i / 3600 % 12);
                uint16_t min         = (uint16_t)(i / 60 % 60);
                uint16_t sec         = (uint16_t)(i % 60);

                time_t et1_start = clock();
                et1::TIME et1 = et1::get_time(day_of_week, is_pm, hour, min, sec);
                time_t et1_end = clock();
                et1_time += et1_end - et1_start;

                time_t et2_start = clock();
                et2::TIME et2 = et2::get_time(day_of_week, is_pm, hour, min, sec);
                time_t et2_end = clock();
                et2_time += et2_end - et2_start;

                int diff = (
                        (
                                (et1.half_day_sec - (int)et2.half_day_sec) +
                                (et1.is_pm == et2.is_pm ? 0 : 43200)
                        )
                ) % 43200;

                bool is_ok = (et1.is_pm == et2.is_pm && (uint16_t)et1.hour == et2.hour && (uint16_t)et1.min == et2.min);
                bool is_fuzzy_ok = (!is_ok && (-2 <= diff && diff <= 2));
                if (is_fuzzy_ok) {++fuzzy_ok_count;}
                if (!(is_ok || is_fuzzy_ok)) {++ng_count;}
        }
        printf("NG count is %d.%s\n", ng_count, ng_count == 0 ? " ALL OK!" : "");
        printf("(Fuzzy OK count is %d) (%lf%%)\n", fuzzy_ok_count, (double)fuzzy_ok_count / (double)sec_till * 100);
        double et1_ms = (double)et1_time / CLOCKS_PER_SEC * 1000.0d;
        double et2_ms = (double)et2_time / CLOCKS_PER_SEC * 1000.0d;
        printf("(et1 execution time is %lfms) (%lfms per a real sec unit)\n", et1_ms, et1_ms / sec_till);
        printf("(et2 execution time is %lfms) (%lfms per a real sec unit)\n", et2_ms, et2_ms / sec_till);
}
```

:::note info
@fujitanozomu 様より `diff` の算出時に時刻区分(午前・午後)が考慮されていないとの指摘がございました。
時刻区分に変動がある場合にのみ、差の数値に下駄を履かせるように変更したしました。
:::

下記のようにしてコンパイル後、実行すると次のような結果を得ることができます。

```text:result
$ gcc -std=c++20 -fmodules-ts ./modules/*.cpp main.cpp -O2 -o et.bin
$ ./et.bin 
NG count is 0. ALL OK!
(Fuzzy OK count is 17708) (2.927910%)
(et1 execution time is 398.303000ms) (0.000659ms per a real sec unit)
(et2 execution time is 398.761000ms) (0.000659ms per a real sec unit)
```

求めた時刻の差は許容範囲内、かつ従来手法と遜色のない実行時間で算出できているようです。

# おわりに 

8bit版はやらないです！