# 開発メモ fancy-date の開発者向け検討メモ・調査結果・実装ログ。エンドユーザー向けの使い方は [README](../README.md)、数詞ライブラリの詳しい設計案は [numeral-design.md](numeral-design.md) を参照。 ## 今後の検討テーマ ### format / token / span - 実装済み: `SpanLike = string | SpanDiff` は加算可能token (`y/M/w/d/H/m/s/S`) のみを受ける。表示順もこの地球的な期間の大きさ順に固定する。`D` は年初通日を使う測定精度として残すが、結果の `SpanDiff` では通常の日数 `d` へ正規化する。`parse_span()` は文字列を正数=未来方向の差分mapへ正規化し、`format_span()` はそこから `label` を導出する。ruby用の列は `Span` に保持せず `format_span_parts()` が `RichText` として都度導出する。`Precision = CorePrecision | Token` なので、`precise` には不断tokenも指定できる。 - 実装済み: 不断token・暦注tokenを `precise` に指定した `span_obj()` は、再適用不能な `SpanMeasure` (`precision/value/label`) を返す。`add()`/`sub()` は `SpanLike` に含まれないため受け付けない。「次の甲子日」などの探索は、周期差の加算ではなく別のfind系APIで扱う。 - `find_span(at, condition, { order, base })` の契約: `order=1/-1/0` は未来/過去/実時刻差で近い側、`base='at'/'match'` はSpanの基準時刻。`find()` の一方向列挙とは分け、R6/LM27を含むラベル条件を日境界探索して通常のSpanへ変換する。 - `span_sub({ dC60: '乙丑' }, { dC60: '甲子' })` は不断cycle限定のsymbolic変換。cycle上の順方向mod差を年cycle=`y`/日cycle=`d`へ割り当て、常に非負のanchorなしSpanを返す。`R6`/`LM27`/`Q`は非連続なのでinvalid。厳密な時刻差は`find_span()`の責務。`span_from_labels(token, from, to)`は互換wrapperとして残す。 - 部分実装: span 同士の symbolic 演算は `span_neg()` / `span_add()` / `span_sub()` で実装済み。同じ token だけを相殺し、異なる token 間の繰り上げ・相殺はしない。残課題は、平均サイズによる lossy な `span_approx()`、anchor 時刻の実暦境界に基づく `span_normalize()`、cyclic token span をどこまで演算対象に含めるかの整理。 - 採用済み token 命名: `dC` は「日不断の number-cycle」、`yC` は「年不断の number-cycle」を表す正本 token とする。干支系は `dC60/dC10/dC12`・`yC60/yC10/yC12` が正本で、既存の `dC/dCS/dCB`・`yC/yCS/yCB` と `A/C/B/a/c/b` は文化的 alias として残す。ルビは `dC60r` / `dC10r` / `dC12r` / `yC60r` など数値付き token に付く。同じ multi-character token registry で `Ha`=午前午後、`da`=paksha のような派生 token も扱える。 - 周期・暦注 token 方針: 九星は `yC9`=年九星、`dC9`=日九星。`E` は一般的な weekday token として、暦ごとの週相当 cycle (`dC7` / `dC8` / `dC10`) を指す。二十八宿のような日不断の宿は `dC28`。六曜は天文現象や lunar mansion ではなく旧暦月日から決まる暦注なので、固有 token `R6`(rokuyo 6)にする。二十七宿は lunar mansion ではあるが日不断ではなく旧暦月日由来なので `LM27` とし、`dC28` と分ける。旧 `f/F/V` alias は廃止し、それぞれ `yC9` / `dC9` / `dC28` または `LM27` を明示する。これらは format/find 表示・検索条件には使うが、通常 parse では日時座標の決定に使わない。干支などの周期 token から候補日時を推定する用途は、parse ではなく将来の find/候補探索 API 拡張で扱う。 - 今後の制約: multi-character token 追加時も、`format_parts()` の `{ token, text, ruby? }` 契約と「`text` 連結が `format()` と一致する」性質を維持する。 ### 数詞・ロケール - `number.ts` には `DIC`/`Numeral` 基盤(`jpn`, `old_jpn`, `english`, `roman`, `angle`)が実装済み。appendix は呼び出し時引数ではなく構築時に一度だけ確定する方式へ変更し、`DIC` に `.語尾(tail)` という日本語専用の公開ファクトリを追加する設計は [numeral-design.md](numeral-design.md) に集約済み。 - 「数詞体系・暦法・地域をまとめたものが暦」という整理に基づくバリエーション切り替えは、ロケール登録簿の設計でカタログ側は解決したが、`.spot()`(地域)側のバリエーション切り替えは未解決。 - 日本語漢数字の文化的バリエーションでは、位取り表記 vs 桁列挙表記が実装候補。和暦の日付は「十三日」のように位取りで畳むが、西暦4桁年は「二〇二四年」のように桁列挙が自然な文脈がある。`jpn.漢字` は位取り式なので、`english`/`roman` と同様に `DIC` を継承しない桁ごとの薄い `Numeral` 実装(例: `jpn.桁読み`)を別途用意する余地がある。 - 廿・卅・卌(合字)は、合字を使わない対抗ポジション(音便なしの `jpn.漢字` 相当)を並べる価値がある。一方、合字をさらに積極的に使う拡張は、体系的規則だった裏付けが薄いため見送るべき。 - 調査したが日付表示には無関係と判断したもの: 大字(壱弐参…)は金銭・法的数量の慣習に限定され、日付そのものを大字で書く歴史的・現代的用例は見つからなかった。命数法の万進/万万進/上数、仏教由来超大数、四の字忌避、地域方言による漢数字表記も、暦日付表示の実装軸としては採用しない。 Sources(多言語数詞一致体系の調査): [CLDR Plural Rules](https://cldr.unicode.org/index/cldr-spec/plural-rules) / [UTS #35 Numbers](https://www.unicode.org/reports/tr35/dev/tr35-numbers.html) / [Russian numerals - Wikipedia](https://en.wikipedia.org/wiki/Russian_numerals) / [Polish numerals - Wikipedia](https://en.wikipedia.org/wiki/Polish_numerals) / [Arabic grammar - Wikipedia](https://en.wikipedia.org/wiki/Arabic_grammar) / [No Gender Polarity in Arabic Numeral Phrases (Linguistic Inquiry)](https://direct.mit.edu/ling/article/52/3/441/97424/No-Gender-Polarity-in-Arabic-Numeral-Phrases) / [Swahili grammar - Wikipedia](https://en.wikipedia.org/wiki/Swahili_grammar) / [Japanese counter word - Wikipedia](https://ja.wikipedia.org/wiki/Japanese_counter_word) / [Tone sandhi - Wikipedia](https://ja.wikipedia.org/wiki/Tone_sandhi) / [Korean numerals - Wikipedia](https://en.wikipedia.org/wiki/Korean_numerals) / [大字(数字) - Wikipedia]() / [廿 - Wiktionary](https://ja.wiktionary.org/wiki/%E5%BB%BF) / [塵劫記 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%A1%B5%E5%8A%AB%E8%A8%98) / [阿僧祇 - Wikipedia](https://ja.wikipedia.org/wiki/%E9%98%BF%E5%83%A7%E7%A5%87) / [京(数) - Wikipedia]() / [四の字 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%9B%9B%E3%81%AE%E5%AD%97) / [縦書きの数字の書き方](https://everydaygoodthing.com/1460.html) ### 暦法・天文モデルの拡張 - インド系暦を本格対応する場合、日の出始まりの civil day 自体は `.dayStart('sunrise')` で表現できる。ただしヒンドゥー暦・パンチャーンガの実務では「日の出時点で存在する tithi をその日の日付/祭日に割り当てる」層が本体になる。必要な追加要素は、(1) tithi(月太陽離角12度ごとの30分割)・paksha(白分/黒分)・nakshatra/yoga/karana 等の位相トークン、(2) 日の出時点での tithi 採用、欠日(kshaya tithi)・重日(adhika/repeated tithi)の扱い、(3) amanta/purnimanta の月名方式、adhika masa/kshaya masa の月規則、(4) 太陽入宮(sankranti)による sidereal solar month と ayanamsha/黄道基準の選択、(5) 地域・宗派・祭日ごとの「前日/翌日採用」「日の出前後の持続条件」などの判定 DSL。単に `dayStart('sunrise')` を追加するだけでは不十分で、月相日を civil day へ投影する専用 rule/assignment 層が必要。 - 太陽暦の上位単位、マヤ長期暦、中東・インド・アフリカの暦を調査する。 - 歴史的時刻表現として、定気法に四半刻表現を採用するか、江戸時代以前の「分」「秒」に近い時刻表現を調査する。ローマ・ユリウス暦サンプルでは、H を horae temporariae、m を pars minuta として表示し、秒・ミリ秒は標準表示から外した。秒は内部精度としては残すが、古代/中世以前の生活時刻語彙として一般化しない。 - 天文モデルは、地球以外の天体向けに `src/nasa` の高精度モデルを追加済み。 - 楕円軌道創作天体対応を実装済み。`KeplerianOrbital` を純粋なケプラー楕円軌道モデルとして新設し、`KeplerianSolarOrbital` はそのラッパーにリファクタした。`placeKeplerianPlanet` / `placeKeplerianSatellite` で恒星・惑星周りの創作天体を配置でき、`src/sample/astro.ts` に `創作赤星` と `創作赤星の衛星` のサンプルを追加した。今後は彗星、多星系の暦を検討する。 - 暦外期間は、ロムルス暦のように暦月だけで1年を表現し尽くさない暦は他に類例が見当たらず、`month_divs` の `null` 要素 + `Indexer.list` への `null` 混在で表現した対応をこれ以上汎用化する必要は薄いと思われる。 ### 性能・パッケージ構成 - `Calendar` 初期化の遅延化を検討した。`import { Calendar } from 'fancy-date'` だけで `src/sample/calendars.ts` の多数の `FancyDate` インスタンスが即座に構築され、Cloudflare Workers のコールドスタートで CPU 予算超過(cpuTime 実測約2010ms)を起こした一因になった。 - 2026-07-11時点の perf 調査: 明確な劣等は cold require だった。対策前は `require('./lib/sample')`/`require('./lib/index')` が約1.86〜1.97sで、原因は `src/sample/calendars.ts` の全サンプル即時 `.init()` と、`src/index.ts` の sample 再エクスポートだった。`FancyDate.lazy(create)` を追加し、`Calendar` の各サンプルを enumerable lazy proxy 化して参照されたサンプルだけ初期化するようにした後は `require('./lib/index')`/`require('./lib/sample')`/`require('./lib/sample/calendars')` が約10〜14msまで低下した。`tithi()` assignment の per-call cost は現時点では支配的でないため、詳細値は `scripts/perf.js` 側の測定項目に留める。 - 調査の結果、コストの正体は「暦を何個構築するか」ではなく「モジュール評価そのもの」。`.init()` 配下は正規表現構築や固定長ループ中心で、天文学的な反復計算は `to_tempos()`/`lunisolar()` まで遅延されている。支配的コストは `sample/eras.ts` の元号配列や `naoj`/`nasa` の巨大な静的データ。 - Proxy による `Calendar` 遅延化は、サンプル暦の即時 `.init()`/`new FancyDate()` を避ける効果が大きかった。一方、同期 API を保ったまま `astro.ts`/`eras.ts` の import 自体を遅延するのは難しく、そこまで必要ならサブパス分割や動的 import を別途検討する。 - サブパス分割(`fancy-date/calendars/core` 等)は実効性があるが、現行の `Calendar.X` 集約アクセスを使い続ける限り恩恵はゼロ。消費側がサブパス import へ移行する非互換な変更が要る。将来、1〜数暦だけを使う利用者が現れた場合の候補として保留する。 - svelte-tick-timer の `/fancy` ページは複数の重い暦を意図的に同時表示するデモなので、暦単位の遅延化をしても結局多くを使う。その用途への対処は `export const ssr = false` のままでよい。 ## 既知課題 - 元号あり暦の anchor 表記規約: `calendar()` の anchor 文字列に `u` 年(era 調整前の通年 index)を使う場合、`parse()` でもその通年を正しく解釈できるようになった。`u` が `sub_tokens` に含まれていたため `parse_by()` 冒頭で `data.u` が `diff.u || 0` に上書きされ失われるのが真因だった。`u` を `main_tokens` へ移動し、`u` 指定時には `calc.eras` から実際の元号を特定して `era_relative_y` を計算、`parse_by()` の平均太陰太陽暦経路では `to_tempos()` の `u` 規則と同じ `EraAdjustedTempoRule` で年 envelope を解決することで、`format(parse(anchor_string)) === anchor_string` の round-trip を実現した。 - `dayBoundary()` は固定オフセットを d/N の構築規則だけに適用する。月・年境界まで丸める `dayStart('sunrise' | 'sunset')` とは違い、月頭の切り詰め区間は既知の例外として残る。 ## 実装済み・検証済み ### 実装済み: format / span / token 表記 - `format()` の既定書式は `Gy年M月d日(E)` の日付表示に戻した。秒・分・時は、暦法を使っていた文化圏やサンプルの主題として時刻表現を持つ場合だけ、各暦の `.lang()` で明示する。ローマ/ロムルス/和暦は従来通り文化的な時刻語彙を出し、バビロニア暦(カスプ/ベール)は時刻制度自体が主題なので `H時m分` まで、Beat は日付側を通常の西暦とし、Swatch Internet Time の時計サンプルとして `@HHH` までを標準表示にする。秒は内部精度に留め、標準表示には出さない。 - `format_parts(utc, fmt)` / `format_parts_by(utc, fmt)` を追加した。戻り値は `{ token, text, ruby? }[]`。`token` は元の format token、リテラル片は `''`、`text` の連結は常に `format()` と一致する。`ruby` は本文 token に添える読みがある場合だけ付け、`dC60r`/`Er` のような `r` suffix token は読みそのものを `text` にするため `ruby` を付けない。`format_parts_by()` が内部で `to_tempos_input()` するため、数値・文字列・解決済み `Tempos` のいずれでも使える。 - `format_parts()` の ruby は、`o`/`r` suffix では明示辞書(`to_label`/`to_ruby`)を使う従来仕様を維持する。一方 bare token には `.numeral_ruby()` で数値本文用の読みを付けられるようにした。`notation()` 第3要素が文字列の token では、直後の同じリテラルを ruby 対象の本文へ吸収するため、和暦の `d日` は `{ text: '24日', ruby: 'にじゅうよっか' }` になる。閏月 ruby は `閏` ではなく `うるう` を前置する。 - `format()` は内部的に `format_parts_by(...).map((p) => p.text).join('')` へ委譲する形にしたため、文字列出力と parts API の整合性を実装上保証している。旧 `format_by()` の役割は `format()` と `format_parts_by()` が巻き取った。 - svelte-tick-timer の `/fancy` ページは `format_parts()` ベースへ移行し、空白 split と固定配列分割代入をやめた。`FormatPart` から `{ text, ruby }` を作って `{text}{ruby}` に流し込む形になり、format 文字列・分割代入・テンプレートの三重管理を解消した。 - `labels()` と `parse_span()` / `format_span()` を追加し、span の表記を暦ごとに調整できるようにした。 - `Span` の内部 anchor を `{ calendar, at?, msec? }` に整理した。`span_obj(to, from)` 由来の span は `at=from` と `msec=to-from` を持ち、`parse_span(text, { at })` は msec なしの基準時刻だけを持つ。`span_msec(span, { at? })` は anchor の msec を使うか、基準時刻から `add()` して実ミリ秒へ変換する。span 同士の演算後は msec を喪失させる方針。 - `span_neg()` / `span_add()` / `span_sub()` を追加した。これは symbolic な span 演算で、同 token の値だけを足し引きし、異 token 間の繰り上げ・相殺は行わない。混合方向は `1ヶ月後31日前` のように part ごとに方向を表示する。演算後は msec を保持せず、anchor の `at` だけ条件付きで残す。 - `precise` に不断tokenを指定できる。加算可能tokenの測定結果は `SpanDiff` へ正規化し、不動の周期・暦注tokenは `SpanMeasure` として分離する。表示時のruby列は `format_span_parts()` が `RichText` として導出し、計算値には保持しない。 - 非 `precise` の span も、固定時間ではなく暦の秒・分・時・日境界に基づいて判定するようにした。 - SpanLike の `前` / `後` 省略表現(例: `1年2ヶ月`)を `後` として解釈するようにした。 - `.assign(...)` の受け皿を追加し、最初の具体例として `tithi(moony)` を `assign({ d: tithi(moony) })` に接続した。`tithi(moony)` は注入された月モデルと、`dayStart()` が決めた暦日境界時刻(`context.at`)で月相を30分割し、`d.now_idx` に割り当てる。`d.succ()`/`back()` が壊れないよう、assignment 前の civil day index は `raw_now_idx` に残し、遷移時はそれを使う。tithi 現象側の通し番号は `assignment_raw_now_idx` に分けて保持し、前後の raw tithi と比較して `assignment_flags` に `skipped`/`repeated` を付ける。assignment は token index の決定、`notation()` は表記、`division()` は時間分割、`dayStart()` は civil day 境界、という責務分離を維持する。サンプルとして `アマンタティティ` / `プールニマンタティティ` を追加し、既存の `アマンタ` / `プールニマンタ` は比較用に残した。tithi サンプルは観測に寄る暦として `天文月` / 満月基準の `天文黒分月` を使い、日の出境界も `hasSolarEvents` を持つ太陽モデルで解決する。 - `panchanga(calendar, utc)` / `panchangaNotes()` / `panchangaCandidates()` を sample helper として追加した。Maya の `mayaLongCount()` と同じく、暦本体の座標階層に無理に押し込まず、派生情報層として tithi/paksha/nakshatra/yoga/karana を計算する。祭日判定は `PanchangaNoteRule` の条件マッチ、parse candidate 化は `panchangaCandidates()` の日境界走査で足場を用意した。現状はサンプル用 helper で、地域・宗派ごとの採用ルール DSL は未実装。 ### 実装済み: 数詞・ロケール - 暦ごとの数値辞書を使った format/parse 入出力に対応した。 - `perf:*` 系の性能測定スクリプトを追加した。入力検証強化(NaN/Infinity ガード追加)後に `bun run perf:core` を実行し、parse/format/to_tempos/span/add-sub/太陰太陽暦/天文現象の既存水準から劣化していないことを確認済み。 - english 数詞の regex が任意の英字列を無条件に飲み込み、元号名・曜日名等と衝突しうる不具合を修正した。数詞語彙だけに一致する正規表現に差し替え、語彙を長さ降順に連結した。 - 韓国語数詞(`kor.漢語系`・`kor.固有系.基本`・`kor.固有系.助数詞前`)に `regex`/`to_number` を実装し、逆引き(parse)を可能にした。固有系の縮約形(`스무`/`스물한` 等)も往復できる。 - トークンごとの数詞出しわけ基盤を追加した。`Indexer` に `numeral`/`numeral_text`/`numeral_ruby`/`numeral_label`/`numeral_label_ruby` を持たせ、`format_number`/`parse_number`/`number_pattern` 等が各トークンの設定を優先して参照する。`FancyDate.token_numeral()` 等の公開APIと、`LocaleApplyOptions` の `token_numerals`/`token_numeral_texts`/`token_numeral_rubys`/`token_numeral_labels`/`token_numeral_label_rubys` から適用できる。`def_to_label()`/`def_regex()`/`def_to_idx()` も各トークンに対応する `Indexer` を配線し、format/parse 双方でトークンごとの数詞が使われるようにした。 - トークン数詞変更時に正規表現を再構築するよう `_set_token_numeral()` から `def_regex()` を呼び出す。`numeral`/`numeral_text` の変更が parse 正規表現に反映される。 - トークンの ruby 表示を、list/rubys が未設定の場合も `numeral_ruby` でフォールバックするようにした。これにより `to_table()` 等、`.format_parts_by()` を経由しない経路でも数詞読みが一貫して表示される。既存 snapshot は意図した挙動変更として更新した。 ### 天文・観測・入力安全性 - 天文定数・近似式の出典は [astronomy-sources.md](astronomy-sources.md) に集約する。平均惑星・衛星データは理科年表由来として扱うが、旧コードに版・ページが残っていないため未特定事項として明記している。 - 非地球惑星の太陽イベント共通層として `PlanetarySolarEventModel` を追加した。太陽黄経モデルは各惑星クラスに残し、赤道座標・地平座標・日の出/南中/日の入探索を共通化する。Mars は Mars24 系の黄経式、水星・金星は JPL SSD の近似 Kepler 要素(1800-2050向け)、冥王星は JPL SBDB の osculating elements から季節黄経を出す `KeplerianSolarOrbital`、木星・土星・天王星・海王星は `MeanPlanetSolarOrbital` の平均黄経モデルで接続している。平均モデルの惑星は、将来の高精度黄経式へクラス単位で差し替えやすい形にしている。 - `SolarEventDayTempoRule(..., 'sunrise' | 'sunset')` を追加し、`RealSunsetDayTempoRule` は `sunset` 固定の薄い互換 wrapper にした。日の出/日没境界の差は `solor()` の `日の出`/`日の入` 選択だけに寄せた。 - `LunarObservation`/`SolarObservation` に `has_sunrise`/`has_moonrise`/`has_transit`/`has_moonset` と `is_up_all_day` を追加した。対応する数値フィールドが NaN になりうる理由を型定義に JSDoc で明記した。`number | undefined` 化は内部影響が大きく見送った。 - mean モデル経路の `solor()` に南中高度・日の出方位・日の入方位を補った。日の入方位は日の出方位を北基準で反転して求める。精密モデルとの差は分点付近で最大0.25度程度。 - `format`/`add`/`sub`/`span` は既存の `to_tempos()`/`span_between()` 経由で NaN/Infinity を検出できる。`find()`、`solor()`、`lunar()`、`noon()` にも非有限値ガードを横展開した。 ### 暦サンプル・暦日境界 - `sample.ts` を `src/sample/` に分割した。 - `src/nasa` を追加し、Mars の太陽季節モデルを試験的に導入した。 - 地域暦の足場として、ナボナサル紀元を anchor にした365日固定のエジプト民用暦を追加した。 - `calendar()` に閏年 offset を追加し、Alexandria 地点のコプト暦を追加した。 - ロムルス暦・ユリウス暦は、共和政期からユリウス暦採用後の帝政期まで市民生活が不定時法(horae temporariae、日の出・日の入りを基準に昼夜12等分)だったことに合わせ、`.division({ H: 'solar' })` を追加した。標準表示も `Ho mo` に寄せ、H は `hora prima`〜`hora duodecima` / `hora ... noctis`、m のラベルは不定時の第1細分(`pars minuta`)として扱う。夜はローマ軍制の vigiliae(4夜警)という別の数え方もあるが、現行 `H` は24スロット(夜12+昼12)の temporal-hour 表示に統一する。`solor()` の天文計算自体は `is_solor` に関わらず同一で、変化するのは表示規約のみ。 - 極域での不定時法は construction 時点で例外化した。`.init()` 冒頭で `this.dic.is_solor && 66.5 <= Math.abs(this.dic.geo[0])` を検査する。66.5度は「これより先は確実に不可能」という下限であり、手前でも夏至/冬至付近の退化ケースは残る。 - バビロニア暦(カスプ/ベール)・オスマン帝国の時刻制度(季節時法/アラトゥルカ)を追加した。バビロニア暦は平気法と同じ mean モデルの太陰太陽暦を使い、カスプ=不定時法+日没境界、ベール=1日12等分の等時法+固定境界で分けた。オスマン帝国の2暦はユリウス暦の日付構造を流用し、季節時法=不定時法、アラトゥルカ=等時法で分けた。 - `dayBoundary(offsetHours)` は d/N(月内日)構築規則だけに作用する固定オフセットとして実装した。offsetHours は H.length ではなく day 長から換算する。def_zero の hour→day→month→year 連鎖へ直接入れると、月・年の zero 点まで動いてしまい、時刻体系だけが違う対の暦で日番号が大きく食い違うため避けた。 - `.dayStart('sunrise' | 'sunset')` は `SolarEventDayTempoRule` で実際の日の出/日の入を暦日境界にする。`dusk()` は `.dayStart('sunset')` の互換 alias。月・年の開始候補も `StartAlignedTempoRule` で「その後に最初に来る太陽イベント」へ丸め上げ、月初/年初直前の短い区間を前月末/前年末として扱う。これにより、月初の `d.succ()` と `add(..., '1日後')` の意味を一致させた。 - `dayStart()` の d/N では、`CachedTempoRule` に `parent.last_at` を cacheKey として渡し、異なる月親で同じ write_at のキャッシュが混ざらないようにした。 - `find_span_time()` は `month.last_at + dayIndex*msec.day` ではなく `resolve_day_start()` を使うようにした。`dayStart()`/`dayBoundary()` 暦で `add()`/`sub()` が1日早い日付を返す実バグを修正した。`SolarEventDayTempoRule` の `now_idx` は、親境界からの経過ミリ秒ではなく civil day index 差分で求める。日の出が前日より早くなる季節でも `d` が重複しないようにするため。 - 非地球太陰太陽暦の足場として `.observedLunisolar()` を追加し、`JupiterObserved` サンプルを追加した。`lunisolar()` は `principalTermCount` を受け取り、地球型の12中気固定ではなく暦の月数(`M.length`)に合わせて月番号を割り当てられる。明示 opt-in した非地球暦では、年番号も地球の `Date#getUTCFullYear()` ではなく暦自身の太陽年座標から解決する。`observedLunisolar({ solarYear })` で暦固有の年解決関数を渡せる。現状は平均軌道の phase-resolved モデルであり、衛星の出没や摂動まで含む高精度モデルではない。 - タイ暦固有の規則を `thaiOfficialLunisolar()` として追加した。既存の `Calendar.タイ太陰太陽暦`(平均モデル)と `Calendar.タイ太陰太陽暦天文`(観測近似)は互換性のため変更せず、`Calendar.タイ太陰太陽暦公式`を別サンプルとして公開する。規則層は月相・中気から閏月を推測するのではなく、タイ暦の年型判定(通常354日、อธิกวาร 355日、อธิกมาส 384日)と、7月加日・8月重複の civil month table を使う。1903〜2460年は参照実装との検証範囲として回帰テストで固定し、2461年以後は最後の seed/anchor から同じ算術規則を proleptic に継続する。将来の政府公表年表を保証する official table モードではなく、既知のタイ固有規則を計算するモデルである。`thaiLunisolar()` は `year_type`、`is_intercalary_day`、`is_leap`(8/8)を返す。1903年未満は歴史的 seed/anchor の不足により引き続き拒否する。 タイ暦の一般化仕様案は [lunisolar-generalization-proposal.md](lunisolar-generalization-proposal.md) に分離した。天文現象の生成、閏月・閏日の civil policy、年番号、日界、表示を別層にする方針で、今回の公式タイ暦はその最初の policy 実装に位置付ける。 - 教会暦対応の第一段階として `src/phenomena/computus.ts` に Gregorian computus / Julian Paschalion と教会暦上の復活祭満月・復活祭・主要祝祭日の純粋計算を追加し、`src/sample/derived/church.ts` の `churchFeastDates()` / `churchFeastNotes()` で任意の市民暦へ変換する。`system`(Computusの伝統)と`calendarSystem`(CivilDateの表示先)を分離し、Julian系の復活祭をGregorian日付へ表示できる。教会暦上の月は`OrbitalModel`/`SATELLITE`として公開せず、必要なら将来内部の位相adapterを追加する。 - 太陽太陰暦一般化のPhase 1として、`policy-regression-spec.js` に年/月/日/時刻区間の半開境界、階層包含、日/月送り、不定時法の単調性を追加した。Phase 2では `src/phenomena/calendar-policy.ts` に `CalendarYearPolicy`、`LunisolarBoundary`、`HourDivisionPolicy` などの型契約を追加し、公開barrelへ再exportした。まだ既存の計算経路へ接続せず、型と回帰specを先に固定する段階である。 - 太陽太陰暦一般化のPhase 3として、`PeriodicCalendarYearPolicy` を追加し、Gregorianの`[4,100,400]`・Julianの`[4]`を既存の `def_year_table()` へ接続した。`leap_shift` を含む旧周期解釈と同値であることを専用specで固定し、年構造policyだけを実経路へ差し替えた。月表・太陰太陽暦境界・日界・Hour policyの接続は後続Phaseに残している。 - 太陽太陰暦一般化のPhase 4として、`lunisolar_boundaries_around()` が朔望月境界列だけを生成し、`PrincipalTermLunisolarPolicy` が中気から月番号・閏月・年番号を付与する構造へ分離した。既存の`LunisolarDate`の公開形状は維持し、境界の`source_kind`/source phase metadataは内部policy結果に限定している。NAOJ fixtureとTempo/Thai回帰は無修正で通過している。 - 太陽太陰暦一般化のPhase 5として、`ThaiModernLunisolarYearPolicy` を追加し、Thaiの通常年・`อธิกวาร`・`อธิกมาส`の年型と月配置を共有 `CalendarYearPolicy` 契約へ接続した。既存のseed/偏差計算と`ThaiLunisolarDate`の公開結果は維持し、日付resolverの月配置だけをpolicy結果から構築する。Thai固有policyと一般のPrincipalTerm policyは同じ契約層にあるが、月境界生成の方式は混同しない。 - 太陽太陰暦一般化のPhase 6として、legacyの`.division({ H: 'equal' | 'solar' })`を`HourDivisionPolicy`へ正規化し、`to_tempos()`のHour構築を`equal`/`temporal`/`table`の3経路へ分けた。`dayStart()`/`dayBoundary()`は引き続き独立した暦日境界policyであり、Hour policyの`arithmetic`は経過時間と境界stepの意味を保持するための契約として先に公開している。既存の定時法・不定時法の数値挙動と、固定table Hourの`succ()`を回帰specで固定した。 - 太陽太陰暦一般化のPhase 7として、`dayStart()`/`dayBoundary()`を`DayBoundaryPolicy`へ正規化した。`midnight`、`fixed-offset`、`solar-event`を共通型で表し、既存のsolar-event優先順位と逆方向の日付解決を維持する。内部設定は非列挙で保持し、既存の`dic` snapshot形状を変更しない。 - 太陽太陰暦一般化のPhase 8として、assignmentの公開契約を`DayAssignmentPolicy`へ統一した。tithiは必要な月モデルをfactoryへ明示注入し、raw連続index、civil dayへの投影、`repeated`/`skipped` flags、cache semanticsをpolicy内部で扱う。assignmentがday boundary policyとは別軸であることを型契約へ反映した。 - 0系API整理のPhase 13として、旧`AssignmentRule`/`AssignmentContext`/`AssignmentResult`とcalendarを渡すadapterを削除した。policy objectはcalendar clone時にidentityを保証せず、値形状を複製して保持するため、利用側はobject identityではなく`assign()`契約を使う。 - 0系API整理のPhase 14として、Hourの入力型`HourDivisionInput`と、内部に保持する正規化済み`HourDivisionPolicy`を分離した。入力では`arithmetic`を省略できるが、正規化後は必須とし、equalは`elapsed-duration`、temporal/tableは`boundary-step`を明示的に保持する。 - 0系API整理のPhase 15として、PrincipalTerm専用の`LunisolarPhaseBoundary`を追加した。`source_at`/`next_source_at`を必須にし、phaseを必要とする中気探索からoptional guardとnon-null assertionを削除した。sourceなしの一般`LunisolarBoundary`はtable等の候補用に残し、内部データの保証範囲を型で分けた。 - 太陽太陰暦一般化のPhase 16として、`CalendarNotePolicy`契約を追加し、`SolarTermPolicy`(mean/observed)、`ZassetsuPolicy`、`JapaneseFixedDateNotePolicy`、`ReligiousFixedDateNotePolicy`へ季節現象・雑節・節句・宗教固定日noteの解決を分離した。公開結果は`FancyDate.note()`を正本とし、term set・雑節・節句の中間APIは削除した。固定noteの内部制約はTempoと同じ0-based index objectで表し、solar term phaseと固定日note catalogの定義値は`calendar-note-data.ts`へ集約した。表示labelは言語依存データとしてlocale側に残す。 - locale整理として、tokenのfallback labels、span unitのruby、雑節の表示labelを`src/locale/labels.ts`/`jaLocale`へ集約した。`LocaleEntry`に`labels`・`spanUnitRuby`・`seasonalNoteLabels`を持たせ、`FancyDate.locale()`から内部設定へ反映する。教会暦・Thai feastのID labelはfeature固有のためsample derived側に残す。 - 太陽太陰暦一般化のPhase 9として、年ごとの宗教行事をcivil dateへ返す`FeastPolicy`契約を追加し、Computusの教会祝祭日を`ChurchFeastPolicy`へ接続した。Computusの伝統、表示先の市民暦、表示ラベルはそれぞれ計算policy・projection・notationの別責務として残した。 - 太陽太陰暦一般化のPhase 10として、予約していた`HourArithmeticPolicy`を`add()`/`span()`へ接続した。`elapsed-duration`ではH/m/s/Sを固定durationへ集約して時刻へ加算し、`boundary-step`ではtemporal/tableの実境界を辿る。等分Hourはelapsed、temporal/table Hourはboundaryを既定値とし、`Tempo.succ()`/`back()`自体は各Hour ruleの境界stepとして維持した。 - 太陽太陰暦一般化のPhase 11として、`ThaiBuddhistFeastPolicy`を追加した。マーカブーチャー(3/15)、ヴィサーカブーチャー(6/15)、アーサーンハブーチャー(8/15)、入安居(8/16)、出安居(11/15)を、閏月年にはアーサーンハブーチャーと入安居だけ後半の8/8へ移し、既存のThai近代規則から現地civil dateへ投影する。これは政府休日・振替休日の年表ではなく、宗教上の月日を返す計算policyである。 - 太陽太陰暦一般化のPhase 12として、Thai feastのsample adapterを追加した。`thaiBuddhistFeastDates()`は既定の日本語labelまたは呼び出し側のlabel override付きで日付を返し、`thaiBuddhistFeastNotes()`は公式Thai暦の日境界から該当行事を逆引きする。core policyの計算結果と、sampleの表示/notes責務を分離している。 - 0系API整理として、policyへ委譲するだけだった`church_feasts()`と、実装経路へ接続されなかった暫定`LunisolarPolicy`/`LunisolarYearContext`/`LunisolarLeapDay`を削除した。互換wrapperを残さず、現在実際に使われている`ChurchFeastPolicy`と`PrincipalTermLunisolarPolicy`を公開面の正本とする。 Sources: [Unequal hours](https://en.wikipedia.org/wiki/Unequal_hours) / [不定時法の説明 - THE SEIKO MUSEUM GINZA](https://museum.seiko.co.jp/knowledge/relation_16/) / [和時計 - Wikipedia](https://ja.wikipedia.org/wiki/%E5%92%8C%E6%99%82%E8%A8%88) / [Danna (Mesopotamian) - Wikipedia]() / [Hour - Wikipedia (Babylonian hours)](https://en.wikipedia.org/wiki/Babylonian_hours) / [Equinoctial hours - Wikipedia](https://en.wikipedia.org/wiki/Equinoctial_hours) / [Babylonian calendar - Wikipedia](https://en.wikipedia.org/wiki/Babylonian_calendar) / [Witnesses of time: How Ottoman Empire measured time - Türkiye Today](https://www.turkiyetoday.com/culture/witnesses-of-time-how-the-ottoman-empire-measured-regulated-and-lived-time-3212480) / [Our Time: On the Durability of the Alaturka Hour System in the Late Ottoman Empire](https://www.academia.edu/10068187/_Our_Time_On_the_Durability_of_the_Alaturka_Hour_System_in_the_Late_Ottoman_Empire_International_Journal_of_Turkish_Studies_16_2010_47_69) ### バグ修正・仕様整理 - 干支のサンプル初期値の誤りを修正した。定気法の年干支(a)起点値が1968年の「戊申」になっていたが、平気法と同じ起点年(皇紀2629年=西暦1969年)の正しい年干支「己酉」に直した。 - 日干支(A)起点値のズレを調査し、最終的な真因は `def_zero()` の日次巡回トークン二重シフトだった。`dC60/dC12/dC10/E/dC28` などの日次 cycle のゼロ点を、d 自身のシフト分を含まない `hour` 起点で計算するよう修正した。これにより Julian の anchor「1582年10月5日は金曜日」という史実や、同一UTC瞬間を指す複数暦の日干支が一致することを確認した。 - 定気法(観測太陰太陽暦モデル)の `.parse()` 年逆算バグを修正した。観測モデルでは `ObservedLunisolarYearRule` がグレゴリオ暦年を使うため、平気法の連続 index 前提の `zero + y*msec.year` では約660年ズレていた。元号開始 msec を起点に `lunisolar()` 探索で目標年へ収束させる分岐を追加した。 - 平均太陰太陽暦の `.parse()` 月逆算バグを修正した。従来は `parse_by()` が中気付近の単発シードから月初を決めていたため、閏月直後の非閏月を直前の閏月へ戻したり、年末閏月・師走を前年へ戻したりしていた。従来シードを近傍探索の中心として残しつつ、前後1年ぶんの候補月から元号・年番号・月番号・閏フラグが一致する月を選ぶようにした。平気法1980〜2025年の月初既定表示 round-trip と、バビロニア暦カスプ/ベールの月初表示同一性で検証済み。 - `export *` の tslib バンドル非互換性を修正した。`src/index.ts`・`src/sample.ts`・`src/sample/index.ts`・`src/naoj.ts`・`src/naoj/index.ts`・`src/nasa/index.ts`・`src/fancy-date.ts` の計7箇所で `export *` を明示的な named export に置き換え、esbuild bundle で `Calendar`/`Tempo`/`to_msec` が undefined にならないことを確認した。 - 元号1年目の `format()`→`parse()` round-trip ズレを修正した。`find_lunisolar_parse_month()` が月の開始時点だけで元号/年号一致を判定していたため、改元が月の途中で起きる年(大正/昭和/平成/令和元年)で約1年ずれていた。月の開始時点と終了直前の両方で一致確認するようにした。 - 元号あり暦の anchor 文字列 round-trip を修正した。`calendar()` の anchor で `u` 年(era 調整前の通年)を使う場合、`parse()` 側で `u` トークンが失われ、かつ元号解決が `G=0` 固定になっていたため全く別の年を指していた。`u` を `main_tokens` へ移動、`u` 指定時に `calc.eras` から実元号を特定、`parse_by()` の平均太陰太陽暦経路で `to_tempos()` の `u` 規則と同じ `EraAdjustedTempoRule` を使うことで、平気法・定気法の anchor 文字列が `format(parse(...))` で元に戻るようになった。 - `scripts/perf.js` の `ga.solar_terms(base)` を `ga.note(base)` に置き換えた。`FancyDate.solar_terms()` は削除され `note()` が公開APIとなったため。 ## 調査メモ・教訓 - 干支調査では、暦システム自身の自己無矛盾チェック(「anchor を format() したら anchor の値に戻るか」)だけでは不十分だった。2020年1月22日=甲子、皇紀2629年=西暦1969年で己酉など、独立に検証可能な実世界の事実と複数日付で突き合わせる方が、真の誤りと暦法差を分けやすい。 - 値のズレが複数の実測点で一定の場合、探索アルゴリズムより初期値・zero 点のような加法的較正定数が疑わしい。 - 既存テストが「既知の別課題として対象外」としている箇所を安易に一緒に直すと、無関係な不具合を自分の変更のせいだと誤診しやすい。独立に検証してから結論を出す。 - 不定時法を極域へ正しく拡張する自然な答えは見当たらない。極域先住民の時間認識も「昼をN等分する」発想とは別系統で、南極観測基地も補給元国の標準時に合わせる例が多い。 - 先住民の極域暦の計算機的表現では、イヌイットの13朔望月暦は「極夜明け最初の日の出」を年始にする候補がある。これは `has_sunrise` の false→true 遷移探索に近い。既存 `find()` は format 済み文字列条件しか扱えないため、`has_sunrise`/`is_up_all_day` のような生の太陽・月イベント判定を条件にできる探索 API が必要になりそう。 - サーミの8季節暦は、生態/感覚に基づく季節境界であり、既存の年/月/日階層にそのまま乗せるのは無理がある。太陽黄経ベースの8分割で近似するなら、文化的運用の単純化であると明記する必要がある。 Sources: [Sámi Eight-Season Calendar](https://www.outlooktraveller.com/experiences/in-the-arctic-time-moves-differently-inside-the-s%C3%A1mi-eight-season-calendar) / [Inuit astronomy](https://en.wikipedia.org/wiki/Inuit_astronomy) / [Time in Antarctica](https://grokipedia.com/page/Time_in_Antarctica)