ya_kansuji 1.2.0 → 1.3.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: 97f2f4c063933b7c86ede7c11d8d34704e612f7eb4e82e2f60f469d8aa8ded4c
4
- data.tar.gz: 4aa55c66f2d6b5ae168278e0b2b1a096e062ae1a3e96d31bcb118f415c184bb0
3
+ metadata.gz: 2ebbcd000d7dd15c3c1438fffa073ee979cda4ae3f74cc898fbe6bab3d0f2308
4
+ data.tar.gz: 1674ffc3d50bc2748f7700e1faa0d579ef1d4d98fdd6830a0b1f7486adc7c169
5
5
  SHA512:
6
- metadata.gz: 0203eaef1b77450bd6ea5c9666cf695bfe72fd7fb21d966d18bb177fa38db3ca500dff690841c3f1b5d25b9406c62f7bf38a90be98f6c5983f5df68fe65df8cb
7
- data.tar.gz: 7a420d832dbd4da1149ac293820dca4f510226c53513bc1f579d2fa889cc8b16708f717406c76be8937f21623090eaf7d073896234296598e40e36a8275091ae
6
+ metadata.gz: 29b3931d46062d1874d25db90d1dc62ae9f633d0328e23f704e661f85ea81816d348afed00d6bc27be90eb9e5b0d7c57c8e9a288bdd5d8727900982354fe5cd3
7
+ data.tar.gz: d3b04eeff5270d76b509ad9ecd0742bc26767066c488259a666650fb1fd11d45bac4604761fa99420614c17ff5313550787d6ddd1281243555501337c8781e61
data/ChangeLog CHANGED
@@ -1,3 +1,13 @@
1
+ 2026-07-14 Tatsuki Sugiura <sugi@nemui.org>
2
+
3
+ * Version: 1.3.0
4
+ * Support fractional numbers (Float / Rational / BigDecimal) on
5
+ to_kan, using Japanese small number units (分 厘 毛 ... 清浄)
6
+ * Fractions are rounded at 10^-21; floats are interpreted by their
7
+ shortest decimal representation (to_s)
8
+ * All builtin formatters render fractions in their own style
9
+ * Add Numeric#to_kan to core extensions and refinements
10
+
1
11
  2026-07-14 Tatsuki Sugiura <sugi@nemui.org>
2
12
 
3
13
  * Version: 1.2.0
data/README.md CHANGED
@@ -23,9 +23,10 @@ YaKansuji は ruby 用の日本語の漢数字ライブラリです。 和暦ラ
23
23
  * カンマ表現 (12,345 / 5億3,456万 / 二万、五十)
24
24
  * 負数 (マイナス五十)
25
25
  * 数値から漢数字, 漢字混じり数値文字列フォーマット
26
+ * 小数 (分厘毛〜清浄)
26
27
  * 複数のビルトインフォーマッタ
27
28
  * フォーマッタプラグイン機構
28
- * (オプション) 標準の String, Interger クラスの拡張
29
+ * (オプション) 標準の String, Numeric クラスの拡張
29
30
 
30
31
  ## インストール
31
32
 
@@ -62,6 +63,23 @@ YaKansuji.to_kan(-12345) # => マイナス一万二千三百四十五
62
63
  対応する値域は `-(10**72 - 1)` から `10**72 - 1` です。
63
64
  数値として整数化した値がこの範囲を超える場合、`RangeError` が発生します。
64
65
 
66
+ #### 小数
67
+
68
+ Float・Rational・BigDecimal を渡すと、小数の命数法 (分 = 10⁻¹ 〜 清浄 = 10⁻²¹) で小数部を出力します。
69
+ 10⁻²¹ より小さい部分は四捨五入されます。
70
+
71
+ ```ruby
72
+ YaKansuji.to_kan(0.5) # => "五分"
73
+ YaKansuji.to_kan(123.456) # => "百二十三・四分五厘六毛"
74
+ YaKansuji.to_kan(3.14159, :judic_v) # => "三・一四一五九"
75
+ YaKansuji.to_kan(123.456, :gov) # => "123.456"
76
+ YaKansuji.to_kan(1.05, :judic_h) # => "1.05"
77
+ ```
78
+
79
+ Float は `to_s` の最短表現として解釈されるため、`0.1` は正確に「一分」になります。
80
+ 逆に浮動小数点演算の誤差はそのまま出力されます (`0.1 + 0.2` → `0.30000000000000004` → 「三分四弾指」)。
81
+ 誤差を避けたい場合は Rational か BigDecimal を渡してください。
82
+
65
83
  このさい、2つめの引数にシンボルを渡すことにより、フォーマッタを切り替えることができます。
66
84
 
67
85
  ```ruby
@@ -84,7 +102,9 @@ YaKansuji.to_kan(12340005, :judic_h) # => "1234万0005"
84
102
 
85
103
  `YaKansuji#register_formatter` を使うと独自のフォーマッタを登録できます。
86
104
  `#call` を持つ任意のオブジェクトもしくはブロックを渡します。第1引数に数字、第2引数にオプションが渡ります。
87
- 負数を渡した場合も、フォーマッタには絶対値 (非負整数) が渡され、符号 (「マイナス」) は `to_kan` 側で前置されます。
105
+ 負数を渡した場合も、フォーマッタには絶対値 (非負の値) が渡され、符号 (「マイナス」) は `to_kan` 側で前置されます。
106
+ 小数を含む値を渡した場合、フォーマッタには正規化済みの値
107
+ (整数なら Integer、小数を含むなら Rational) がそのまま渡ります。
88
108
 
89
109
  ```ruby
90
110
  YaKansuji.register_formatter :hoge, -> (num, opts = {}) {
@@ -100,14 +120,14 @@ YaKansuji.to_kan(123, :hoge) # => "たくさん"
100
120
 
101
121
  ### 標準クラス拡張
102
122
 
103
- 標準では YaKansuji の特異メソッドを直接呼ぶことで使用できますが、短く書く場合、必要応じてビルトインクラス (String, Integer) の拡張を使うことができます。
123
+ 標準では YaKansuji の特異メソッドを直接呼ぶことで使用できますが、短く書く場合、必要応じてビルトインクラス (String, Numeric) の拡張を使うことができます。
104
124
 
105
- クラス拡張では `String#to_i` を置き換え、 `Integer#to_kan` を追加します。
125
+ クラス拡張では `String#to_i` を置き換え、`Numeric#to_kan` を追加します。
106
126
  拡張の方法は、Refinements と直接拡張の好きな方を選択できます。
107
127
 
108
128
  #### [Refinements](https://docs.ruby-lang.org/en/2.6.0/syntax/refinements_rdoc.html) による拡張
109
129
 
110
- `YaKansuji::CoreRefine` を using すると、 `String#to_i` を置き換え、 `Integer#to_kan` を追加します。
130
+ `YaKansuji::CoreRefine` を using すると、 `String#to_i` を置き換え、 `Numeric#to_kan` を追加します。
111
131
 
112
132
  ```ruby
113
133
  puts "二万".to_i # => 0
@@ -117,6 +137,7 @@ using YaKansuji::CoreRefine
117
137
  puts "二万".to_i # => 20000
118
138
  puts 20000.to_kan #= > "二万"
119
139
  puts 20000.to_kan(:gov) # => "2万"
140
+ puts 0.5.to_kan # => "五分"
120
141
  ```
121
142
 
122
143
  ちなみに、 `to_i` に base が渡された場合は、漢数字が解釈されずにビルトインクラスのものが呼ばれます。
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ya_kansuji'
4
+
5
+ # Core extension by kansuji
6
+ class Numeric
7
+ def to_kan(formatter = :simple, options = {})
8
+ YaKansuji.to_kan self, formatter, options
9
+ end
10
+ end
@@ -2,3 +2,4 @@
2
2
 
3
3
  require 'ya_kansuji/core_ext/string'
4
4
  require 'ya_kansuji/core_ext/integer'
5
+ require 'ya_kansuji/core_ext/numeric'
@@ -18,6 +18,12 @@ module YaKansuji
18
18
  YaKansuji.to_kan self, formatter, options
19
19
  end
20
20
  end
21
+
22
+ refine Numeric do
23
+ def to_kan(formatter = :simple, options = {})
24
+ YaKansuji.to_kan self, formatter, options
25
+ end
26
+ end
21
27
  end
22
28
  end
23
29
 
@@ -7,13 +7,15 @@ module YaKansuji
7
7
  module_function
8
8
 
9
9
  def call(num, _options = {})
10
- return '0' if num.zero?
10
+ int, frac = Formatter.split_fraction(num)
11
+ frac.empty? ? frac_part = nil : frac_part = ".#{frac.join}"
12
+ return frac_part ? "0#{frac_part}" : '0' if int.zero?
11
13
 
12
14
  parts = []
13
- chunks = Formatter.split_by_unit4(num)
15
+ chunks = Formatter.split_by_unit4(int)
14
16
  (chunks.size - 1).downto(0) do |idx4|
15
17
  i4 = chunks[idx4]
16
- next if i4.zero?
18
+ next if i4.zero? && !(idx4.zero? && frac_part)
17
19
 
18
20
  unit4 = Formatter::UNIT4_UNITS[idx4]
19
21
  if i4 == 1
@@ -22,7 +24,9 @@ module YaKansuji
22
24
  end
23
25
  parts << (i4.to_s + unit4)
24
26
  end
25
- parts.join(', ')
27
+ ret = parts.join(', ')
28
+ ret << frac_part if frac_part
29
+ ret
26
30
  end
27
31
  end
28
32
  YaKansuji.register_formatter :gov, Formatter::Gov
@@ -8,14 +8,15 @@ module YaKansuji
8
8
  module_function
9
9
 
10
10
  def call(num, _options = {})
11
- return '0' if num.zero?
11
+ int, frac = Formatter.split_fraction(num)
12
+ return '0' if int.zero? && frac.empty?
12
13
 
13
14
  ret = +''
14
15
  head = true
15
- chunks = Formatter.split_by_unit4(num)
16
+ chunks = Formatter.split_by_unit4(int)
16
17
  (chunks.size - 1).downto(0) do |idx4|
17
18
  i4 = chunks[idx4]
18
- next if i4.zero?
19
+ next if i4.zero? && !(idx4.zero? && !frac.empty?)
19
20
 
20
21
  unit4 = Formatter::UNIT4_UNITS[idx4]
21
22
  if head
@@ -25,6 +26,7 @@ module YaKansuji
25
26
  end
26
27
  head = false
27
28
  end
29
+ ret << '.' << frac.join unless frac.empty?
28
30
  ret.tr('0-9', '0-9')
29
31
  end
30
32
  YaKansuji.register_formatter :judic_h, self
@@ -8,14 +8,15 @@ module YaKansuji
8
8
  module_function
9
9
 
10
10
  def call(num, _options = {})
11
- return '〇' if num.zero?
11
+ int, frac = Formatter.split_fraction(num)
12
+ return '〇' if int.zero? && frac.empty?
12
13
 
13
14
  ret = +''
14
15
  head = true
15
- chunks = Formatter.split_by_unit4(num)
16
+ chunks = Formatter.split_by_unit4(int)
16
17
  (chunks.size - 1).downto(0) do |idx4|
17
18
  i4 = chunks[idx4]
18
- next if i4.zero?
19
+ next if i4.zero? && !(idx4.zero? && !frac.empty?)
19
20
 
20
21
  unit4 = Formatter::UNIT4_UNITS[idx4]
21
22
  if head
@@ -25,6 +26,7 @@ module YaKansuji
25
26
  end
26
27
  head = false
27
28
  end
29
+ ret << '・' << frac.join unless frac.empty?
28
30
  ret.tr('0-9', '〇一二三四五六七八九')
29
31
  end
30
32
  YaKansuji.register_formatter :judic_v, self
@@ -8,13 +8,15 @@ module YaKansuji
8
8
  module_function
9
9
 
10
10
  def call(num, _options = {})
11
- return '0' if num.zero?
11
+ int, frac = Formatter.split_fraction(num)
12
+ frac.empty? ? frac_part = nil : frac_part = ".#{frac.join}"
13
+ return frac_part ? "0#{frac_part}" : '0' if int.zero?
12
14
 
13
15
  ret = +''
14
- chunks = Formatter.split_by_unit4(num)
16
+ chunks = Formatter.split_by_unit4(int)
15
17
  (chunks.size - 1).downto(0) do |idx4|
16
18
  i4 = chunks[idx4]
17
- next if i4.zero?
19
+ next if i4.zero? && !(idx4.zero? && frac_part)
18
20
 
19
21
  unit4 = Formatter::UNIT4_UNITS[idx4]
20
22
  if i4 == 1
@@ -24,6 +26,7 @@ module YaKansuji
24
26
 
25
27
  ret << ((i4 >= 1000 ? "#{i4.to_s[0]},#{i4.to_s[1..-1]}" : i4.to_s) + unit4)
26
28
  end
29
+ ret << frac_part if frac_part
27
30
  ret
28
31
  end
29
32
  YaKansuji.register_formatter :lawyer, self
@@ -10,10 +10,11 @@ module YaKansuji
10
10
  module_function
11
11
 
12
12
  def call(num, _options = {})
13
- return '零' if num.zero?
13
+ int, frac = Formatter.split_fraction(num)
14
+ return '零' if int.zero? && frac.empty?
14
15
 
15
16
  ret = nil
16
- chunks = Formatter.split_by_unit4(num)
17
+ chunks = Formatter.split_by_unit4(int)
17
18
  (chunks.size - 1).downto(0) do |idx4|
18
19
  i4 = chunks[idx4]
19
20
  next if i4.zero?
@@ -40,6 +41,13 @@ module YaKansuji
40
41
  end
41
42
  ret << unit4
42
43
  end
44
+ unless frac.empty?
45
+ ret << '・' if ret
46
+ ret ||= +''
47
+ frac.each_with_index do |d, idx|
48
+ ret << DIGITS[d - 1] << UNIT_FRAC[idx] unless d.zero?
49
+ end
50
+ end
43
51
  ret || ''
44
52
  end
45
53
  YaKansuji.register_formatter :simple, self
@@ -6,6 +6,7 @@ module YaKansuji
6
6
  UNIT4_BASE = 10_000
7
7
  UNIT4_CHUNK_LIMIT = UNIT_EXP4.size + 1
8
8
  UNIT4_UNITS = ([''] + UNIT_EXP4).freeze
9
+ EMPTY_FRACTION = [].freeze
9
10
 
10
11
  module_function
11
12
 
@@ -18,5 +19,21 @@ module YaKansuji
18
19
  end
19
20
  chunks
20
21
  end
22
+
23
+ def split_fraction(num)
24
+ num = YaKansuji.normalize_value(num)
25
+ raise ArgumentError, 'Value must be non-negative' if num.negative?
26
+
27
+ return [num, EMPTY_FRACTION] if num.is_a?(Integer)
28
+
29
+ int = num.truncate
30
+ scaled = ((num - int) * FRAC_BASE).to_i
31
+ digits = []
32
+ UNIT_FRAC.size.times do
33
+ scaled, d = scaled.divmod(10)
34
+ digits.unshift(d) unless digits.empty? && d.zero?
35
+ end
36
+ [int, digits]
37
+ end
21
38
  end
22
39
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module YaKansuji
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.0'
5
5
  end
data/lib/ya_kansuji.rb CHANGED
@@ -7,6 +7,9 @@ module YaKansuji
7
7
  UNIT_EXP3 = %w(十 百 千).freeze
8
8
  UNIT_EXP4 = %w(万 億 兆 京 垓 𥝱 穣 溝 澗 正 載 極 恒河沙 阿僧祇 那由他 不可思議 無量大数).freeze
9
9
  MAX_VALUE = (10_000**(UNIT_EXP4.size + 1)) - 1
10
+ UNIT_FRAC = %w(分 厘 毛 糸 忽 微 繊 沙 塵 埃 渺 漠
11
+ 模糊 逡巡 須臾 瞬息 弾指 刹那 六徳 虚空 清浄).freeze
12
+ FRAC_BASE = 10**UNIT_FRAC.size
10
13
  NUM_ALT_CHARS = '〇一二三四五六七八九0123456789零壱壹弌弐貳貮参參弎肆伍陸漆質柒捌玖拾什陌佰阡仟萬秭'
11
14
  NUM_NORMALIZED_CHARS = '01234567890123456789011122233345677789十十百百千千万𥝱'
12
15
  # rubocop:disable Lint/DuplicateRegexpCharacterClassElement
@@ -81,6 +84,44 @@ module YaKansuji
81
84
  matched[0].start_with?('マイナス') ? -ret : ret
82
85
  end
83
86
 
87
+ def normalize_value(num)
88
+ return num if num.is_a?(Integer)
89
+
90
+ unless num.is_a?(Numeric)
91
+ return num._to_i_ya_kansuji_orig if num.respond_to?(:_to_i_ya_kansuji_orig)
92
+
93
+ return num.to_i
94
+ end
95
+
96
+ return num.to_i unless num.respond_to?(:truncate)
97
+
98
+ num = rational_from_float(num) if num.is_a?(Float)
99
+ num = num.to_r if num.respond_to?(:to_r)
100
+ int = num.truncate
101
+ scaled = ((num - int) * FRAC_BASE).round
102
+ if scaled.abs >= FRAC_BASE
103
+ if scaled.negative?
104
+ int -= 1
105
+ else
106
+ int += 1
107
+ end
108
+ scaled = 0
109
+ end
110
+ scaled.zero? ? int : Rational((int * FRAC_BASE) + scaled, FRAC_BASE)
111
+ end
112
+
113
+ def rational_from_float(num)
114
+ raise FloatDomainError, num.to_s if num.nan? || num.infinite?
115
+
116
+ m = /\A(-?)(\d+)(?:\.(\d+))?(?:e([+-]?\d+))?\z/.match(num.to_s)
117
+ raise FloatDomainError, num.to_s unless m
118
+
119
+ ret = Rational(Integer(m[2] + (m[3] || ''), 10))
120
+ ret = -ret unless m[1].empty?
121
+ exp = (m[4] ? Integer(m[4], 10) : 0) - (m[3] ? m[3].size : 0)
122
+ exp >= 0 ? ret * (10**exp) : ret / (10**-exp)
123
+ end
124
+
84
125
  def register_formatter(sym, proc = nil, &block)
85
126
  if block_given?
86
127
  @@formatters[sym] = block
@@ -100,8 +141,8 @@ module YaKansuji
100
141
  end
101
142
 
102
143
  def to_kan(num, formatter = :simple, options = {})
103
- num.respond_to?(:_to_i_ya_kansuji_orig) ? num = num._to_i_ya_kansuji_orig : num = num.to_i
104
- if num.abs > MAX_VALUE
144
+ num = normalize_value(num)
145
+ if num.truncate.abs > MAX_VALUE
105
146
  raise RangeError, "Value must be between #{-MAX_VALUE} and #{MAX_VALUE}"
106
147
  end
107
148
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ya_kansuji
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tatsuki Sugiura
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-13 00:00:00.000000000 Z
11
+ date: 2026-07-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: |
14
14
  Pure ruby library of Japanese kansuji.
@@ -25,6 +25,7 @@ files:
25
25
  - lib/ya_kansuji.rb
26
26
  - lib/ya_kansuji/core_ext.rb
27
27
  - lib/ya_kansuji/core_ext/integer.rb
28
+ - lib/ya_kansuji/core_ext/numeric.rb
28
29
  - lib/ya_kansuji/core_ext/string.rb
29
30
  - lib/ya_kansuji/core_refine.rb
30
31
  - lib/ya_kansuji/formatter.rb