num2words 0.3.0 → 0.4.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 +4 -4
- data/.github/workflows/ci.yml +40 -0
- data/CHANGELOG.md +39 -0
- data/Gemfile +1 -0
- data/README.md +83 -0
- data/Rakefile +38 -2
- data/benchmark/converter_benchmark.rb +96 -0
- data/docs/api.md +478 -0
- data/docs/api.ru.md +478 -0
- data/docs/limits.md +140 -0
- data/docs/limits.ru.md +140 -0
- data/docs/locale_development.md +241 -0
- data/docs/locale_development.ru.md +241 -0
- data/docs/rails.md +130 -0
- data/docs/rails.ru.md +130 -0
- data/lib/num2words/config.rb +44 -0
- data/lib/num2words/converter.rb +2 -1
- data/lib/num2words/rails/helpers.rb +27 -0
- data/lib/num2words/rails/validators/currency_validator.rb +30 -0
- data/lib/num2words/rails/validators/locale_validator.rb +22 -0
- data/lib/num2words/railtie.rb +27 -0
- data/lib/num2words/version.rb +1 -1
- data/lib/num2words.rb +2 -0
- data/num2words.gemspec +1 -0
- metadata +30 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 288098773139982ac980a108f2bde3e3220c7b090cadd82cf8bada24053cce84
|
|
4
|
+
data.tar.gz: 0103cff58a94f52fd255aec76b83d4d487cd3767a7861ea25b2aff44c334797b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9570c86cb884cd7abc6c8f16df6946d85ab8292c38a05a8567b48179a1aa04fdc3d4ecfb8252f358c35bcac4d093c5f236c04a63b16009ee04487b47d5fad3d
|
|
7
|
+
data.tar.gz: 613f2887655f9b5dcd1d9dcf927bf6d368337fa95abe2f2e7b909a87df7caf8d5ea88fda30e167ab22f9b624ef20bc68371ed916a52d239ea3156c9f6ef09bdb
|
|
@@ -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,45 @@
|
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
## [0.4.0] - 2026-07-05
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Опциональные Rails helpers и Railtie:
|
|
18
|
+
- `number_to_words`
|
|
19
|
+
- `currency_to_words`
|
|
20
|
+
- `date_to_words`
|
|
21
|
+
- `time_to_words`
|
|
22
|
+
- `datetime_to_words`
|
|
23
|
+
- Интеграционные specs для проверки Railtie в минимальном Rails-приложении.
|
|
24
|
+
- Подключение Rails helpers в controller helper context.
|
|
25
|
+
- Валидатор ActiveModel `num2words_currency` для проверки валюты по локали.
|
|
26
|
+
- Валидатор ActiveModel `num2words_locale` для проверки поддерживаемой локали.
|
|
27
|
+
- Публичный metadata API `Num2words.available_locales`.
|
|
28
|
+
- Публичный metadata API `Num2words.currency_available?`.
|
|
29
|
+
- Публичный metadata API `Num2words.currency_info`.
|
|
30
|
+
- Публичный metadata API `Num2words.default_currency_info`.
|
|
31
|
+
- Документация по Rails-интеграции: `docs/rails.md` и `docs/rails.ru.md`.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## [0.3.1] - 2026-07-04
|
|
36
|
+
|
|
37
|
+
### Added
|
|
38
|
+
- Набор benchmark-проверок: `benchmark/converter_benchmark.rb` и `bundle exec rake benchmark`.
|
|
39
|
+
- Справочник API: `docs/api.md` и `docs/api.ru.md`.
|
|
40
|
+
- Документация по числовым лимитам: `docs/limits.md` и `docs/limits.ru.md`.
|
|
41
|
+
- Rake-задачи для разработки и релизной проверки:
|
|
42
|
+
- `bundle exec rake spec`
|
|
43
|
+
- `bundle exec rake build`
|
|
44
|
+
- `bundle exec rake release:check`
|
|
45
|
+
- `bundle exec rake locales`
|
|
46
|
+
- `bundle exec rake currencies`
|
|
47
|
+
|
|
48
|
+
### Fixed
|
|
49
|
+
- Добавлена runtime-зависимость `bigdecimal` для совместимости с Ruby 3.4+.
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
14
53
|
## [0.3.0] - 2026-06-27
|
|
15
54
|
|
|
16
55
|
### Added
|
data/Gemfile
CHANGED
data/README.md
CHANGED
|
@@ -14,6 +14,9 @@ Ruby-гем для преобразования чисел, дробей, ден
|
|
|
14
14
|
- Даты, время и дата-время.
|
|
15
15
|
- Регистры вывода: `word_case: :upper | :downcase | :capitalize | :title`.
|
|
16
16
|
- Удобные методы для `Integer`, `Float`, `String`, `Date`, `Time`, `DateTime`.
|
|
17
|
+
- Опциональные Rails helpers и Railtie.
|
|
18
|
+
- ActiveModel validators для локалей и валют.
|
|
19
|
+
- Metadata API для локалей и валют.
|
|
17
20
|
- Интерактивная консоль `num2words-console`.
|
|
18
21
|
|
|
19
22
|
---
|
|
@@ -143,6 +146,70 @@ Num2words.to_currency(BigDecimal("21.05"), :ru)
|
|
|
143
146
|
- `minor: :always | :nonzero | :never` — вывод младшей денежной единицы.
|
|
144
147
|
- `code: :USD` — выбор валюты для `to_currency`.
|
|
145
148
|
|
|
149
|
+
Полная справка по API:
|
|
150
|
+
|
|
151
|
+
- [English](docs/api.md)
|
|
152
|
+
- [Русский](docs/api.ru.md)
|
|
153
|
+
|
|
154
|
+
Ограничения числовых разрядов:
|
|
155
|
+
|
|
156
|
+
- [English](docs/limits.md)
|
|
157
|
+
- [Русский](docs/limits.ru.md)
|
|
158
|
+
|
|
159
|
+
Rails-интеграция:
|
|
160
|
+
|
|
161
|
+
- [English](docs/rails.md)
|
|
162
|
+
- [Русский](docs/rails.ru.md)
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Rails
|
|
167
|
+
|
|
168
|
+
Если Rails доступен, helpers подключаются автоматически через Railtie.
|
|
169
|
+
|
|
170
|
+
```erb
|
|
171
|
+
<%= number_to_words(123, locale: :ru) %>
|
|
172
|
+
<%= currency_to_words(@payment.amount, locale: :ru, code: @payment.currency) %>
|
|
173
|
+
<%= date_to_words(Date.current, locale: I18n.locale) %>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Также доступны ActiveModel validators:
|
|
177
|
+
|
|
178
|
+
```ruby
|
|
179
|
+
class Payment < ApplicationRecord
|
|
180
|
+
validates :currency, num2words_currency: { locale: ->(payment) { payment.locale } }
|
|
181
|
+
validates :locale, num2words_locale: true
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## Metadata API
|
|
188
|
+
|
|
189
|
+
```ruby
|
|
190
|
+
Num2words.available_locales
|
|
191
|
+
# => [:ar, :be, :bg, :bn, :cs, ...]
|
|
192
|
+
|
|
193
|
+
Num2words.currency_available?(:ru, :RUB)
|
|
194
|
+
# => true
|
|
195
|
+
|
|
196
|
+
Num2words.currency_info(:ru, :RUB)
|
|
197
|
+
# => {
|
|
198
|
+
# code: :RUB,
|
|
199
|
+
# major_unit: ["рубль", "рубля", "рублей"],
|
|
200
|
+
# minor_unit: ["копейка", "копейки", "копеек"],
|
|
201
|
+
# symbol: "₽"
|
|
202
|
+
# }
|
|
203
|
+
|
|
204
|
+
Num2words.default_currency_info(:ru)
|
|
205
|
+
# => {
|
|
206
|
+
# code: :RUB,
|
|
207
|
+
# major_unit: ["рубль", "рубля", "рублей"],
|
|
208
|
+
# minor_unit: ["копейка", "копейки", "копеек"],
|
|
209
|
+
# symbol: "₽"
|
|
210
|
+
# }
|
|
211
|
+
```
|
|
212
|
+
|
|
146
213
|
---
|
|
147
214
|
|
|
148
215
|
## Консоль
|
|
@@ -203,6 +270,11 @@ KZT MYR NOK PKR PLN RON RSD RUB SAR SEK THB TRY UAH USD VND
|
|
|
203
270
|
|
|
204
271
|
После добавления локали стоит покрыть ее отдельным spec и добавить в `spec/currency_catalog_spec.rb`.
|
|
205
272
|
|
|
273
|
+
Подробная инструкция:
|
|
274
|
+
|
|
275
|
+
- [English](docs/locale_development.md)
|
|
276
|
+
- [Русский](docs/locale_development.ru.md)
|
|
277
|
+
|
|
206
278
|
---
|
|
207
279
|
|
|
208
280
|
## Тестирование
|
|
@@ -211,6 +283,17 @@ KZT MYR NOK PKR PLN RON RSD RUB SAR SEK THB TRY UAH USD VND
|
|
|
211
283
|
bundle exec rspec
|
|
212
284
|
```
|
|
213
285
|
|
|
286
|
+
## Rake tasks
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
bundle exec rake spec
|
|
290
|
+
bundle exec rake build
|
|
291
|
+
bundle exec rake release:check
|
|
292
|
+
bundle exec rake locales
|
|
293
|
+
bundle exec rake currencies
|
|
294
|
+
bundle exec rake benchmark
|
|
295
|
+
```
|
|
296
|
+
|
|
214
297
|
---
|
|
215
298
|
|
|
216
299
|
## 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)
|