num2words 0.3.0 → 0.3.1

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: 82267cd5645c3767f5b34c5c7c54d6f997ee26f19425b86937ee021f747f19f4
4
- data.tar.gz: 33da5152ce64b117d4d9e59f2f33a63331b037cea6562f3c6fafc77d6b9f9f71
3
+ metadata.gz: 78f705a73f571176b4d920dfe220590d2bd217ea513fb817028a0ae2c22221cd
4
+ data.tar.gz: 1e8726b93feee82a3b87143ae8a25edf388d726f65ecf424ec713fcc4323ea08
5
5
  SHA512:
6
- metadata.gz: 9808bb66c09d3acecb944d1c1b37a3af17e46f758f6f500d5f618eecca798b3bf1e5b8bff415e23f9eb0d47804b672c1b509de2d89e77861dbc2fec2e88a8775
7
- data.tar.gz: fac35c5117774db5619fc82d4153f8ce0495dcb13e27fdba3b4726e211f6a155788f4b1a035987f4a083ec7035ea0b6429bb8d3549d03c8c64906d1325731f81
6
+ metadata.gz: d8a078fef78bc1e26fc28d90c6604e6a3a09aa05e3b716df2faea6d7423bb91a4de1c907eaa7d7e9c85b0d3019fa98257f6b499113f1987d646c902680bcdd66
7
+ data.tar.gz: a87e663b01668bdf114cba5eb2c2f9f55b453b4aae314b5ceceac78047574e1fc5917a46d97fddaf60cea2a64333c2b1f0901694daed5aade3beff7d6682c1b9
@@ -0,0 +1,40 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ - master
8
+ pull_request:
9
+
10
+ jobs:
11
+ test:
12
+ name: Ruby ${{ matrix.ruby }}
13
+ runs-on: ubuntu-22.04
14
+
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ ruby:
19
+ - "2.7"
20
+ - "3.0"
21
+ - "3.1"
22
+ - "3.2"
23
+ - "3.3"
24
+ - "3.4"
25
+
26
+ steps:
27
+ - name: Checkout
28
+ uses: actions/checkout@v4
29
+
30
+ - name: Set up Ruby
31
+ uses: ruby/setup-ruby@v1
32
+ with:
33
+ ruby-version: ${{ matrix.ruby }}
34
+ bundler-cache: true
35
+
36
+ - name: Run specs
37
+ run: bundle exec rspec
38
+
39
+ - name: Build gem
40
+ run: gem build num2words.gemspec
data/CHANGELOG.md CHANGED
@@ -11,6 +11,24 @@
11
11
 
12
12
  ---
13
13
 
14
+ ## [0.3.1] - 2026-07-04
15
+
16
+ ### Added
17
+ - Набор benchmark-проверок: `benchmark/converter_benchmark.rb` и `bundle exec rake benchmark`.
18
+ - Справочник API: `docs/api.md` и `docs/api.ru.md`.
19
+ - Документация по числовым лимитам: `docs/limits.md` и `docs/limits.ru.md`.
20
+ - Rake-задачи для разработки и релизной проверки:
21
+ - `bundle exec rake spec`
22
+ - `bundle exec rake build`
23
+ - `bundle exec rake release:check`
24
+ - `bundle exec rake locales`
25
+ - `bundle exec rake currencies`
26
+
27
+ ### Fixed
28
+ - Добавлена runtime-зависимость `bigdecimal` для совместимости с Ruby 3.4+.
29
+
30
+ ---
31
+
14
32
  ## [0.3.0] - 2026-06-27
15
33
 
16
34
  ### Added
data/README.md CHANGED
@@ -143,6 +143,16 @@ Num2words.to_currency(BigDecimal("21.05"), :ru)
143
143
  - `minor: :always | :nonzero | :never` — вывод младшей денежной единицы.
144
144
  - `code: :USD` — выбор валюты для `to_currency`.
145
145
 
146
+ Полная справка по API:
147
+
148
+ - [English](docs/api.md)
149
+ - [Русский](docs/api.ru.md)
150
+
151
+ Ограничения числовых разрядов:
152
+
153
+ - [English](docs/limits.md)
154
+ - [Русский](docs/limits.ru.md)
155
+
146
156
  ---
147
157
 
148
158
  ## Консоль
@@ -203,6 +213,11 @@ KZT MYR NOK PKR PLN RON RSD RUB SAR SEK THB TRY UAH USD VND
203
213
 
204
214
  После добавления локали стоит покрыть ее отдельным spec и добавить в `spec/currency_catalog_spec.rb`.
205
215
 
216
+ Подробная инструкция:
217
+
218
+ - [English](docs/locale_development.md)
219
+ - [Русский](docs/locale_development.ru.md)
220
+
206
221
  ---
207
222
 
208
223
  ## Тестирование
@@ -211,6 +226,17 @@ KZT MYR NOK PKR PLN RON RSD RUB SAR SEK THB TRY UAH USD VND
211
226
  bundle exec rspec
212
227
  ```
213
228
 
229
+ ## Rake tasks
230
+
231
+ ```bash
232
+ bundle exec rake spec
233
+ bundle exec rake build
234
+ bundle exec rake release:check
235
+ bundle exec rake locales
236
+ bundle exec rake currencies
237
+ bundle exec rake benchmark
238
+ ```
239
+
214
240
  ---
215
241
 
216
242
  ## Roadmap
data/Rakefile CHANGED
@@ -2,9 +2,45 @@
2
2
 
3
3
  require "bundler/gem_tasks"
4
4
  require "rspec/core/rake_task"
5
+ require "yaml"
5
6
 
6
- # таска для запуска тестов
7
7
  RSpec::Core::RakeTask.new(:spec)
8
8
 
9
- # по умолчанию запускаем тесты
9
+ task build: "build:clean"
10
+
11
+ namespace :build do
12
+ task :clean do
13
+ Dir.glob("num2words-*.gem").each { |file| File.delete(file) }
14
+ end
15
+ end
16
+
17
+ namespace :release do
18
+ desc "Run tests and build the gem"
19
+ task check: [:spec, :build]
20
+ end
21
+
22
+ desc "Print supported locale codes"
23
+ task :locales do
24
+ locales = Dir[File.expand_path("config/locales/*.yml", __dir__)]
25
+ .map { |file| File.basename(file, ".yml") }
26
+ .sort
27
+
28
+ puts locales.join(" ")
29
+ end
30
+
31
+ desc "Print supported currency codes"
32
+ task :currencies do
33
+ currencies = YAML.load_file(File.expand_path("config/locales/ru.yml", __dir__))
34
+ .dig("ru", "num2words", "currencies")
35
+ .keys
36
+ .sort
37
+
38
+ puts currencies.join(" ")
39
+ end
40
+
41
+ desc "Run converter benchmark"
42
+ task :benchmark do
43
+ sh "ruby benchmark/converter_benchmark.rb"
44
+ end
45
+
10
46
  task default: :spec
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "benchmark"
4
+ require_relative "../lib/num2words"
5
+
6
+ DEFAULT_ITERATIONS = 100
7
+
8
+ def all_locales
9
+ Dir[File.expand_path("../config/locales/*.yml", __dir__)]
10
+ .map { |file| File.basename(file, ".yml").to_sym }
11
+ .sort
12
+ end
13
+
14
+ def selected_locales
15
+ requested = ENV.fetch("LOCALES", "").split(",").map(&:strip).reject(&:empty?).map(&:to_sym)
16
+ return all_locales if requested.empty?
17
+
18
+ unknown = requested - all_locales
19
+ abort "Unknown locales: #{unknown.join(', ')}" unless unknown.empty?
20
+
21
+ requested
22
+ end
23
+
24
+ def iterations
25
+ Integer(ENV.fetch("ITERATIONS", DEFAULT_ITERATIONS.to_s))
26
+ rescue ArgumentError
27
+ abort "ITERATIONS must be an integer"
28
+ end
29
+
30
+ SCENARIOS = {
31
+ integer: ->(locale) { Num2words.to_words(123_456_789, locale) },
32
+ decimal: ->(locale) { Num2words.to_words("12345.67", locale) },
33
+ date: ->(locale) { Num2words.to_words("2024-08-21", locale) },
34
+ time: ->(locale) { Num2words.to_words("14:35:42", locale) },
35
+ datetime: ->(locale) { Num2words.to_words("2024-08-21 14:35:42", locale) },
36
+ currency: ->(locale) { Num2words.to_currency("12345.67", locale) }
37
+ }.freeze
38
+
39
+ def measure_scenarios(locales, iterations_count)
40
+ SCENARIOS.map do |name, callable|
41
+ calls = locales.size * iterations_count
42
+ elapsed = Benchmark.realtime do
43
+ iterations_count.times do
44
+ locales.each { |locale| callable.call(locale) }
45
+ end
46
+ end
47
+
48
+ [name, calls, elapsed]
49
+ end
50
+ end
51
+
52
+ def measure_slowest_locales(locales, iterations_count)
53
+ locales.map do |locale|
54
+ calls = SCENARIOS.size * iterations_count
55
+ elapsed = Benchmark.realtime do
56
+ iterations_count.times do
57
+ SCENARIOS.each_value { |callable| callable.call(locale) }
58
+ end
59
+ end
60
+
61
+ [locale, calls, elapsed]
62
+ end.sort_by { |(_, _, elapsed)| -elapsed }.first(10)
63
+ end
64
+
65
+ def print_table(headers, rows)
66
+ widths = headers.each_with_index.map do |header, index|
67
+ ([header.length] + rows.map { |row| row[index].to_s.length }).max
68
+ end
69
+
70
+ puts headers.each_with_index.map { |header, index| header.ljust(widths[index]) }.join(" ")
71
+ puts widths.map { |width| "-" * width }.join(" ")
72
+ rows.each do |row|
73
+ puts row.each_with_index.map { |value, index| value.to_s.ljust(widths[index]) }.join(" ")
74
+ end
75
+ end
76
+
77
+ locales = selected_locales
78
+ iterations_count = iterations
79
+
80
+ puts "num2words benchmark"
81
+ puts "Locales: #{locales.join(', ')}"
82
+ puts "Iterations per locale: #{iterations_count}"
83
+ puts
84
+
85
+ scenario_rows = measure_scenarios(locales, iterations_count).map do |name, calls, elapsed|
86
+ [name, calls, format("%.4f", elapsed), format("%.4f", (elapsed / calls) * 1000)]
87
+ end
88
+
89
+ print_table(["Scenario", "Calls", "Total, s", "ms/call"], scenario_rows)
90
+ puts
91
+
92
+ slowest_rows = measure_slowest_locales(locales, iterations_count).map do |locale, calls, elapsed|
93
+ [locale, calls, format("%.4f", elapsed), format("%.4f", (elapsed / calls) * 1000)]
94
+ end
95
+
96
+ print_table(["Slowest locale", "Calls", "Total, s", "ms/call"], slowest_rows)
data/docs/api.md ADDED
@@ -0,0 +1,422 @@
1
+ # API Reference
2
+
3
+ This document describes the public API of `num2words`.
4
+
5
+ ## Loading
6
+
7
+ ```ruby
8
+ require "num2words"
9
+ ```
10
+
11
+ This loads the module API and core extensions for supported Ruby classes.
12
+
13
+ ## Module API
14
+
15
+ ### `Num2words.to_words`
16
+
17
+ ```ruby
18
+ Num2words.to_words(value, locale = I18n.default_locale, only = nil, short = false, **options)
19
+ ```
20
+
21
+ Converts numbers, dates, times and date-times to words.
22
+
23
+ Examples:
24
+
25
+ ```ruby
26
+ Num2words.to_words(123, :ru)
27
+ # => "сто двадцать три"
28
+
29
+ Num2words.to_words("3,5", :ru)
30
+ # => "три целых пять десятых"
31
+
32
+ Num2words.to_words("2024-08-21", :en)
33
+ # => "the twenty-first of August, two thousand twenty four"
34
+
35
+ Num2words.to_words("14:35:42", :fr)
36
+ # => "quatorze heures trente cinq minutes quarante deux secondes"
37
+ ```
38
+
39
+ Keyword locale form is also supported:
40
+
41
+ ```ruby
42
+ Num2words.to_words(123, locale: :en)
43
+ # => "one hundred twenty three"
44
+ ```
45
+
46
+ ### `Num2words.to_currency`
47
+
48
+ ```ruby
49
+ Num2words.to_currency(amount, locale = I18n.default_locale, **options)
50
+ ```
51
+
52
+ Converts a numeric amount to words using the selected locale and currency.
53
+
54
+ Examples:
55
+
56
+ ```ruby
57
+ Num2words.to_currency("21,05", :ru)
58
+ # => "двадцать один рубль пять копеек"
59
+
60
+ Num2words.to_currency(12.50, :en, code: :EUR)
61
+ # => "twelve euros fifty cents"
62
+
63
+ Num2words.to_currency(BigDecimal("21.05"), :ru)
64
+ # => "двадцать один рубль пять копеек"
65
+ ```
66
+
67
+ ## Core Extensions
68
+
69
+ `num2words` adds convenience methods to common classes.
70
+
71
+ ### `Integer#to_words`
72
+
73
+ ```ruby
74
+ 123.to_words(:ru)
75
+ # => "сто двадцать три"
76
+ ```
77
+
78
+ ### `Integer#to_currency`
79
+
80
+ ```ruby
81
+ 123.to_currency(:en, code: :USD)
82
+ # => "one hundred twenty three dollars zero cents"
83
+ ```
84
+
85
+ ### `Float#to_words`
86
+
87
+ ```ruby
88
+ 45.67.to_words(:ru)
89
+ # => "сорок пять целых шестьдесят семь сотых"
90
+ ```
91
+
92
+ ### `Float#to_currency`
93
+
94
+ ```ruby
95
+ 12.5.to_currency(:ru, code: :USD)
96
+ # => "двенадцать долларов пятьдесят центов"
97
+ ```
98
+
99
+ ### `String#to_words`
100
+
101
+ ```ruby
102
+ "007".to_words(:en)
103
+ # => "seven"
104
+
105
+ "3,5".to_words(:ru)
106
+ # => "три целых пять десятых"
107
+
108
+ "2024-08-21 14:35:42".to_words(:ru)
109
+ # => "двадцать первое августа две тысячи двадцать четвёртого года, четырнадцать часов тридцать пять минут сорок две секунды"
110
+ ```
111
+
112
+ `String#to_currency` is not currently defined. Use `Num2words.to_currency("12.50", :en)` for string amounts.
113
+
114
+ ### `Date#to_words`
115
+
116
+ ```ruby
117
+ Date.new(2024, 8, 21).to_words(:ru)
118
+ # => "двадцать первое августа две тысячи двадцать четвёртого года"
119
+ ```
120
+
121
+ ### `Time#to_words`
122
+
123
+ ```ruby
124
+ Time.new(2024, 8, 21, 14, 35, 42).to_words(:en)
125
+ # => "fourteen hours thirty five minutes forty two seconds"
126
+ ```
127
+
128
+ ### `DateTime#to_words`
129
+
130
+ ```ruby
131
+ DateTime.parse("2024-08-21 14:35:42").to_words(:en)
132
+ # => "the twenty-first of August, two thousand twenty four at fourteen hours thirty five minutes forty two seconds"
133
+ ```
134
+
135
+ ## Supported Input Types
136
+
137
+ `to_words` supports:
138
+
139
+ - `Integer`
140
+ - `Float`
141
+ - `Date`
142
+ - `Time`
143
+ - `DateTime`
144
+ - integer strings, for example `"007"` or `"-42"`
145
+ - decimal strings with dot or comma, for example `"3.5"` or `"3,5"`
146
+ - date strings, for example `"2024-08-21"` or `"21.08.2024"`
147
+ - time strings, for example `"14:35"` or `"14:35:42"`
148
+ - date-time strings, for example `"2024-08-21 14:35:42"`
149
+
150
+ `to_currency` supports:
151
+
152
+ - `Integer`
153
+ - `Float`
154
+ - `String`
155
+ - `BigDecimal`
156
+
157
+ String currency amounts may use dot or comma as decimal separator.
158
+
159
+ ## Options
160
+
161
+ ### `locale`
162
+
163
+ Selects the locale.
164
+
165
+ ```ruby
166
+ Num2words.to_words(123, :ru)
167
+ Num2words.to_words(123, locale: :ru)
168
+ ```
169
+
170
+ If no locale is passed, `I18n.default_locale` is used.
171
+
172
+ ### `style`
173
+
174
+ Controls fractional output in `to_words`.
175
+
176
+ Supported values:
177
+
178
+ - `:fraction` - default fraction style.
179
+ - `:decimal` - reads fractional digits one by one when the locale supports decimal digit style.
180
+
181
+ ```ruby
182
+ Num2words.to_words(12.12, :en)
183
+ # => "twelve and twelve hundredths"
184
+
185
+ Num2words.to_words(12.12, :en, style: :decimal)
186
+ # => "twelve point one two"
187
+ ```
188
+
189
+ ### `joiner`
190
+
191
+ Controls the connector for fraction style.
192
+
193
+ Supported values:
194
+
195
+ - `:default`
196
+ - `:and`
197
+
198
+ ```ruby
199
+ Num2words.to_words(0.5, :ru)
200
+ # => "ноль целых пять десятых"
201
+
202
+ Num2words.to_words(0.5, :ru, joiner: :and)
203
+ # => "ноль и пять десятых"
204
+ ```
205
+
206
+ Invalid values raise `ArgumentError`.
207
+
208
+ ### `date_case`
209
+
210
+ Controls date day case for locales that support it.
211
+
212
+ Supported values:
213
+
214
+ - `:default`
215
+ - `:genitive`
216
+
217
+ ```ruby
218
+ Num2words.to_words("2024-08-21", :ru)
219
+ # => "двадцать первое августа две тысячи двадцать четвёртого года"
220
+
221
+ Num2words.to_words("2024-08-21", :ru, date_case: :genitive)
222
+ # => "двадцать первого августа две тысячи двадцать четвёртого года"
223
+ ```
224
+
225
+ Invalid values raise `ArgumentError`.
226
+
227
+ ### `short`
228
+
229
+ Enables short output for time or date-time values.
230
+
231
+ ```ruby
232
+ Num2words.to_words("14:35:42", :ru, short: true)
233
+ # => "четырнадцать часов тридцать пять минут"
234
+ ```
235
+
236
+ With date-time values, `short: true` returns a shortened date-time string unless `only` is used.
237
+
238
+ ### `only`
239
+
240
+ Selects a part of a date-time value.
241
+
242
+ ```ruby
243
+ Num2words.to_words("2024-08-21 14:35:42", :ru, only: :date)
244
+ # => "двадцать первое августа две тысячи двадцать четвёртого года"
245
+
246
+ Num2words.to_words("2024-08-21 14:35:42", :ru, only: :time)
247
+ # => "четырнадцать часов тридцать пять минут сорок две секунды"
248
+ ```
249
+
250
+ The legacy positional form is also supported:
251
+
252
+ ```ruby
253
+ "2024-08-21 14:35:42".to_words(:ru, :date)
254
+ "2024-08-21 14:35:42".to_words(:ru, :time)
255
+ ```
256
+
257
+ ### `format`
258
+
259
+ Used by date and time conversion.
260
+
261
+ For dates:
262
+
263
+ - `:default`
264
+ - `:nominative` when provided by locale data.
265
+ - `:short` returns numeric date format `DD.MM.YYYY`.
266
+
267
+ For times:
268
+
269
+ - `:default`
270
+ - `:hours_only`
271
+ - `:hours_minutes`
272
+ - `:hours_minutes_seconds`
273
+
274
+ ```ruby
275
+ Num2words.to_words("14:35:42", :ru, format: :hours_minutes)
276
+ # => "четырнадцать часов тридцать пять минут"
277
+ ```
278
+
279
+ Unsupported time formats raise `ArgumentError`.
280
+
281
+ ### `word_case`
282
+
283
+ Changes result casing.
284
+
285
+ Supported values:
286
+
287
+ - `:default`
288
+ - `:upper`
289
+ - `:downcase`
290
+ - `:capitalize`
291
+ - `:title`
292
+
293
+ ```ruby
294
+ Num2words.to_words(21, :en, word_case: :upper)
295
+ # => "TWENTY ONE"
296
+
297
+ Num2words.to_words(21, :en, word_case: :title)
298
+ # => "Twenty One"
299
+ ```
300
+
301
+ ### `feminine`
302
+
303
+ For locales with grammatical gender, selects feminine forms where supported.
304
+
305
+ ```ruby
306
+ Num2words.to_words(1, :ru, feminine: true)
307
+ # => "одна"
308
+ ```
309
+
310
+ ### `code`
311
+
312
+ Selects currency code in `to_currency`.
313
+
314
+ ```ruby
315
+ Num2words.to_currency(12.50, :ru, code: :USD)
316
+ # => "двенадцать долларов пятьдесят центов"
317
+ ```
318
+
319
+ If the currency is not available for the locale, `num2words` falls back to the locale default currency and may emit a warning depending on configuration.
320
+
321
+ ### `minor`
322
+
323
+ Controls minor currency unit output.
324
+
325
+ Supported values:
326
+
327
+ - `:always` - always include minor units.
328
+ - `:nonzero` - include minor units only when the minor value is greater than zero.
329
+ - `:never` - do not include minor units.
330
+
331
+ ```ruby
332
+ Num2words.to_currency(12, :ru)
333
+ # => "двенадцать рублей ноль копеек"
334
+
335
+ Num2words.to_currency(12, :ru, minor: :nonzero)
336
+ # => "двенадцать рублей"
337
+
338
+ Num2words.to_currency(12.50, :ru, minor: :never)
339
+ # => "двенадцать рублей"
340
+ ```
341
+
342
+ Invalid values raise `ArgumentError`.
343
+
344
+ ## Configuration
345
+
346
+ ### `Num2words.default_currency`
347
+
348
+ Gets or sets default currency.
349
+
350
+ ```ruby
351
+ Num2words.default_currency(:ru)
352
+ # => :RUB
353
+
354
+ Num2words.default_currency(:ru, :USD)
355
+ # => :USD
356
+ ```
357
+
358
+ ### `Num2words.available_currencies`
359
+
360
+ Returns currency codes available for a locale.
361
+
362
+ ```ruby
363
+ Num2words.available_currencies(:ru)
364
+ # => [:RUB, :USD, :EUR, ...]
365
+ ```
366
+
367
+ ### `Num2words.currency_warnings`
368
+
369
+ Controls warnings when requested currency is unavailable.
370
+
371
+ ```ruby
372
+ Num2words.currency_warnings = false
373
+ ```
374
+
375
+ ## Locales and Currencies
376
+
377
+ Supported locales:
378
+
379
+ ```text
380
+ ar be bg bn cs da de el en es et fa fi fr gu he hi hr hu id it ja kn ko
381
+ kz lt lv ml mr ms nb nl pa pl pt ro ru sk sl sr sv sw ta te th tr uk ur
382
+ vi zh
383
+ ```
384
+
385
+ Each completed locale supports the same 32 currency codes:
386
+
387
+ ```text
388
+ BDT BGN BRL BYN CNY CZK DKK EUR GBP HUF IDR ILS INR IRR JPY KES KRW
389
+ KZT MYR NOK PKR PLN RON RSD RUB SAR SEK THB TRY UAH USD VND
390
+ ```
391
+
392
+ ## Errors
393
+
394
+ Unsupported input type:
395
+
396
+ ```ruby
397
+ Num2words.to_words(Object.new, :en)
398
+ # raises ArgumentError: Unsupported input type: ...
399
+ ```
400
+
401
+ Unsupported options:
402
+
403
+ ```ruby
404
+ Num2words.to_words(0.5, :ru, joiner: :plus)
405
+ # raises ArgumentError: Unsupported joiner option: :plus
406
+
407
+ Num2words.to_currency(12, :ru, minor: :sometimes)
408
+ # raises ArgumentError: Unsupported minor option: :sometimes
409
+ ```
410
+
411
+ Unsupported currency amount:
412
+
413
+ ```ruby
414
+ Num2words.to_currency("abc", :en)
415
+ # raises ArgumentError: Unsupported currency amount: "abc"
416
+ ```
417
+
418
+ ## Development References
419
+
420
+ - [Numeric limits](limits.md)
421
+ - [Locale development guide](locale_development.md)
422
+ - [Russian locale development guide](locale_development.ru.md)