wareki 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8be029e4138ca6a683ff52bd0e4070db0bb2e025a9e1024b3a0ad0cd60ca34fa
4
- data.tar.gz: e8b300fd9f65529284acdb43e7dfbcd91c3a2e92f07824bcc3500fa78199c2dd
3
+ metadata.gz: 23992b6b4cd96544832ea1db1e843279eddabc6d205d2a7af87c30e29079fc7b
4
+ data.tar.gz: 706f0a880da6c9a05a3fa013450aa04dbd15993d5144df7aeef92d9f49b19f4e
5
5
  SHA512:
6
- metadata.gz: ef1021d0c68056c285c2095c83b66adeb8ce25ed4d7530065f9e06e41ed3129251faf094bb599a4cc5c68506586a996b87ecb67c8d72f59da8c9328c2914c876
7
- data.tar.gz: d98a3ed03d156f6a3a04931ec62ad4cce55dc008ffbd3c4529a6a41c0cd2b6e5279fce42246359177072568b22527da75dba637cf9050ff10883075934f33899
6
+ metadata.gz: d03b898c5bd3efc4087d9a9b6cfdb54526b30c7088e2a051b3bc31ae9c3994acca5607a8920ddd44f6acc1b9331ac4e62a6a82943689e3e41beb5633f165a5bc
7
+ data.tar.gz: 77d82b574ff6fee3fee4c19f27faf532ac040b2fb933b9c33d86ffd1fd6deb32a6aa9c5a2c08eb4ee039594e60032c97a2a0e95d26a9489b2790d7290480073c
data/ChangeLog CHANGED
@@ -1,3 +1,57 @@
1
+ 2026-07-13 Tatsuki Sugiura <sugi@nemui.org>
2
+
3
+ * Version: 2.0.0
4
+ * Time.parse now understands Japanese kansuji/zenkaku time notations
5
+ (十二時三十四分五十六秒, 午後三時半, 正午, ...) via the patched
6
+ Date._parse; Date.parse no longer misreads bare 時分秒 strings
7
+ * Add %JT* time format directives (%JTf/%JTF/%JTH(k)/%JTM(k)/%JTS(k));
8
+ Time#strftime now supports all %J directives and DateTime#strftime
9
+ expands %JT too; add Time#to_wareki_date
10
+ * Strict date validation: nonexistent dates now raise
11
+ Wareki::InvalidDate (< ArgumentError) (month/day out of range,
12
+ nonexistent leap month, and the Meiji 5 December calendar-switch
13
+ gap for all era notations); patched Date.parse re-raises it
14
+ instead of falling back to stdlib misinterpretation
15
+ * README: document that conversion from Date/jd prefers northern court
16
+ eras during the Nanboku-cho period (previous statement was wrong;
17
+ behavior unchanged from 1.1.0)
18
+ * Update kyuureki calendar data (regenerated from
19
+ manakai/data-locale); corrects month boundaries of some
20
+ historical years
21
+ * Wareki::Date#- returns day count for date-like operands;
22
+ #+ raises TypeError for non-numeric operands (stdlib parity)
23
+ * Support date calculation with ActiveSupport::Duration
24
+ (was NotImplementedError)
25
+ * Add Wareki::Date#hash, #<=>, Comparable and #succ
26
+ * Invalidate cached jd on attribute writes; writers keep
27
+ year/era_year consistent
28
+ * Freeze era definition structs to prevent accidental mutation
29
+ of shared state
30
+ * Fix imperial_year= setter (sign error)
31
+ * Parse apostrophe leap-month markers so %Jf/%Jm output round-trips
32
+ * Fix misokka (晦日) parsing for 皇紀/西暦/紀元前/era-less dates
33
+ * DateTime#strftime now supports %J directives
34
+ * Escaped %%J or invalid %J sequences no longer trigger conversion;
35
+ Wareki::Date#strftime honors %% escapes too
36
+ * Fast-path non-wareki strings in patched Date.parse/_parse
37
+ * Wareki.parse_to_date falls back on UnsupportedDateRange too
38
+ * Fix deprecated Utils.i_to_kan (was calling undefined method)
39
+ * gemspec: SPDX license identifier (BSD-2-Clause)
40
+
41
+ [ by masa.kunikata <masa.kunikata@gmail.com> ]
42
+ * Add zero padding format feature
43
+ * Use zero padded format on default
44
+
45
+ 2019-12-24 Tatsuki Sugiura <sugi@nemui.org>
46
+
47
+ * Version: 1.1.0
48
+
49
+ [ by Akinori MUSHA <knu@idaemons.org> ]
50
+ * Partially traditional kanji variants support
51
+ * Compatibility fix: Date.parse with no arguement
52
+ * Provides Date._parse overrides (currently no kansuji
53
+ support for time fields at present)
54
+
1
55
  2019-04-15 Tatsuki Sugiura <sugi@nemui.org>
2
56
 
3
57
  * Verion: 1.0.0 🎉, compat with 0.x mostly.
data/README.md CHANGED
@@ -1,9 +1,10 @@
1
1
  # wareki - ruby 和暦ライブラリ
2
2
 
3
3
  [<img src="https://badge.fury.io/rb/wareki.svg" alt="Gem Version" />](https://badge.fury.io/rb/wareki)
4
- [<img src="https://travis-ci.org/sugi/wareki.svg?branch=master" alt="Build Status" />](https://travis-ci.org/sugi/wareki)
5
- [<img src="https://coveralls.io/repos/sugi/wareki/badge.svg?branch=master&service=github" alt="Coverage Status" />](https://coveralls.io/github/sugi/wareki?branch=master)
4
+ [<img src="https://github.com/sugi/wareki/actions/workflows/ci.yml/badge.svg" alt="Build Status" />](https://github.com/sugi/wareki/actions/workflows/ci.yml)
5
+ [<img src="https://coveralls.io/repos/github/sugi/wareki/badge.svg?branch=master" alt="Coverage Status" />](https://coveralls.io/github/sugi/wareki?branch=master)
6
6
  [<img src="https://api.codeclimate.com/v1/badges/c9209422700b526d2b45/maintainability" />](https://codeclimate.com/github/sugi/wareki/maintainability)
7
+ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/a5e0b6022b6d485b86a195be7a392da5)](https://app.codacy.com/gh/sugi/wareki/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
7
8
 
8
9
  ## 概要
9
10
 
@@ -19,10 +20,15 @@
19
20
  * 閏月サポート
20
21
  * 月の別名のサポート (如月、弥生、師走など)
21
22
  * 元年、正月、朔日、晦日、廿一日、卅日 などの特殊な表記の日付サポート
23
+ * 時刻の漢数字・全角数字表記のサポート (十二時三十四分五十六秒、午後三時半、正午 など)
22
24
  * Date クラスの拡張
23
25
  * 標準の strftime に和暦へのフォーマット文字列を追加
24
26
  * to_wareki_date を追加
25
27
  * Date::JAPAN (明治改暦日)の追加
28
+ * Time / DateTime クラスの拡張
29
+ * strftime に時刻の全角・漢数字フォーマット文字列 (%JT 系) を追加
30
+ * Time.parse で和暦・漢数字時刻の文字列を解釈 (要 `require 'time'`)
31
+ * Time#to_wareki_date を追加
26
32
 
27
33
  ## インストール
28
34
 
@@ -70,6 +76,20 @@ Wareki::Date.parse("正嘉元年 うるう3月 12日") # => Wareki::Date イ
70
76
  Wareki::Date.new("明治", 8, 2, 1).to_date # => 標準 Date インスタンス 1875-02-01
71
77
  ```
72
78
 
79
+ ### 時刻のパースとフォーマット
80
+
81
+ 時刻の漢数字・全角数字表記も透過的に扱えます。時刻には暦のようなマッピングは
82
+ 存在しないため、単純な数字変換として処理されます。
83
+
84
+ ```ruby
85
+ require 'time'
86
+ t = Time.parse("平成元年五月四日 午後三時三十四分五十六秒")
87
+ # => 1989-05-04 15:34:56
88
+ t.strftime("%JF %JTF") # => "平成元年五月四日 十五時三十四分五十六秒"
89
+ t.strftime("%JTf") # => "15時34分56秒"
90
+ t.strftime("%JTHk時%JTMk分") # => "十五時三十四分"
91
+ ```
92
+
73
93
  ### 和暦・旧暦へのフォーマット
74
94
 
75
95
  日本では明治5年まで、グレゴリオ暦でもユリウス歴でもない旧暦が使われていました。これもフォーマット文字列経由で透過的に扱えます。
@@ -119,6 +139,8 @@ strftime が拡張されるので、 `config/locale/ja.yml` にそのままフ
119
139
 
120
140
  ## 追加フォーマット文字列一覧
121
141
 
142
+ 通常の strftime のフォーマット文字列に **加えて**、 以下が使用できます。これ以外のフォーマット文字列に関しては、そのまま strftime へ引き渡され、プラットフォーム依存で解決されます。
143
+
122
144
  * %Jf: "%Je%Jg年%Js%Jl月%Jd日" の略 (例: 平成23年3月12日)
123
145
  * %JF: "%Je%JGK年%JLk%JSk月%JDk日" の略 (例: 平成二十三年三月十二日)
124
146
  * %Jy: "%Je%Jg" の略 (元号+半角数字年)
@@ -151,6 +173,18 @@ strftime が拡張されるので、 `config/locale/ja.yml` にそのままフ
151
173
  * %JDk: 和暦日の漢数字
152
174
  * %JDK: 和暦日の漢数字の特殊記法 (朔、晦)
153
175
 
176
+ 以下の時刻フォーマット文字列は Time / DateTime の strftime でのみ使用できます
177
+ (Date は時刻情報を持たないため展開されません)。
178
+
179
+ * %JTf: "%JTH時%JTM分%JTS秒" 相当の半角ゼロ埋め複合表記 (例: 15時04分05秒)
180
+ * %JTF: 漢数字の複合表記 (例: 十五時四分五秒)
181
+ * %JTH: 時の全角数字
182
+ * %JTHk: 時の漢数字
183
+ * %JTM: 分の全角数字
184
+ * %JTMk: 分の漢数字
185
+ * %JTS: 秒の全角数字
186
+ * %JTSk: 秒の漢数字
187
+
154
188
  ## 仕様、限界、制限など
155
189
 
156
190
  * 作者は暦の専門家ではまったくありません。ツッコミお待ちしています。
@@ -159,17 +193,24 @@ strftime が拡張されるので、 `config/locale/ja.yml` にそのままフ
159
193
  * 日本暦日原典の対照表が、どの暦法を元にしているのか分かっていません。単にデータだけを利用しています。
160
194
  * 内部的には全てユリウス日を経由して変換しています。
161
195
  * パース時には元号の存在しない年(例: 霊亀百年)を受け入れます。しかし現状、Date に変換した場合この情報は捨てられ、パース時の元号を復元することはできません。(Wareki::Date を直接扱えば可能です)
162
- * 改暦による存在しない日(明治5年12月3日〜31)は基本的に例外(ArgumentError)を上げます。が、現状このチェックは完全ではありません。
163
- * 北朝の元号も解釈できます。しかし現在の所、北朝の元号で文字列にフォーマットすることはできません。
196
+ * 存在しない日付(月・日の範囲超過、存在しない閏月、改暦により存在しない明治5年12月3日〜31日など)は例外(Wareki::InvalidDate, ArgumentError のサブクラス)を上げます。
197
+ * 南朝・北朝どちらの元号も解釈できます。Date やユリウス日からの変換では、南北朝期は通常の慣例に従い北朝の元号を優先します(元弘・延元など、北朝側の改元前で北朝に固有の元号がない期間は南朝の元号になります)。
198
+ * 元号の開始日より前の日付表記(例: 令和元年1月1日)は受理し、その元号の元年として解釈します。逆変換では実際の元号(平成31年1月1日)になります。
164
199
  * 10月の別名は「神無月」しかサポートしていません
165
200
  * 将来の日付に関しては、現在の元号がずっと継続しているとみなします
166
201
  * 日本でユリウス暦は使われていないので、Date::JAPAN は単にグレゴリオ暦への改暦日の目安、と言うだけです。Date.new で使う意味はほぼありません。
202
+ * 時刻の解釈・出力は単純な数字変換のみで、十二時辰などの伝統的時刻制度との対応はありません
203
+ * 時刻パース時、「午前」は無変換、「午後」は12時未満の時にのみ+12します (午後十二時 → 12:00)
204
+ * 「三時間」「13時代」のような「数字+時」を含む複合語も時刻として解釈されることがあります (このため、従来 Date.parse が数字を日として解釈して成功していた文字列がエラーになる場合があります)
205
+ * 範囲外の時刻 (二十五時など) は ASCII 表記 (25:00) と同様に扱われ、Time.parse では通常 ArgumentError になります
206
+ * 和暦・漢数字時刻のパース対応は Date.parse / Date._parse / Time.parse のみです。DateTime.parse は Ruby 内部のパーサを直接呼ぶため対象外です
167
207
 
168
208
  ## 参照元データ
169
209
 
170
210
  作成には以下のデータを参照しました。
171
211
 
172
- * 旧暦: [suikawiki - 旧暦](http://wiki.suikawiki.org/n/%E6%97%A7%E6%9A%A6#section-%E5%AF%BE%E7%85%A7%E8%A1%A8%E3%81%A8%E5%A4%89%E6%8F%9B%E3%83%84%E3%83%BC%E3%83%AB) にある「日本暦日原典」第4版準拠の先発グレゴリオ暦対照表
212
+ * Wakaba 氏による https://github.com/manakai/data-locale のデータを利用しています
213
+ * 旧暦: [suikawiki - 旧暦](http://wiki.suikawiki.org/n/%E6%97%A7%E6%9A%A6#section-%E5%AF%BE%E7%85%A7%E8%A1%A8%E3%81%A8%E5%A4%89%E6%8F%9B%E3%83%84%E3%83%BC%E3%83%AB) にある「日本暦日原典」第4版準拠の先発グレゴリオ暦対照表
173
214
  * 元号: [Wikipedia - 元号一覧_(日本)](https://ja.wikipedia.org/wiki/%E5%85%83%E5%8F%B7%E4%B8%80%E8%A6%A7_%28%E6%97%A5%E6%9C%AC%29) のユリウス歴とグレゴリオ暦表記の物に基づいています
174
215
 
175
216
  ## ライセンス
@@ -1,15 +1,14 @@
1
- # frozen_string_literal: true
2
1
  module Wareki
3
2
  Year = Struct.new(:year, :start, :end, :leap_month, :month_starts, :month_days)
4
3
  YEAR_DEFS = [
5
4
  Year.new(445, 1883618, 1884001, 5, [1883618, 1883648, 1883677, 1883707, 1883736, 1883766, 1883795, 1883825, 1883854, 1883884, 1883913, 1883943, 1883972], [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30]),
6
5
  Year.new(446, 1884002, 1884355, nil, [1884002, 1884032, 1884061, 1884091, 1884120, 1884150, 1884179, 1884209, 1884238, 1884268, 1884297, 1884327], [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29]),
7
- Year.new(447, 1884356, 1884710, nil, [1884356, 1884386, 1884415, 1884445, 1884475, 1884504, 1884534, 1884563, 1884593, 1884622, 1884652, 1884681], [30, 29, 30, 30, 29, 30, 29, 30, 29, 30, 29, 30]),
8
- Year.new(448, 1884711, 1885094, 2, [1884711, 1884740, 1884770, 1884799, 1884829, 1884858, 1884888, 1884917, 1884947, 1884977, 1885006, 1885036, 1885065], [29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30]),
6
+ Year.new(447, 1884356, 1884710, nil, [1884356, 1884386, 1884415, 1884445, 1884474, 1884504, 1884534, 1884563, 1884593, 1884622, 1884652, 1884681], [30, 29, 30, 29, 30, 30, 29, 30, 29, 30, 29, 30]),
7
+ Year.new(448, 1884711, 1885094, 2, [1884711, 1884740, 1884770, 1884799, 1884829, 1884858, 1884888, 1884917, 1884947, 1884976, 1885006, 1885036, 1885065], [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30]),
9
8
  Year.new(449, 1885095, 1885448, nil, [1885095, 1885124, 1885154, 1885183, 1885213, 1885242, 1885272, 1885301, 1885331, 1885360, 1885390, 1885419], [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30]),
10
9
  Year.new(450, 1885449, 1885832, 10, [1885449, 1885479, 1885508, 1885538, 1885567, 1885597, 1885626, 1885656, 1885685, 1885715, 1885744, 1885774, 1885803], [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30]),
11
- Year.new(451, 1885833, 1886186, nil, [1885833, 1885862, 1885892, 1885922, 1885951, 1885981, 1886010, 1886040, 1886069, 1886099, 1886128, 1886158], [29, 30, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29]),
12
- Year.new(452, 1886187, 1886541, nil, [1886187, 1886217, 1886246, 1886276, 1886305, 1886335, 1886364, 1886394, 1886424, 1886453, 1886483, 1886512], [30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30]),
10
+ Year.new(451, 1885833, 1886186, nil, [1885833, 1885862, 1885892, 1885921, 1885951, 1885981, 1886010, 1886040, 1886069, 1886099, 1886128, 1886158], [29, 30, 29, 30, 30, 29, 30, 29, 30, 29, 30, 29]),
11
+ Year.new(452, 1886187, 1886541, nil, [1886187, 1886217, 1886246, 1886276, 1886305, 1886335, 1886364, 1886394, 1886423, 1886453, 1886483, 1886512], [30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30]),
13
12
  Year.new(453, 1886542, 1886925, 6, [1886542, 1886571, 1886601, 1886630, 1886660, 1886689, 1886719, 1886748, 1886778, 1886807, 1886837, 1886866, 1886896], [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30]),
14
13
  Year.new(454, 1886926, 1887279, nil, [1886926, 1886955, 1886985, 1887014, 1887044, 1887073, 1887103, 1887132, 1887162, 1887191, 1887221, 1887250], [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30]),
15
14
  Year.new(455, 1887280, 1887633, nil, [1887280, 1887309, 1887339, 1887369, 1887398, 1887428, 1887457, 1887487, 1887516, 1887546, 1887575, 1887605], [29, 30, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29]),
@@ -255,7 +254,7 @@ module Wareki
255
254
  Year.new(695, 1974927, 1975310, 2, [1974927, 1974956, 1974986, 1975015, 1975045, 1975074, 1975104, 1975133, 1975163, 1975192, 1975222, 1975252, 1975281], [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30]),
256
255
  Year.new(696, 1975311, 1975664, nil, [1975311, 1975340, 1975370, 1975399, 1975429, 1975458, 1975488, 1975517, 1975547, 1975576, 1975606, 1975636], [29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 30, 29]),
257
256
  Year.new(697, 1975665, 1976048, 12, [1975665, 1975694, 1975724, 1975753, 1975783, 1975813, 1975842, 1975871, 1975901, 1975931, 1975960, 1975990, 1976020], [29, 30, 29, 30, 30, 29, 29, 30, 30, 29, 30, 30, 29]),
258
- Year.new(698, 1976049, 1976403, nil, [1976049, 1976079, 1976108, 1976137, 1976167, 1976196, 1976226, 1976255, 1976285, 1976314, 1976344, 1976374], [30, 29, 29, 30, 29, 30, 29, 30, 29, 30, 30, 30]),
257
+ Year.new(698, 1976049, 1976403, nil, [1976049, 1976079, 1976108, 1976137, 1976167, 1976196, 1976226, 1976255, 1976285, 1976315, 1976344, 1976374], [30, 29, 29, 30, 29, 30, 29, 30, 30, 29, 30, 30]),
259
258
  Year.new(699, 1976404, 1976757, nil, [1976404, 1976433, 1976463, 1976492, 1976521, 1976551, 1976580, 1976609, 1976639, 1976669, 1976698, 1976728], [29, 30, 29, 29, 30, 29, 29, 30, 30, 29, 30, 30]),
260
259
  Year.new(700, 1976758, 1977141, 7, [1976758, 1976788, 1976817, 1976847, 1976876, 1976905, 1976935, 1976964, 1976993, 1977023, 1977052, 1977082, 1977112], [30, 29, 30, 29, 29, 30, 29, 29, 30, 29, 30, 30, 30]),
261
260
  Year.new(701, 1977142, 1977495, nil, [1977142, 1977171, 1977201, 1977231, 1977260, 1977289, 1977319, 1977348, 1977377, 1977407, 1977436, 1977466], [29, 30, 30, 29, 29, 30, 29, 29, 30, 29, 30, 30]),
@@ -300,7 +299,7 @@ module Wareki
300
299
  Year.new(740, 1991375, 1991729, nil, [1991375, 1991405, 1991434, 1991464, 1991493, 1991523, 1991552, 1991582, 1991612, 1991641, 1991671, 1991700], [30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30]),
301
300
  Year.new(741, 1991730, 1992113, 3, [1991730, 1991759, 1991789, 1991818, 1991847, 1991877, 1991906, 1991936, 1991966, 1991995, 1992025, 1992055, 1992084], [29, 30, 29, 29, 30, 29, 30, 30, 29, 30, 30, 29, 30]),
302
301
  Year.new(742, 1992114, 1992467, nil, [1992114, 1992143, 1992173, 1992202, 1992231, 1992261, 1992290, 1992320, 1992349, 1992379, 1992409, 1992439], [29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30, 29]),
303
- Year.new(743, 1992468, 1992822, nil, [1992468, 1992498, 1992527, 1992557, 1992586, 1992615, 1992645, 1992674, 1992704, 1992734, 1992763, 1992793], [30, 29, 30, 29, 29, 30, 29, 30, 30, 29, 30, 30]),
302
+ Year.new(743, 1992468, 1992822, nil, [1992468, 1992498, 1992527, 1992557, 1992586, 1992615, 1992645, 1992674, 1992704, 1992733, 1992763, 1992793], [30, 29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30]),
304
303
  Year.new(744, 1992852, 1993205, 1, [1992823, 1992852, 1992882, 1992911, 1992941, 1992970, 1992999, 1993029, 1993058, 1993087, 1993117, 1993147, 1993176], [29, 30, 29, 30, 29, 29, 30, 29, 29, 30, 30, 29, 30]),
305
304
  Year.new(745, 1993206, 1993559, nil, [1993206, 1993236, 1993266, 1993295, 1993325, 1993354, 1993383, 1993413, 1993442, 1993471, 1993501, 1993531], [30, 30, 29, 30, 29, 29, 30, 29, 29, 30, 30, 29]),
306
305
  Year.new(746, 1993560, 1993943, 9, [1993560, 1993590, 1993620, 1993649, 1993679, 1993709, 1993738, 1993767, 1993797, 1993826, 1993856, 1993885, 1993915], [30, 30, 29, 30, 30, 29, 29, 30, 29, 30, 29, 30, 29]),
@@ -351,7 +350,7 @@ module Wareki
351
350
  Year.new(791, 2010009, 2010362, nil, [2010009, 2010038, 2010068, 2010098, 2010128, 2010157, 2010187, 2010216, 2010245, 2010275, 2010304, 2010334], [29, 30, 30, 30, 29, 30, 29, 29, 30, 29, 30, 29]),
352
351
  Year.new(792, 2010363, 2010746, 11, [2010363, 2010393, 2010423, 2010452, 2010481, 2010511, 2010541, 2010570, 2010600, 2010630, 2010659, 2010689, 2010718], [30, 30, 29, 29, 30, 30, 29, 30, 30, 29, 30, 29, 29]),
353
352
  Year.new(793, 2010747, 2011101, nil, [2010747, 2010777, 2010806, 2010836, 2010865, 2010895, 2010924, 2010954, 2010984, 2011013, 2011043, 2011072], [30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29, 30]),
354
- Year.new(794, 2011102, 2011456, nil, [2011102, 2011131, 2011161, 2011190, 2011219, 2011249, 2011278, 2011308, 2011338, 2011367, 2011397, 2011427], [29, 30, 29, 29, 30, 29, 30, 30, 29, 30, 30, 30]),
353
+ Year.new(794, 2011102, 2011456, nil, [2011102, 2011131, 2011161, 2011190, 2011220, 2011249, 2011278, 2011308, 2011338, 2011367, 2011397, 2011427], [29, 30, 29, 30, 29, 29, 30, 30, 29, 30, 30, 30]),
355
354
  Year.new(795, 2011457, 2011840, 7, [2011457, 2011486, 2011515, 2011545, 2011574, 2011603, 2011633, 2011662, 2011692, 2011722, 2011751, 2011781, 2011811], [29, 29, 30, 29, 29, 30, 29, 30, 30, 29, 30, 30, 30]),
356
355
  Year.new(796, 2011841, 2012194, nil, [2011841, 2011870, 2011899, 2011929, 2011958, 2011987, 2012017, 2012046, 2012076, 2012105, 2012135, 2012165], [29, 29, 30, 29, 29, 30, 29, 30, 29, 30, 30, 30]),
357
356
  Year.new(797, 2012195, 2012548, nil, [2012195, 2012224, 2012254, 2012283, 2012313, 2012342, 2012371, 2012401, 2012430, 2012460, 2012489, 2012519], [29, 30, 29, 30, 29, 29, 30, 29, 30, 29, 30, 30]),
@@ -377,7 +376,7 @@ module Wareki
377
376
  Year.new(817, 2019488, 2019871, 4, [2019488, 2019518, 2019547, 2019577, 2019607, 2019636, 2019666, 2019695, 2019725, 2019754, 2019784, 2019813, 2019842], [30, 29, 30, 30, 29, 30, 29, 30, 29, 30, 29, 29, 30]),
378
377
  Year.new(818, 2019872, 2020226, nil, [2019872, 2019902, 2019931, 2019961, 2019991, 2020020, 2020050, 2020079, 2020109, 2020138, 2020168, 2020197], [30, 29, 30, 30, 29, 30, 29, 30, 29, 30, 29, 30]),
379
378
  Year.new(819, 2020227, 2020580, nil, [2020227, 2020256, 2020286, 2020315, 2020345, 2020374, 2020404, 2020433, 2020463, 2020493, 2020522, 2020552], [29, 30, 29, 30, 29, 30, 29, 30, 30, 29, 30, 29]),
380
- Year.new(820, 2020611, 2020964, 1, [2020581, 2020611, 2020641, 2020670, 2020699, 2020728, 2020758, 2020788, 2020817, 2020847, 2020877, 2020906, 2020936], [30, 30, 29, 29, 29, 30, 30, 29, 30, 30, 29, 30, 29]),
379
+ Year.new(820, 2020611, 2020964, 1, [2020581, 2020611, 2020640, 2020670, 2020699, 2020728, 2020758, 2020788, 2020817, 2020847, 2020877, 2020906, 2020936], [30, 29, 30, 29, 29, 30, 30, 29, 30, 30, 29, 30, 29]),
381
380
  Year.new(821, 2020965, 2021319, nil, [2020965, 2020995, 2021024, 2021053, 2021083, 2021112, 2021142, 2021171, 2021201, 2021231, 2021260, 2021290], [30, 29, 29, 30, 29, 30, 29, 30, 30, 29, 30, 30]),
382
381
  Year.new(822, 2021320, 2021703, 9, [2021320, 2021350, 2021379, 2021408, 2021437, 2021467, 2021496, 2021525, 2021555, 2021585, 2021614, 2021644, 2021674], [30, 29, 29, 29, 30, 29, 29, 30, 30, 29, 30, 30, 30]),
383
382
  Year.new(823, 2021704, 2022057, nil, [2021704, 2021733, 2021763, 2021792, 2021821, 2021851, 2021880, 2021909, 2021939, 2021969, 2021998, 2022028], [29, 30, 29, 29, 30, 29, 29, 30, 30, 29, 30, 30]),
data/lib/wareki/common.rb CHANGED
@@ -9,38 +9,87 @@ module Wareki
9
9
  GREGORIAN_START_YEAR = 1873
10
10
  IMPERIAL_START = 1_480_041 # Date.new(-660, 2, 11, Date::GREGORIAN).jd
11
11
  IMPERIAL_START_YEAR = -660
12
+ WESTERN_ERA_NAMES = ['', '西暦', '紀元前'].freeze
13
+ IMPERIAL_ERA_NAMES = %w(皇紀 神武天皇即位紀元).freeze
12
14
  DATE_INFINITY = ::Date.new(280_000_000, 12, 31) # Use 280000000 for jruby limitation...
13
15
  YEAR_BY_NUM = Hash[*YEAR_DEFS.map { |y| [y.year, y] }.flatten].freeze
14
- ERA_BY_NAME = Hash[*(ERA_NORTH_DEFS + ERA_DEFS).map { |g| [g.name, g] }.flatten]
15
- ERA_BY_NAME['皇紀'] = ERA_BY_NAME['神武天皇即位紀元'] = Era.new('皇紀', -660, 1_480_041, DATE_INFINITY.jd)
16
- ERA_BY_NAME['西暦'] = ERA_BY_NAME[''] = Era.new('西暦', 1, 1_721_424, DATE_INFINITY.jd)
17
- ERA_BY_NAME.keys.each do |era_name|
18
- alt_era_name = era_name.tr('宝霊神応暦祥寿斎観寛徳禄万福禎国亀令', '寳靈神應曆祥壽斉觀寬德祿萬福禎國龜令')
19
- alt_era_name == era_name and next
20
- ERA_BY_NAME[alt_era_name] = ERA_BY_NAME[era_name]
21
- end
22
- {'' => '明治', '㍽' => '大正', '㍼' => '昭和', '㍻' => '平成', '㋿' => '令和'}.each do |short, canon|
23
- ERA_BY_NAME[short] = ERA_BY_NAME[canon]
24
- end
16
+ KANJI_VARIANTS = {
17
+ '' => '',
18
+ '' => '',
19
+ '神' => '神',
20
+ '応' => '',
21
+ '暦' => '曆',
22
+ '祥' => '祥',
23
+ '寿' => '壽',
24
+ '' => '',
25
+ '観' => '觀',
26
+ '寛' => '寬',
27
+ '徳' => '德',
28
+ '禄' => '祿',
29
+ '万' => '萬',
30
+ '福' => '福',
31
+ '禎' => '禎',
32
+ '国' => '國',
33
+ '亀' => '龜',
34
+ '令' => '令',
35
+ }.freeze
36
+ SQUARE_ERAS = {
37
+ '㍾' => '明治',
38
+ '㍽' => '大正',
39
+ '㍼' => '昭和',
40
+ '㍻' => '平成',
41
+ '㋿' => '令和',
42
+ }.freeze
43
+ NORMALIZE_KANJI_VARIANTS_REGEX = Regexp.union(*KANJI_VARIANTS.values)
44
+ NORMALIZE_KANJI_VARIANTS_HASH = KANJI_VARIANTS.each_with_object({}) { |(n, s), h| s.each_char { |c| h[c] = n } }
45
+ NORMALIZE_KANJI_VARIANTS = ->(str) { str.gsub(NORMALIZE_KANJI_VARIANTS_REGEX, NORMALIZE_KANJI_VARIANTS_HASH) }
46
+ ERA_BY_NAME = Hash[*(ERA_NORTH_DEFS + ERA_DEFS).flat_map { |g| [g.name, g] }]
47
+ ERA_BY_NAME['皇紀'] = ERA_BY_NAME['神武天皇即位紀元'] = Era.new('皇紀', -660, 1_480_041, DATE_INFINITY.jd).freeze
48
+ ERA_BY_NAME['西暦'] = ERA_BY_NAME[''] = Era.new('西暦', 1, 1_721_424, DATE_INFINITY.jd).freeze
49
+ ERA_BY_NAME.default_proc = ->(hash, key) { hash.fetch(SQUARE_ERAS[key] || NORMALIZE_KANJI_VARIANTS[key], nil) }
25
50
  ERA_BY_NAME.freeze
26
- NUM_CHARS = '零壱壹弌弐貳貮参參弎肆伍陸漆質柒捌玖〇一二三四五六七八九十拾什卄廿卅丗卌百陌佰皕阡仟千万萬億兆京垓01234567890123456789'.freeze
51
+ ERA_REGEX = Regexp.new(
52
+ Regexp.union(*ERA_BY_NAME.keys, *SQUARE_ERAS.keys).source.gsub(
53
+ Regexp.union(*KANJI_VARIANTS.keys),
54
+ KANJI_VARIANTS.each_with_object({}) { |(canon, variants), h| h[canon] = "[#{canon}#{variants}]" }
55
+ )
56
+ )
57
+ NUM_CHARS = '零壱壹弌弐貳貮参參弎肆伍陸漆質柒捌玖〇一二三四五六七八九十拾什卄廿卅丗卌百陌佰皕阡仟千万萬億兆京垓01234567890123456789'
27
58
  ALT_MONTH_NAME = %w(睦月 如月 弥生 卯月 皐月 水無月 文月 葉月 長月 神無月 霜月 師走).freeze
28
- REGEX = %r{^
29
- (?<era_name>西暦|紀元前|#{ERA_BY_NAME.keys.join('|')})?
30
- (?:(?<year>[元#{NUM_CHARS}]+)年)?
59
+ REGEX = %r{
60
+ (?:(?<era_name>紀元前|#{ERA_REGEX})?
61
+ (?:(?<year>[元#{NUM_CHARS}]+)年))?
31
62
  (?:(?<is_leap>閏|潤|うるう)?
32
- (?:(?<month>[正#{NUM_CHARS}]+) |
63
+ (?:(?<month>[正#{NUM_CHARS}]+)(?<is_leap_post>['’])?月 |
33
64
  (?<alt_month>#{ALT_MONTH_NAME.join('|')})))?
34
65
  (?:(?<day>[元朔晦#{NUM_CHARS}]+)日|元旦)?
35
- $}x.freeze
66
+ }x.freeze
67
+ # REGEX が空でないマッチを返すには 年/月/日/元旦 か、「月」を含まない
68
+ # 月の別名 (弥生・師走) のいずれかが必要。それ以外は素の Date.parse に
69
+ # 直行させて monkey patch のオーバーヘッドを避ける。
70
+ PARSE_QUICK_FILTER = /[年月日]|元旦|弥生|師走/.freeze
71
+ TIME_REGEX = %r{
72
+ (?<noon>正午) |
73
+ (?:(?<ampm>午前|午後)[[:space:]]*)?
74
+ (?<hour>[#{NUM_CHARS}]+)[[:space:]]*時
75
+ (?:[[:space:]]*
76
+ (?:(?<half>半) |
77
+ (?<min>[#{NUM_CHARS}]+)[[:space:]]*分
78
+ (?:[[:space:]]*(?<sec>[#{NUM_CHARS}]+)[[:space:]]*秒)?))?
79
+ }x.freeze
80
+ # TIME_REGEX が非空マッチしうるのは「時」か「正午」を含む文字列のみ
81
+ TIME_PARSE_QUICK_FILTER = /時|正午/.freeze
36
82
 
37
83
  class UnsupportedDateRange < StandardError; end
84
+ class InvalidDate < ArgumentError; end
38
85
 
39
86
  module_function
40
87
 
41
88
  def parse_to_date(str, start = ::Date::ITALY)
42
89
  Date.parse(str).to_date(start)
43
- rescue ArgumentError
90
+ rescue InvalidDate
91
+ raise
92
+ rescue ArgumentError, UnsupportedDateRange
44
93
  ::Date.parse(str, true, start)
45
94
  end
46
95
  end
data/lib/wareki/date.rb CHANGED
@@ -8,32 +8,25 @@ require 'wareki/kansuji'
8
8
  module Wareki
9
9
  # Wareki date handling class, main implementation.
10
10
  class Date
11
- attr_accessor :year, :month, :day, :era_year, :era_name
11
+ include Comparable
12
+
13
+ attr_reader :year, :month, :day, :era_year, :era_name
12
14
 
13
15
  def self.today
14
16
  jd(::Date.today.jd)
15
17
  end
16
18
 
17
- def self._check_invalid_date(era, year, month, day)
18
- month == 12 or return true
19
- day > 2 or return true
20
- (era == '明治' && year == 5 ||
21
- %w(皇紀 神武天皇即位紀元).member?(era) &&
22
- year == GREGORIAN_START_YEAR - IMPERIAL_START_YEAR - 1) and
23
- return false
24
- true
25
- end
26
-
27
19
  def self._parse(str)
28
20
  str = str.to_s.gsub(/[[:space:]]/, '')
29
- match = (!str.empty? && REGEX.match(str)) or
21
+ match = REGEX.match(str)
22
+ (match && !match[0].empty?) or
30
23
  raise ArgumentError, "Invaild Date: #{str}"
31
24
  era = match[:era_name]
32
25
  if (era.nil? || era == '') && match[:year].nil?
33
26
  year = Date.today.year
34
27
  else
35
- (year = Utils.k2i(match[:year])) > 0 or
36
- raise ArgumentError, "Invalid year: #{str}"
28
+ year = Utils.k2i(match[:year])
29
+ year > 0 or raise ArgumentError, "Invalid year: #{str}"
37
30
  end
38
31
  month = day = 1
39
32
 
@@ -47,20 +40,20 @@ module Wareki
47
40
  end
48
41
 
49
42
  month > 12 || month < 1 and
50
- raise ArgumentError, "Invalid month: #{str}"
43
+ raise InvalidDate, "invalid date (month out of range): #{str}"
44
+
45
+ is_leap = !!(match[:is_leap] || match[:is_leap_post])
51
46
 
52
47
  if match[:day]
53
48
  if match[:day] == '晦'
54
- day = Utils.last_day_of_month(ERA_BY_NAME[era].year + year - 1, month, match[:is_leap])
49
+ civil_year = Utils.era_year_to_civil(era, year)
50
+ day = Utils.last_day_of_era_month(era, civil_year, month, is_leap)
55
51
  else
56
52
  day = Utils.k2i(match[:day])
57
53
  end
58
54
  end
59
55
 
60
- _check_invalid_date(era, year, month, day) or
61
- raise ArgumentError, "Invaild Date: #{str}"
62
-
63
- {era: era, year: year, month: month, day: day, is_leap: !!match[:is_leap]}
56
+ {era: era, year: year, month: month, day: day, is_leap: is_leap}
64
57
  end
65
58
 
66
59
  def self.parse(str)
@@ -86,23 +79,13 @@ module Wareki
86
79
  end
87
80
 
88
81
  def initialize(era_name, era_year, month = 1, day = 1, is_leap_month = false)
89
- raise ArgumentError, "Undefined era '#{era_name}'" if
90
- era_name.to_s != '' && era_name != '紀元前' && !ERA_BY_NAME[era_name]
91
-
82
+ @era_name = era_name.to_s
83
+ @era_year = era_year
92
84
  @month = month
93
85
  @day = day
94
86
  @is_leap_month = is_leap_month
95
- @era_name = era_name
96
- @era_year = era_year
97
- if era_name.to_s == '' || era_name == '西暦'
98
- @year = @era_year
99
- elsif era_name.to_s == '紀元前'
100
- @year = -@era_year
101
- elsif %w(皇紀 神武天皇即位紀元).include? era_name
102
- @year = era_year + IMPERIAL_START_YEAR
103
- else
104
- @year = ERA_BY_NAME[era_name].year + era_year - 1
105
- end
87
+ @year = Utils.era_year_to_civil(@era_name, @era_year)
88
+ _validate_date!
106
89
  end
107
90
 
108
91
  def imperial_year
@@ -110,7 +93,7 @@ module Wareki
110
93
  end
111
94
 
112
95
  def imperial_year=(v)
113
- @year = v - IMPERIAL_START_YEAR
96
+ self.year = v + IMPERIAL_START_YEAR
114
97
  end
115
98
 
116
99
  def leap_month?
@@ -118,28 +101,86 @@ module Wareki
118
101
  end
119
102
 
120
103
  def leap_month=(v)
104
+ @jd = nil
121
105
  @is_leap_month = v
122
106
  end
123
107
 
108
+ def year=(v)
109
+ era_year = Utils.civil_to_era_year(@era_name, v)
110
+ @jd = nil
111
+ @year = v
112
+ @era_year = era_year
113
+ end
114
+
115
+ def month=(v)
116
+ @jd = nil
117
+ @month = v
118
+ end
119
+
120
+ def day=(v)
121
+ @jd = nil
122
+ @day = v
123
+ end
124
+
125
+ def era_year=(v)
126
+ year = Utils.era_year_to_civil(@era_name, v)
127
+ @jd = nil
128
+ @era_year = v
129
+ @year = year
130
+ end
131
+
132
+ def era_name=(v)
133
+ era_name = v.to_s
134
+ year = Utils.era_year_to_civil(era_name, @era_year)
135
+ @jd = nil
136
+ @era_name = era_name
137
+ @year = year
138
+ end
139
+
124
140
  def __set_jd(v)
125
141
  @jd = v
126
142
  end
127
143
 
128
144
  def month_index
129
145
  return month - 1 if
130
- ['', '西暦', '紀元前'].include?(@era_name) || @year >= GREGORIAN_START_YEAR
146
+ WESTERN_ERA_NAMES.include?(@era_name) || @year >= GREGORIAN_START_YEAR
131
147
 
132
148
  yobj = YEAR_BY_NUM[@year] or
133
149
  raise UnsupportedDateRange, "Cannot get year info of #{inspect}"
134
150
  idx = month - 1
135
- idx += 1 if leap_month? || yobj.leap_month && month > yobj.leap_month
151
+ idx += 1 if leap_month? || (yobj.leap_month && month > yobj.leap_month)
136
152
  idx
137
153
  end
138
154
 
155
+ def last_day_of_month
156
+ Utils.last_day_of_era_month(@era_name, @year, month, leap_month?)
157
+ end
158
+
159
+ def _validate_date!
160
+ (month.is_a?(Integer) && month >= 1 && month <= 12) or
161
+ raise InvalidDate, "invalid date (month out of range): #{inspect}"
162
+ (day.is_a?(Integer) && day >= 1) or
163
+ raise InvalidDate, "invalid date (day out of range): #{inspect}"
164
+ if !WESTERN_ERA_NAMES.include?(@era_name) && @year < GREGORIAN_START_YEAR
165
+ # 暦テーブル外の年は従来どおり jd 変換時の UnsupportedDateRange に委ねる
166
+ yobj = YEAR_BY_NUM[@year] or return
167
+ !leap_month? || yobj.leap_month == month or
168
+ raise InvalidDate, "invalid date (no leap month): #{inspect}"
169
+ day <= yobj.month_days[month_index] or
170
+ raise InvalidDate, "invalid date (day out of range): #{inspect}"
171
+ else
172
+ leap_month? and
173
+ raise InvalidDate, "invalid date (no leap month): #{inspect}"
174
+ day <= last_day_of_month or
175
+ raise InvalidDate, "invalid date (day out of range): #{inspect}"
176
+ end
177
+ end
178
+
139
179
  def jd
140
180
  @jd and return @jd
141
181
 
142
- ['', '西暦', '紀元前'].include?(@era_name) and
182
+ _validate_date!
183
+ WESTERN_ERA_NAMES.include?(@era_name) and
143
184
  return @jd = ::Date.new(@year, month, day, ::Date::ITALY).jd
144
185
 
145
186
  @year >= GREGORIAN_START_YEAR and
@@ -158,17 +199,28 @@ module Wareki
158
199
  to_date.to_time
159
200
  end
160
201
 
202
+ FORMAT_DIRECTIVE_REGEX = /%J(-|[_0]{0,2}[0-9]*|)([fFyYegGoOiImMsSlLdD][kK]?)/.freeze
203
+ FORMAT_EXPANSION_REGEX = /(?<!%)(?:%%)*\K#{FORMAT_DIRECTIVE_REGEX}/.freeze
204
+
205
+ def expand_wareki_format(format_str)
206
+ format_str.to_str.gsub(FORMAT_EXPANSION_REGEX) { format($2, $1) || $& }
207
+ end
208
+
161
209
  def strftime(format_str = '%JF')
162
- ret = format_str.to_str.gsub(/%J([fFyYegGoOiImMsSlLdD][kK]?)/) { format($1) || $& }
210
+ ret = expand_wareki_format(format_str)
163
211
  ret.index('%') or return ret
164
212
  d = to_date
165
213
  d.respond_to?(:_wareki_strftime_orig) ? d._wareki_strftime_orig(ret) : d.strftime(ret)
166
214
  end
167
215
 
168
- def format(key)
216
+ def _number_format(opt)
217
+ Utils.number_format(opt)
218
+ end
219
+
220
+ def format(key, opt = '')
169
221
  case key.to_sym
170
222
  when :e then era_name
171
- when :g then era_name.to_s == '' ? '' : era_year
223
+ when :g then era_name.to_s == '' ? '' : Kernel.format(_number_format(opt), era_year)
172
224
  when :G then era_name.to_s == '' ? '' : Utils.i2z(era_year)
173
225
  when :Gk then era_name.to_s == '' ? '' : YaKansuji.to_kan(era_year, :simple)
174
226
  when :GK
@@ -185,14 +237,14 @@ module Wareki
185
237
  when :i then imperial_year
186
238
  when :I then Utils.i2z(imperial_year)
187
239
  when :Ik then YaKansuji.to_kan(imperial_year, :simple)
188
- when :s then month
240
+ when :s then Kernel.format(_number_format(opt), month)
189
241
  when :S then Utils.i2z(month)
190
242
  when :Sk then YaKansuji.to_kan(month, :simple)
191
243
  when :SK then Utils.alt_month_name(month)
192
244
  when :l then leap_month? ? "'" : ''
193
245
  when :L then leap_month? ? '’' : ''
194
246
  when :Lk then leap_month? ? '閏' : ''
195
- when :d then day
247
+ when :d then Kernel.format(_number_format(opt), day)
196
248
  when :D then Utils.i2z(day)
197
249
  when :Dk then YaKansuji.to_kan(day, :simple)
198
250
  when :DK
@@ -200,19 +252,19 @@ module Wareki
200
252
  '元'
201
253
  elsif day == 1
202
254
  '朔'
203
- elsif day == Utils.last_day_of_month(year, month, leap_month?)
255
+ elsif day == last_day_of_month
204
256
  '晦'
205
257
  else
206
258
  YaKansuji.to_kan(day, :simple)
207
259
  end
208
- when :m then "#{format(:s)}#{format(:l)}"
260
+ when :m then "#{format(:s, opt)}#{format(:l)}"
209
261
  when :M then "#{format(:Lk)}#{format(:S)}"
210
262
  when :Mk then "#{format(:Lk)}#{format(:Sk)}"
211
- when :y then "#{format(:e)}#{format(:g)}"
263
+ when :y then "#{format(:e)}#{format(:g, opt)}"
212
264
  when :Y then "#{format(:e)}#{format(:G)}"
213
265
  when :Yk then "#{format(:e)}#{format(:Gk)}"
214
266
  when :YK then "#{format(:e)}#{format(:GK)}"
215
- when :f then "#{format(:e)}#{format(:g)}年#{format(:s)}#{format(:l)}月#{format(:d)}日"
267
+ when :f then "#{format(:e)}#{format(:g, opt)}年#{format(:s, opt)}#{format(:l)}月#{format(:d, opt)}日"
216
268
  when :F then "#{format(:e)}#{format(:GK)}年#{format(:Lk)}#{format(:Sk)}月#{format(:Dk)}日"
217
269
  end
218
270
  end
@@ -238,20 +290,48 @@ module Wareki
238
290
  true
239
291
  end
240
292
 
293
+ def hash
294
+ [self.class, @era_name, @era_year, @year, @month, @day, leap_month?].hash
295
+ end
296
+
297
+ def <=>(other)
298
+ ojd = _jd_if_date_like(other)
299
+ ojd = other if ojd.nil? && other.is_a?(Numeric)
300
+ ojd.nil? and return nil
301
+ jd <=> ojd
302
+ end
303
+
304
+ def succ
305
+ self + 1
306
+ end
307
+
241
308
  def -(other)
242
- self.class.jd jd - _to_jd_for_calc(other)
309
+ n = _to_days(other)
310
+ n.nil? or return self.class.jd(jd - n)
311
+ ojd = _jd_if_date_like(other)
312
+ ojd and return jd - ojd
313
+ raise TypeError, "Cannot subtract #{other.inspect} from Wareki::Date"
243
314
  end
244
315
 
245
316
  def +(other)
246
- self.class.jd jd + _to_jd_for_calc(other)
317
+ n = _to_days(other)
318
+ n.nil? and raise TypeError, "Cannot add #{other.inspect} to Wareki::Date"
319
+ self.class.jd(jd + n)
247
320
  end
248
321
 
249
- def _to_jd_for_calc(other)
250
- other.class.to_s == 'ActiveSupport::Duration' and
251
- raise NotImplementedError, 'Date calcration with ActiveSupport::Duration currently is not supported. Please use numeric.'
252
- other.respond_to?(:to_date) and other = other.to_date
253
- other.respond_to?(:jd) and other = other.jd
254
- other
322
+ def _to_days(other)
323
+ # rubocop:disable Style/ClassEqualityComparison
324
+ return other.in_days if other.class.name == 'ActiveSupport::Duration'
325
+ # rubocop:enable Style/ClassEqualityComparison
326
+
327
+ other.is_a?(Numeric) ? other : nil
328
+ end
329
+
330
+ def _jd_if_date_like(other)
331
+ return other.jd if other.respond_to?(:jd)
332
+
333
+ other.respond_to?(:to_date) && !other.is_a?(Numeric) and other = other.to_date
334
+ other.respond_to?(:jd) ? other.jd : nil
255
335
  end
256
336
  end
257
337
  end
@@ -252,7 +252,7 @@ module Wareki
252
252
  Era.new("昭和", 1926, 2424875, 2447534),
253
253
  Era.new("平成", 1989, 2447535, 2458604),
254
254
  Era.new("令和", 2019, 2458605, DAY_MAX),
255
- ].freeze
255
+ ].each(&:freeze).freeze
256
256
  ERA_NORTH_DEFS = [
257
257
  Era.new("大化", 645, 1956842, 1958551),
258
258
  Era.new("白雉", 650, 1958551, 1960259),
@@ -502,5 +502,5 @@ module Wareki
502
502
  Era.new("昭和", 1926, 2424875, 2447534),
503
503
  Era.new("平成", 1989, 2447535, 2458604),
504
504
  Era.new("令和", 2019, 2458605, DAY_MAX),
505
- ].freeze
505
+ ].each(&:freeze).freeze
506
506
  end
@@ -1,10 +1,30 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'wareki/date'
3
5
  module Wareki
4
6
  # :nodoc:
5
7
  module StdExt
8
+ module_function
9
+
10
+ def wareki_directive?(format)
11
+ !!(format.to_str =~ Wareki::Date::FORMAT_EXPANSION_REGEX)
12
+ end
13
+
14
+ def wareki_time_directive?(format)
15
+ !!(format.to_str =~ Wareki::Utils::TIME_FORMAT_EXPANSION_REGEX)
16
+ end
17
+
18
+ # %JT 時刻トークンと %J 日付トークンの両方を展開した文字列を返す
19
+ def expand_all_wareki_formats(format, datetime)
20
+ ret = format
21
+ ret = Wareki::Utils.expand_time_format(ret, datetime) if wareki_time_directive?(ret)
22
+ ret = datetime.to_wareki_date.expand_wareki_format(ret) if wareki_directive?(ret)
23
+ ret
24
+ end
6
25
  end
7
26
  end
27
+
8
28
  # :nodoc:
9
29
  class Date
10
30
  JAPAN = Wareki::GREGORIAN_START
@@ -15,19 +35,55 @@ class Date
15
35
 
16
36
  alias _wareki_strftime_orig strftime
17
37
  def strftime(format = '%F')
18
- if format.index('%J')
19
- to_wareki_date.strftime(format)
20
- else
21
- _wareki_strftime_orig(format)
22
- end
38
+ return _wareki_strftime_orig(format) unless Wareki::StdExt.wareki_directive?(format)
39
+
40
+ _wareki_strftime_orig(to_wareki_date.expand_wareki_format(format))
23
41
  end
24
42
 
25
43
  class << self
26
44
  alias _wareki_parse_orig parse
27
- def parse(str, comp = true, start = ::Date::ITALY)
45
+ def parse(str = '-4712-01-01', comp = true, start = ::Date::ITALY)
46
+ str = Wareki::Utils.normalize_time(str)
47
+ str.to_s =~ Wareki::PARSE_QUICK_FILTER or
48
+ return ::Date._wareki_parse_orig(str, comp, start)
28
49
  Wareki::Date.parse(str).to_date(start)
50
+ rescue Wareki::InvalidDate
51
+ raise
29
52
  rescue ArgumentError, Wareki::UnsupportedDateRange
30
53
  ::Date._wareki_parse_orig(str, comp, start)
31
54
  end
55
+
56
+ alias _wareki__parse_orig _parse
57
+ def _parse(str, comp = true)
58
+ str = Wareki::Utils.normalize_time(str)
59
+ str.to_s =~ Wareki::PARSE_QUICK_FILTER or
60
+ return ::Date._wareki__parse_orig(str, comp)
61
+ di = Wareki::Date._parse(str)
62
+ wdate = Wareki::Date.new(di[:era], di[:year], di[:month], di[:day], di[:is_leap])
63
+ rescue ArgumentError, Wareki::UnsupportedDateRange
64
+ ::Date._wareki__parse_orig(str, comp)
65
+ else
66
+ ::Date._wareki__parse_orig(str.sub(Wareki::REGEX, wdate.strftime('%F ')), comp)
67
+ end
68
+ end
69
+ end
70
+
71
+ # :nodoc:
72
+ class DateTime
73
+ alias _wareki_strftime_orig strftime
74
+ def strftime(format = '%FT%T%:z')
75
+ _wareki_strftime_orig(Wareki::StdExt.expand_all_wareki_formats(format, self))
76
+ end
77
+ end
78
+
79
+ # :nodoc:
80
+ class Time
81
+ def to_wareki_date
82
+ Wareki::Date.jd(to_date.jd)
83
+ end
84
+
85
+ alias _wareki_strftime_orig strftime
86
+ def strftime(format)
87
+ _wareki_strftime_orig(Wareki::StdExt.expand_all_wareki_formats(format, self))
32
88
  end
33
89
  end
data/lib/wareki/utils.rb CHANGED
@@ -5,6 +5,9 @@ require 'ya_kansuji'
5
5
  module Wareki
6
6
  # Static utility methods.
7
7
  module Utils
8
+ TIME_FORMAT_DIRECTIVE_REGEX = /%J(-|[_0]{0,2}[0-9]*|)(T(?:[fF]|[HMS]k?))/.freeze
9
+ TIME_FORMAT_EXPANSION_REGEX = /(?<!%)(?:%%)*\K#{TIME_FORMAT_DIRECTIVE_REGEX}/.freeze
10
+
8
11
  module_function
9
12
 
10
13
  def last_day_of_month(year, month, is_leap)
@@ -16,25 +19,47 @@ module Wareki
16
19
  end
17
20
 
18
21
  def _last_day_of_month_gregorian(year, month)
19
- tmp_y = year
20
- tmp_m = month
21
- if month == 12
22
- tmp_y += 1
23
- tmp_m = 1
24
- else
25
- tmp_m += 1
26
- end
27
- (::Date.new(tmp_y, tmp_m, 1, ::Date::GREGORIAN) - 1).day
22
+ ::Date.new(year, month, -1, ::Date::GREGORIAN).day
28
23
  end
29
24
 
30
25
  def _last_day_of_month_from_defs(year, month, is_leap)
31
26
  yobj = YEAR_BY_NUM[year] or
32
- raise UnsupportedDateRange, "Cannot find year #{inspect}"
27
+ raise UnsupportedDateRange, "Cannot find year #{year}"
33
28
  month_idx = month - 1
34
- month_idx += 1 if is_leap || yobj.leap_month && yobj.leap_month < month
29
+ month_idx += 1 if is_leap || (yobj.leap_month && yobj.leap_month < month)
35
30
  yobj.month_days[month_idx]
36
31
  end
37
32
 
33
+ def era_year_to_civil(era_name, era_year)
34
+ era_name = era_name.to_s
35
+ return era_year if ['', '西暦'].include?(era_name)
36
+ return -era_year if era_name == '紀元前'
37
+ return era_year + IMPERIAL_START_YEAR if IMPERIAL_ERA_NAMES.include?(era_name)
38
+
39
+ era = ERA_BY_NAME[era_name] or
40
+ raise ArgumentError, "Undefined era '#{era_name}'"
41
+ era.year + era_year - 1
42
+ end
43
+
44
+ def civil_to_era_year(era_name, year)
45
+ era_name = era_name.to_s
46
+ return year if ['', '西暦'].include?(era_name)
47
+ return -year if era_name == '紀元前'
48
+ return year - IMPERIAL_START_YEAR if IMPERIAL_ERA_NAMES.include?(era_name)
49
+
50
+ era = ERA_BY_NAME[era_name] or
51
+ raise ArgumentError, "Undefined era '#{era_name}'"
52
+ year - era.year + 1
53
+ end
54
+
55
+ def last_day_of_era_month(era_name, civil_year, month, is_leap)
56
+ if WESTERN_ERA_NAMES.include?(era_name.to_s)
57
+ ::Date.new(civil_year, month, -1, ::Date::ITALY).day
58
+ else
59
+ last_day_of_month(civil_year, month, is_leap)
60
+ end
61
+ end
62
+
38
63
  def alt_month_name_to_i(name)
39
64
  i = ALT_MONTH_NAME.index(name) or return false
40
65
  i + 1
@@ -85,6 +110,7 @@ module Wareki
85
110
 
86
111
  def find_year(d)
87
112
  jd = _to_jd(d)
113
+ jd < YEAR_DEFS.first.start and return nil
88
114
  YEAR_DEFS.bsearch { |y| y.end >= jd }
89
115
  end
90
116
 
@@ -111,6 +137,59 @@ module Wareki
111
137
  end
112
138
  end
113
139
 
140
+ # 日本語の時刻表記 (漢数字・全角数字の時分秒、午前/午後、半、正午) を
141
+ # 等価な "HH:MM(:SS)" 表記へ置換する。値の範囲チェックは行わず、
142
+ # 妥当性判断は Ruby 標準パーサに委ねる (二十五時 -> "25:00")。
143
+ def normalize_time(str)
144
+ str.to_s =~ TIME_PARSE_QUICK_FILTER or return str
145
+ str.to_s.sub(TIME_REGEX) { _time_match_to_s(Regexp.last_match) }
146
+ end
147
+
148
+ def _time_match_to_s(match)
149
+ return '12:00' if match[:noon]
150
+
151
+ hour = k2i(match[:hour])
152
+ hour += 12 if match[:ampm] == '午後' && hour < 12
153
+ min = 0
154
+ min = 30 if match[:half]
155
+ min = k2i(match[:min]) if match[:min]
156
+ return Kernel.format('%<hour>02d:%<min>02d', hour: hour, min: min) unless match[:sec]
157
+
158
+ Kernel.format('%<hour>02d:%<min>02d:%<sec>02d', hour: hour, min: min, sec: k2i(match[:sec]))
159
+ end
160
+
161
+ def number_format(opt)
162
+ case opt
163
+ when '', '0', '_0' then '%02d'
164
+ when '-' then '%d'
165
+ when /_\Z/ then '%2d'
166
+ when /0?_/ then "%#{opt.sub(/0?_/, '')}d"
167
+ when /_?0/ then "%#{opt.sub(/_?0/, '0')}d"
168
+ else "%0#{opt}d"
169
+ end
170
+ end
171
+
172
+ def expand_time_format(format_str, time)
173
+ format_str.to_str.gsub(TIME_FORMAT_EXPANSION_REGEX) { _format_time_directive($2, $1, time) || $& }
174
+ end
175
+
176
+ def _format_time_directive(key, opt, time)
177
+ case key.to_sym
178
+ when :Tf
179
+ nf = number_format(opt)
180
+ Kernel.format("#{nf}時#{nf}分#{nf}秒", time.hour, time.min, time.sec)
181
+ when :TF
182
+ "#{YaKansuji.to_kan(time.hour, :simple)}時#{YaKansuji.to_kan(time.min, :simple)}分" \
183
+ "#{YaKansuji.to_kan(time.sec, :simple)}秒"
184
+ when :TH then i2z(time.hour)
185
+ when :THk then YaKansuji.to_kan(time.hour, :simple)
186
+ when :TM then i2z(time.min)
187
+ when :TMk then YaKansuji.to_kan(time.min, :simple)
188
+ when :TS then i2z(time.sec)
189
+ when :TSk then YaKansuji.to_kan(time.sec, :simple)
190
+ end
191
+ end
192
+
114
193
  # DEPRECATED
115
194
  def kan_to_i(*args)
116
195
  warn '[DEPRECATED] Wareki::Utils.kan_to_i: Please use ya_kansuji gem to handle kansuji'
@@ -120,7 +199,7 @@ module Wareki
120
199
  # DEPRECATED
121
200
  def i_to_kan(*args)
122
201
  warn '[DEPRECATED] Wareki::Utils.i_to_kan: Please use ya_kansuji gem to handle kansuji'
123
- i2k(*args)
202
+ YaKansuji.to_kan(*args)
124
203
  end
125
204
  end
126
205
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Wareki
2
- VERSION = '1.0.0'.freeze
4
+ VERSION = '2.0.0'
3
5
  end
data/lib/wareki.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'wareki/version'
2
4
  require 'wareki/date'
3
5
  require 'wareki/std_ext'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wareki
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tatsuki Sugiura
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-15 00:00:00.000000000 Z
11
+ date: 2026-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ya_kansuji
@@ -30,48 +30,6 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 2.0.0
33
- - !ruby/object:Gem::Dependency
34
- name: bundler
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '1.9'
40
- type: :development
41
- prerelease: false
42
- version_requirements: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '1.9'
47
- - !ruby/object:Gem::Dependency
48
- name: rake
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '10.0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '10.0'
61
- - !ruby/object:Gem::Dependency
62
- name: rspec
63
- requirement: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- type: :development
69
- prerelease: false
70
- version_requirements: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
33
  description: |
76
34
  Pure ruby library of Wareki (Japanese calendar date) that supports string parsing,
77
35
  formatting, and bi-directional convertion with standard Date class.
@@ -95,9 +53,10 @@ files:
95
53
  - lib/wareki/version.rb
96
54
  homepage: https://github.com/sugi/wareki
97
55
  licenses:
98
- - BSD
99
- metadata: {}
100
- post_install_message:
56
+ - BSD-2-Clause
57
+ metadata:
58
+ rubygems_mfa_required: 'true'
59
+ post_install_message:
101
60
  rdoc_options: []
102
61
  require_paths:
103
62
  - lib
@@ -112,9 +71,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
112
71
  - !ruby/object:Gem::Version
113
72
  version: '0'
114
73
  requirements: []
115
- rubyforge_project:
116
- rubygems_version: 2.7.6.2
117
- signing_key:
74
+ rubygems_version: 3.1.6
75
+ signing_key:
118
76
  specification_version: 4
119
77
  summary: Pure ruby library of Wareki (Japanese calendar date)
120
78
  test_files: []