num2words 0.1.6 → 0.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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -0
  3. data/README.md +145 -135
  4. data/bin/console +2 -14
  5. data/config/locales/ar.yml +5 -1
  6. data/config/locales/be.yml +4 -0
  7. data/config/locales/bg.yml +4 -0
  8. data/config/locales/bn.yml +4 -0
  9. data/config/locales/cs.yml +6 -2
  10. data/config/locales/da.yml +4 -0
  11. data/config/locales/de.yml +8 -4
  12. data/config/locales/el.yml +4 -0
  13. data/config/locales/en.yml +4 -0
  14. data/config/locales/es.yml +4 -0
  15. data/config/locales/et.yml +9 -5
  16. data/config/locales/fa.yml +15 -11
  17. data/config/locales/fi.yml +8 -4
  18. data/config/locales/fr.yml +4 -0
  19. data/config/locales/gu.yml +4 -0
  20. data/config/locales/he.yml +6 -2
  21. data/config/locales/hi.yml +4 -0
  22. data/config/locales/hr.yml +8 -4
  23. data/config/locales/hu.yml +6 -2
  24. data/config/locales/id.yml +4 -0
  25. data/config/locales/it.yml +4 -0
  26. data/config/locales/ja.yml +10 -5
  27. data/config/locales/kn.yml +186 -10
  28. data/config/locales/ko.yml +184 -13
  29. data/config/locales/kz.yml +183 -17
  30. data/config/locales/lt.yml +184 -8
  31. data/config/locales/lv.yml +188 -12
  32. data/config/locales/ml.yml +189 -13
  33. data/config/locales/mr.yml +184 -8
  34. data/config/locales/ms.yml +184 -8
  35. data/config/locales/nb.yml +183 -17
  36. data/config/locales/nl.yml +184 -13
  37. data/config/locales/pa.yml +189 -13
  38. data/config/locales/pl.yml +184 -13
  39. data/config/locales/pt.yml +184 -13
  40. data/config/locales/ro.yml +183 -12
  41. data/config/locales/ru.yml +8 -0
  42. data/config/locales/sk.yml +184 -8
  43. data/config/locales/sl.yml +185 -9
  44. data/config/locales/sr.yml +186 -15
  45. data/config/locales/sv.yml +182 -13
  46. data/config/locales/sw.yml +184 -8
  47. data/config/locales/ta.yml +184 -8
  48. data/config/locales/te.yml +185 -9
  49. data/config/locales/th.yml +183 -12
  50. data/config/locales/tr.yml +183 -12
  51. data/config/locales/uk.yml +189 -18
  52. data/config/locales/ur.yml +187 -11
  53. data/config/locales/vi.yml +183 -12
  54. data/config/locales/zh.yml +191 -9
  55. data/exe/num2words-console +7 -0
  56. data/lib/num2words/console.rb +16 -0
  57. data/lib/num2words/converter.rb +195 -47
  58. data/lib/num2words/locales/ar.rb +130 -0
  59. data/lib/num2words/locales/be.rb +101 -0
  60. data/lib/num2words/locales/bg.rb +127 -0
  61. data/lib/num2words/locales/bn.rb +112 -0
  62. data/lib/num2words/locales/cs.rb +106 -0
  63. data/lib/num2words/locales/da.rb +97 -0
  64. data/lib/num2words/locales/de.rb +119 -0
  65. data/lib/num2words/locales/el.rb +143 -0
  66. data/lib/num2words/locales/en.rb +90 -0
  67. data/lib/num2words/locales/es.rb +185 -0
  68. data/lib/num2words/locales/et.rb +104 -0
  69. data/lib/num2words/locales/fa.rb +104 -0
  70. data/lib/num2words/locales/fi.rb +104 -0
  71. data/lib/num2words/locales/fr.rb +121 -0
  72. data/lib/num2words/locales/gu.rb +112 -0
  73. data/lib/num2words/locales/he.rb +130 -0
  74. data/lib/num2words/locales/hi.rb +112 -0
  75. data/lib/num2words/locales/hr.rb +117 -0
  76. data/lib/num2words/locales/hu.rb +111 -0
  77. data/lib/num2words/locales/id.rb +104 -0
  78. data/lib/num2words/locales/it.rb +142 -0
  79. data/lib/num2words/locales/ja.rb +126 -0
  80. data/lib/num2words/locales/kn.rb +122 -2
  81. data/lib/num2words/locales/ko.rb +158 -2
  82. data/lib/num2words/locales/kz.rb +137 -2
  83. data/lib/num2words/locales/lt.rb +146 -2
  84. data/lib/num2words/locales/lv.rb +148 -2
  85. data/lib/num2words/locales/ml.rb +142 -2
  86. data/lib/num2words/locales/mr.rb +142 -2
  87. data/lib/num2words/locales/ms.rb +134 -2
  88. data/lib/num2words/locales/nb.rb +153 -0
  89. data/lib/num2words/locales/nl.rb +142 -2
  90. data/lib/num2words/locales/pa.rb +122 -2
  91. data/lib/num2words/locales/pl.rb +149 -2
  92. data/lib/num2words/locales/pt.rb +164 -2
  93. data/lib/num2words/locales/ro.rb +165 -2
  94. data/lib/num2words/locales/ru.rb +100 -0
  95. data/lib/num2words/locales/sk.rb +124 -2
  96. data/lib/num2words/locales/sl.rb +154 -2
  97. data/lib/num2words/locales/sr.rb +141 -3
  98. data/lib/num2words/locales/sv.rb +141 -2
  99. data/lib/num2words/locales/sw.rb +133 -2
  100. data/lib/num2words/locales/ta.rb +142 -2
  101. data/lib/num2words/locales/te.rb +142 -2
  102. data/lib/num2words/locales/th.rb +133 -2
  103. data/lib/num2words/locales/tr.rb +141 -2
  104. data/lib/num2words/locales/uk.rb +134 -2
  105. data/lib/num2words/locales/ur.rb +130 -2
  106. data/lib/num2words/locales/vi.rb +142 -2
  107. data/lib/num2words/locales/zh.rb +172 -2
  108. data/lib/num2words/version.rb +1 -1
  109. data/num2words.gemspec +3 -3
  110. metadata +13 -10
  111. data/lib/num2words/locales/no.rb +0 -19
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "date"
4
4
  require "time"
5
+ require "bigdecimal"
5
6
 
6
7
  module Num2words
7
8
  class Converter
@@ -15,14 +16,19 @@ module Num2words
15
16
  style = opts[:style] || :fraction
16
17
  word_case = opts[:word_case] || :default
17
18
  date_format = opts[:format] || :default
19
+ date_case = opts[:date_case] || :default
20
+ joiner = opts[:joiner] || :default
21
+
22
+ validate_option!(:date_case, date_case, %i[default genitive])
23
+ validate_option!(:joiner, joiner, %i[default and])
18
24
 
19
25
  locale_data = Locales[locale]
20
26
 
21
27
  result = case detect_type(number)
22
- when :float then to_words_fractional(number, locale, feminine, locale_data, style: style)
28
+ when :float then to_words_fractional(number, locale, feminine, locale_data, style: style, joiner: joiner)
23
29
  when :integer then to_words_integer(number, locale, feminine, locale_data)
24
- when :datetime then to_words_datetime(number, locale, locale_data, format: date_format, only: type_only, short: type_short)
25
- when :date then to_words_date(number, locale, locale_data, format: date_format)
30
+ when :datetime then to_words_datetime(number, locale, locale_data, format: date_format, only: type_only, short: type_short, date_case: date_case)
31
+ when :date then to_words_date(number, locale, locale_data, format: date_format, date_case: date_case)
26
32
  when :time then to_words_time(number, locale, locale_data, short: type_short)
27
33
  else nil
28
34
  end
@@ -36,6 +42,9 @@ module Num2words
36
42
  locale = args.first.is_a?(Symbol) ? args.first : opts[:locale] || I18n.default_locale
37
43
  word_case = opts[:word_case] || :downcase
38
44
  currency = (opts[:code] || Num2words.default_currency(locale)).to_s.upcase.to_sym
45
+ minor = opts[:minor] || :always
46
+
47
+ validate_option!(:minor, minor, %i[always nonzero never])
39
48
 
40
49
  unless Num2words.available_currencies(locale).include?(currency)
41
50
  warn I18n.t("num2words.warnings.currency_not_available",
@@ -46,22 +55,46 @@ module Num2words
46
55
  currency_data = I18n.t("num2words.currencies.#{currency}", locale: locale) or
47
56
  raise ArgumentError, "Currency #{currency} not defined in locale #{locale}"
48
57
 
49
- major_value, minor_value = sprintf('%.2f', amount.abs).split('.').map(&:to_i)
58
+ decimal_amount = decimal_currency_amount(amount)
59
+ major_value, minor_value = format('%.2f', decimal_amount.abs).split('.').map(&:to_i)
60
+ major_feminine = locale_currency_major_feminine?(Locales[locale], currency)
61
+ minor_feminine = locale_currency_minor_feminine?(Locales[locale], currency)
50
62
 
51
63
  parts = [
52
- to_words(major_value, locale: locale),
53
- pluralize(major_value, *currency_data[:major_unit]),
54
- to_words(minor_value, locale: locale, feminine: true),
55
- pluralize(minor_value, *currency_data[:minor_unit])
64
+ locale_currency_number_words(Locales[locale], major_value, currency, unit: :major, locale: locale, feminine: major_feminine),
65
+ locale_pluralize(Locales[locale], major_value, currency_data[:major_unit])
56
66
  ]
57
67
 
58
- parts.unshift(Locales[locale]::GRAMMAR[:minus] || "minus") if amount.negative?
68
+ include_minor = minor == :always || (minor == :nonzero && minor_value.positive?)
69
+ if include_minor
70
+ parts.concat([
71
+ locale_currency_number_words(Locales[locale], minor_value, currency, unit: :minor, locale: locale, feminine: minor_feminine),
72
+ locale_pluralize(Locales[locale], minor_value, currency_data[:minor_unit])
73
+ ])
74
+ end
75
+
76
+ parts.unshift(locale_minus_word(Locales[locale])) if decimal_amount.negative?
59
77
 
60
- apply_case(parts.join(" ").strip, word_case)
78
+ apply_case(locale_join_currency_parts(Locales[locale], parts), word_case)
61
79
  end
62
80
 
63
81
  private
64
82
 
83
+ def validate_option!(name, value, allowed_values)
84
+ return if allowed_values.include?(value)
85
+
86
+ raise ArgumentError, "Unsupported #{name} option: #{value.inspect}"
87
+ end
88
+
89
+ def decimal_currency_amount(amount)
90
+ return amount if amount.is_a?(BigDecimal)
91
+
92
+ normalized_amount = amount.is_a?(String) ? amount.tr(",", ".") : amount.to_s
93
+ BigDecimal(normalized_amount)
94
+ rescue ArgumentError
95
+ raise ArgumentError, "Unsupported currency amount: #{amount.inspect}"
96
+ end
97
+
65
98
  def pluralize(number, singular, few, plural)
66
99
  number = number.abs
67
100
  return plural if (11..14).include?(number % 100)
@@ -73,10 +106,103 @@ module Num2words
73
106
  end
74
107
  end
75
108
 
109
+ def locale_pluralize(locale_data, number, forms)
110
+ return locale_data.pluralize(number, *forms) if locale_data.respond_to?(:pluralize)
111
+
112
+ pluralize(number, *forms)
113
+ end
114
+
115
+ def locale_currency_major_feminine?(locale_data, currency)
116
+ return locale_data.currency_major_feminine?(currency) if locale_data.respond_to?(:currency_major_feminine?)
117
+
118
+ false
119
+ end
120
+
121
+ def locale_currency_minor_feminine?(locale_data, currency)
122
+ return locale_data.currency_minor_feminine?(currency) if locale_data.respond_to?(:currency_minor_feminine?)
123
+
124
+ true
125
+ end
126
+
127
+ def locale_currency_number_words(locale_data, number, currency, unit:, locale:, feminine:)
128
+ if locale_data.respond_to?(:currency_number_words)
129
+ return locale_data.currency_number_words(number, currency, unit: unit)
130
+ end
131
+
132
+ to_words(number, locale: locale, feminine: feminine)
133
+ end
134
+
135
+ def locale_join_currency_parts(locale_data, parts)
136
+ return locale_data.join_currency_parts(parts) if locale_data.respond_to?(:join_currency_parts)
137
+
138
+ parts.join(" ").strip
139
+ end
140
+
141
+ def locale_time_unit_feminine?(locale_data, unit, default)
142
+ return locale_data.time_unit_feminine?(unit) if locale_data.respond_to?(:time_unit_feminine?)
143
+
144
+ default
145
+ end
146
+
147
+ def locale_time_number_words(locale_data, number, unit, locale, feminine)
148
+ if locale_data.respond_to?(:time_number_words)
149
+ return locale_data.time_number_words(number, unit: unit)
150
+ end
151
+
152
+ to_words_integer(number, locale, feminine, locale_data)
153
+ end
154
+
155
+ def locale_join_time_words(locale_data, number_words, unit_words)
156
+ return locale_data.join_time_words(number_words, unit_words) if locale_data.respond_to?(:join_time_words)
157
+
158
+ [number_words, unit_words].join(" ")
159
+ end
160
+
161
+ def locale_minus_word(locale_data)
162
+ return locale_data.minus_word if locale_data.respond_to?(:minus_word)
163
+
164
+ locale_data::GRAMMAR[:minus]
165
+ end
166
+
167
+ def locale_fraction_joiner(locale_data, joiner)
168
+ return locale_data.fraction_joiner(joiner) if locale_data.respond_to?(:fraction_joiner)
169
+
170
+ locale_data::GRAMMAR[:conjunction]
171
+ end
172
+
173
+ def locale_default_fraction_word(locale_data)
174
+ return locale_data.default_fraction_word if locale_data.respond_to?(:default_fraction_word)
175
+
176
+ locale_data::GRAMMAR[:default_fraction]
177
+ end
178
+
179
+ def locale_decimal_separator_word(locale_data)
180
+ return locale_data.decimal_separator_word if locale_data.respond_to?(:decimal_separator_word)
181
+
182
+ locale_data::GRAMMAR[:decimal_separator]
183
+ end
184
+
185
+ def locale_join_decimal_words(locale_data, words)
186
+ return locale_data.join_decimal_words(words) if locale_data.respond_to?(:join_decimal_words)
187
+
188
+ words.compact.reject(&:empty?).join(" ")
189
+ end
190
+
191
+ def locale_join_fraction_words(locale_data, words)
192
+ return locale_data.join_fraction_words(words) if locale_data.respond_to?(:join_fraction_words)
193
+
194
+ words.reject(&:empty?).join(" ")
195
+ end
196
+
76
197
  # n — 0..999, scale_idx — индекс разряда (0 — единицы, 1 — тысячи, ...)
77
198
  # feminine: true — использовать женский род для единиц (нужно для тысяч/копеек)
78
- def triple_to_words(n, scale_idx, local_data, feminine: false)
199
+ def triple_to_words(n, scale_idx, local_data, feminine: false, locale: nil)
79
200
  return [] if n.zero?
201
+
202
+ if local_data.respond_to?(:triple_to_words)
203
+ return local_data.triple_to_words(n, scale_idx, feminine: feminine)
204
+ end
205
+
80
206
  words = []
81
207
 
82
208
  words << local_data::HUNDREDS[n / 100] if n >= 100
@@ -90,19 +216,21 @@ module Num2words
90
216
  words << (feminine ? local_data::ONES_FEM[ones] : local_data::ONES_MASC[ones]) if ones.positive?
91
217
  end
92
218
 
93
- words << pluralize(n, *local_data::SCALES[scale_idx]) unless scale_idx.zero?
219
+ words << locale_pluralize(local_data, n, local_data::SCALES[scale_idx]) unless scale_idx.zero?
94
220
  words.compact
95
221
  end
96
222
 
97
- def to_words_fractional(number, locale, feminine, locale_data, style: :fraction)
98
- minus_word = locale_data::GRAMMAR[:minus] || "minus"
99
- conjunction_word = locale_data::GRAMMAR[:conjunction] || "and"
100
- default_fraction = locale_data::GRAMMAR[:default_fraction] || "parts"
223
+ def to_words_fractional(number, locale, feminine, locale_data, style: :fraction, joiner: :default)
224
+ minus_word = locale_minus_word(locale_data)
225
+ conjunction_word = locale_fraction_joiner(locale_data, joiner)
226
+ default_fraction = locale_default_fraction_word(locale_data)
101
227
  fractions_data = locale_data::FRACTIONS || {}
102
228
 
103
- sign_word = number.negative? ? minus_word : ""
229
+ negative = number.is_a?(String) ? number.start_with?("-") : number.negative?
230
+ sign_word = negative ? minus_word : ""
104
231
 
105
- integer_string, fraction_string = number.abs.to_s.split('.', 2)
232
+ absolute_number = number.is_a?(String) ? number.sub(/\A-/, "").tr(",", ".") : number.abs.to_s
233
+ integer_string, fraction_string = absolute_number.split('.', 2)
106
234
  integer_value = integer_string.to_i
107
235
 
108
236
  return to_words_integer(integer_value, locale, feminine, locale_data) if fraction_string.to_i.zero?
@@ -113,23 +241,29 @@ module Num2words
113
241
 
114
242
  integer_words = to_words_integer(integer_value, locale, feminine, locale_data)
115
243
 
116
- if locale.to_sym == :en && style == :decimal
117
- fraction_digits = fraction_string.chars.map { |d| to_words_integer(d.to_i, locale, feminine, locale_data) }
118
- full_string = [sign_word, integer_words, "point", fraction_digits.join(" ")].reject(&:empty?).join(" ")
119
- return full_string
244
+ if style == :decimal && locale_data.respond_to?(:decimal_fraction_words)
245
+ fraction_digits = locale_data.decimal_fraction_words(fraction_string)
246
+ return locale_join_decimal_words(locale_data, [sign_word, integer_words, locale_decimal_separator_word(locale_data), fraction_digits])
120
247
  end
121
248
 
122
- numerator_words = to_words_integer(numerator, locale, (locale.to_sym == :ru ? true : feminine), locale_data)
249
+ numerator_feminine = locale_data.respond_to?(:fraction_numerator_feminine?) ? locale_data.fraction_numerator_feminine? : feminine
250
+ numerator_words = to_words_integer(numerator, locale, numerator_feminine, locale_data)
123
251
 
124
252
  denom_forms = fractions_data[denominator] || fractions_data[denominator.to_s] # массив склонений
125
- denominator_words = denom_forms.is_a?(Array) ? pluralize(numerator, *denom_forms) : default_fraction
253
+ denominator_words = denom_forms.is_a?(Array) ? locale_pluralize(locale_data, numerator, denom_forms) : default_fraction
126
254
 
127
- [sign_word, integer_words, conjunction_word, numerator_words, denominator_words].reject(&:empty?).join(" ")
255
+ locale_join_fraction_words(locale_data, [sign_word, integer_words, conjunction_word, numerator_words, denominator_words])
128
256
  end
129
257
 
130
258
  def to_words_integer(number, locale, feminine, locale_data)
259
+ return locale_data.integer_to_words(number, feminine: feminine) if locale_data.respond_to?(:integer_to_words)
260
+
131
261
  integer_value = Integer(number)
132
262
 
263
+ minus_word = locale_minus_word(locale_data)
264
+ negative = integer_value.negative?
265
+ integer_value = integer_value.abs
266
+
133
267
  return (feminine ? locale_data::ONES_FEM[0] : locale_data::ONES_MASC[0]) if integer_value.zero?
134
268
 
135
269
  groups = integer_value.to_s
@@ -139,14 +273,15 @@ module Num2words
139
273
  words = []
140
274
  groups.each_with_index do |group_value, index|
141
275
  scale_index = groups.size - index - 1
142
- group_feminine = (scale_index == 1) || feminine
143
- words.concat triple_to_words(group_value, scale_index, locale_data, feminine: group_feminine)
276
+ group_feminine = (locale_data.respond_to?(:feminine_group?) && locale_data.feminine_group?(scale_index)) || feminine
277
+ words.concat triple_to_words(group_value, scale_index, locale_data, feminine: group_feminine, locale: locale)
144
278
  end
145
279
 
280
+ words.unshift(minus_word) if negative
146
281
  words.join(" ")
147
282
  end
148
283
 
149
- def to_words_date(date, locale, locale_data, format: :default)
284
+ def to_words_date(date, locale, locale_data, format: :default, date_case: :default)
150
285
  date = Date.parse(date.to_s) unless date.is_a?(Date)
151
286
 
152
287
  day, month, year = [date.day, date.month, date.year]
@@ -159,9 +294,18 @@ module Num2words
159
294
  raise ArgumentError, "Months not found for locale #{locale}" unless months
160
295
  raise ArgumentError, "Template not found for locale #{locale}" unless template
161
296
 
162
- day_words = to_words_ordinal(day, locale, format, locale_data, gender: :neuter)
297
+ day_gender = date_case.to_sym == :genitive ? :masculine : :neuter
298
+ day_words = if locale_data.respond_to?(:date_day)
299
+ locale_data.date_day(day, format: format, date_case: date_case)
300
+ else
301
+ to_words_ordinal(day, locale, format, locale_data, gender: day_gender)
302
+ end
163
303
  month_words = months[month - 1]
164
- year_words = to_words_ordinal(year, locale, format, locale_data)
304
+ year_words = if locale_data.respond_to?(:date_year)
305
+ locale_data.date_year(year, format: format)
306
+ else
307
+ to_words_ordinal(year, locale, format, locale_data)
308
+ end
165
309
 
166
310
  template % { day: day_words, month: month_words, year: year_words }
167
311
  end
@@ -173,7 +317,7 @@ module Num2words
173
317
  gender_data = ordinals[gender] || ordinals[:masculine]
174
318
  raise ArgumentError, "Gender #{gender} not found for locale #{locale}, format #{format}" unless gender_data
175
319
 
176
- return gender_data[value - 1] if gender_data[value]
320
+ return gender_data[value - 1] if value.between?(1, gender_data.length)
177
321
 
178
322
  if value > 31
179
323
  thousands = (value / 100) * 100
@@ -197,18 +341,22 @@ module Num2words
197
341
  words = locale_data::TIME[:words]
198
342
  template = locale_data::TIME_TEMPLATE
199
343
 
200
- hours = [
201
- to_words_integer(time.hour, locale, false, locale_data),
202
- pluralize(time.hour, *words[:hour])
203
- ].join(" ")
204
- minutes = [
205
- to_words_integer(time.min, locale, true, locale_data),
206
- pluralize(time.min, *words[:minute])
207
- ].join(" ")
208
- seconds = [
209
- to_words_integer(time.sec, locale, true, locale_data),
210
- pluralize(time.sec, *words[:second])
211
- ].join(" ")
344
+ hour_feminine = locale_time_unit_feminine?(locale_data, :hour, false)
345
+ minute_feminine = locale_time_unit_feminine?(locale_data, :minute, true)
346
+ second_feminine = locale_time_unit_feminine?(locale_data, :second, true)
347
+
348
+ hours = locale_join_time_words(locale_data,
349
+ locale_time_number_words(locale_data, time.hour, :hour, locale, hour_feminine),
350
+ locale_pluralize(locale_data, time.hour, words[:hour])
351
+ )
352
+ minutes = locale_join_time_words(locale_data,
353
+ locale_time_number_words(locale_data, time.min, :minute, locale, minute_feminine),
354
+ locale_pluralize(locale_data, time.min, words[:minute])
355
+ )
356
+ seconds = locale_join_time_words(locale_data,
357
+ locale_time_number_words(locale_data, time.sec, :second, locale, second_feminine),
358
+ locale_pluralize(locale_data, time.sec, words[:second])
359
+ )
212
360
 
213
361
  format = if short
214
362
  time.min.zero? && time.sec.zero? ? :hours_only : :hours_minutes
@@ -228,13 +376,13 @@ module Num2words
228
376
  end
229
377
  end
230
378
 
231
- def to_words_datetime(datetime, locale, locale_data, format: :default, only: nil, short: false)
379
+ def to_words_datetime(datetime, locale, locale_data, format: :default, only: nil, short: false, date_case: :default)
232
380
  datetime = DateTime.parse(datetime) if datetime.is_a?(String)
233
381
 
234
382
  date_format = short && only == :date ? :short : format
235
383
  time_format = short && only == :time ? :short : :default
236
384
 
237
- date_part = to_words_date(datetime.to_date, locale, locale_data, format: date_format)
385
+ date_part = to_words_date(datetime.to_date, locale, locale_data, format: date_format, date_case: date_case)
238
386
  time_part = to_words_time(datetime.to_time, locale, locale_data, format: time_format, short: short)
239
387
 
240
388
  return date_part if only == :date
@@ -260,12 +408,12 @@ module Num2words
260
408
  case value
261
409
  when Integer then :integer
262
410
  when Float then :float
411
+ when DateTime then :datetime
263
412
  when Date then :date
264
413
  when Time then :time
265
- when DateTime then :datetime
266
414
  when String
267
415
  return :integer if value.match?(/\A-?\d+\z/)
268
- return :float if value.match?(/\A-?\d+\.\d+\z/)
416
+ return :float if value.match?(/\A-?\d+[\.,]\d+\z/)
269
417
  return :time if value.match?(/\A\d{1,2}:\d{2}(:\d{2})?\z/)
270
418
 
271
419
  # Форматы даты
@@ -20,6 +20,136 @@ module Num2words
20
20
  DATETIME_TEMPLATE = I18n.t("num2words.datetime.template", locale: :ar)
21
21
 
22
22
  ORDINALS = I18n.t("num2words.numbers.ordinals", locale: :ar)
23
+
24
+ module_function
25
+
26
+ def integer_to_words(number, feminine: false)
27
+ integer_value = Integer(number)
28
+ negative = integer_value.negative?
29
+ integer_value = integer_value.abs
30
+
31
+ return (feminine ? ONES_FEM[0] : ONES_MASC[0]) if integer_value.zero?
32
+
33
+ groups = integer_value.to_s
34
+ .chars.reverse.each_slice(3).map(&:reverse)
35
+ .map(&:join).map!(&:to_i).reverse
36
+
37
+ words = []
38
+ groups.each_with_index do |group_value, index|
39
+ next if group_value.zero?
40
+
41
+ scale_idx = groups.size - index - 1
42
+ words << scale_group_to_words(group_value, scale_idx, feminine: feminine && scale_idx.zero?)
43
+ end
44
+
45
+ result = words.join(" و")
46
+ negative ? [minus_word, result].join(" ") : result
47
+ end
48
+
49
+ def triple_to_words(number, scale_idx, feminine: false)
50
+ scale_group_to_words(number, scale_idx, feminine: feminine).split
51
+ end
52
+
53
+ def scale_group_to_words(number, scale_idx, feminine: false)
54
+ return under_thousand(number, feminine: feminine) if scale_idx.zero?
55
+
56
+ scale_forms = SCALES[scale_idx]
57
+ case number
58
+ when 1
59
+ scale_forms[0]
60
+ when 2
61
+ scale_forms[1]
62
+ else
63
+ scale_form = number.between?(3, 10) ? scale_forms[2] : scale_forms[0]
64
+ [under_thousand(number), scale_form].join(" ")
65
+ end
66
+ end
67
+
68
+ def under_thousand(number, feminine: false)
69
+ hundreds = number / 100
70
+ rest = number % 100
71
+
72
+ return under_hundred(rest, feminine: feminine) if hundreds.zero?
73
+ return HUNDREDS[hundreds] if rest.zero?
74
+
75
+ [HUNDREDS[hundreds], under_hundred(rest, feminine: feminine)].join(" و")
76
+ end
77
+
78
+ def under_hundred(number, feminine: false)
79
+ ones_data = feminine ? ONES_FEM : ONES_MASC
80
+
81
+ return ones_data[number] if number < 10
82
+ return TEENS[number - 10] if number < 20
83
+
84
+ tens = number / 10
85
+ ones = number % 10
86
+
87
+ return TENS[tens] if ones.zero?
88
+
89
+ [ones_data[ones], TENS[tens]].join(" و")
90
+ end
91
+
92
+ def minus_word
93
+ GRAMMAR[:minus]
94
+ end
95
+
96
+ def fraction_joiner(joiner)
97
+ joiner.to_sym == :and ? "و" : GRAMMAR[:conjunction]
98
+ end
99
+
100
+ def default_fraction_word
101
+ GRAMMAR[:default_fraction]
102
+ end
103
+
104
+ def decimal_separator_word
105
+ GRAMMAR[:conjunction]
106
+ end
107
+
108
+ def decimal_fraction_words(fraction_string)
109
+ fraction_string.chars.map { |digit| integer_to_words(digit.to_i) }.join(" ")
110
+ end
111
+
112
+ def join_fraction_words(words)
113
+ parts = words.reject(&:empty?)
114
+ joiner_index = parts.index("و")
115
+
116
+ if joiner_index && parts[joiner_index + 1]
117
+ parts[joiner_index + 1] = "و#{parts[joiner_index + 1]}"
118
+ parts.delete_at(joiner_index)
119
+ end
120
+
121
+ parts.join(" ")
122
+ end
123
+
124
+ def fraction_numerator_feminine?
125
+ false
126
+ end
127
+
128
+ def date_day(day, format:, date_case:)
129
+ ordinal(day, format)
130
+ end
131
+
132
+ def date_year(year, format:)
133
+ integer_to_words(year)
134
+ end
135
+
136
+ def ordinal(value, format, gender: :masculine)
137
+ ordinals = ORDINALS[format] || ORDINALS[:default]
138
+ gender_data = ordinals[gender] || ordinals[:masculine]
139
+
140
+ return gender_data[value - 1] if value.between?(1, gender_data.length)
141
+
142
+ integer_to_words(value)
143
+ end
144
+
145
+ def pluralize(number, singular, dual, plural)
146
+ number = number.abs
147
+ return singular if number == 1
148
+ return dual if number == 2
149
+ return plural if number.zero? || number.between?(3, 10)
150
+
151
+ singular
152
+ end
23
153
  end
24
154
 
25
155
  register :ar, AR
@@ -20,6 +20,107 @@ module Num2words
20
20
  DATETIME_TEMPLATE = I18n.t("num2words.datetime.template", locale: :be)
21
21
 
22
22
  ORDINALS = I18n.t("num2words.numbers.ordinals", locale: :be)
23
+
24
+ module_function
25
+
26
+ def feminine_group?(scale_idx)
27
+ scale_idx == 1
28
+ end
29
+
30
+ def fraction_numerator_feminine?
31
+ true
32
+ end
33
+
34
+ def minus_word
35
+ GRAMMAR[:minus]
36
+ end
37
+
38
+ def fraction_joiner(joiner)
39
+ joiner.to_sym == :and ? "і" : GRAMMAR[:conjunction]
40
+ end
41
+
42
+ def default_fraction_word
43
+ GRAMMAR[:default_fraction]
44
+ end
45
+
46
+ def triple_to_words(number, scale_idx, feminine: false)
47
+ words = []
48
+
49
+ words << HUNDREDS[number / 100] if number >= 100
50
+ rest = number % 100
51
+
52
+ if rest.between?(10, 19)
53
+ words << TEENS[rest - 10]
54
+ else
55
+ words << TENS[rest / 10] if rest >= 20
56
+ ones = rest % 10
57
+ words << (feminine ? ONES_FEM[ones] : ONES_MASC[ones]) if ones.positive?
58
+ end
59
+
60
+ words << pluralize(number, *SCALES[scale_idx]) unless scale_idx.zero?
61
+ words.compact
62
+ end
63
+
64
+ def date_day(day, format:, date_case:)
65
+ return ordinal(day, :default, gender: :masculine) if date_case.to_sym == :genitive
66
+
67
+ ordinal(day, :nominative, gender: :neuter)
68
+ end
69
+
70
+ def date_year(year, format:)
71
+ ordinal(year, :default)
72
+ end
73
+
74
+ def ordinal(value, format, gender: :masculine)
75
+ ordinals = ORDINALS[format]
76
+ gender_data = ordinals[gender] || ordinals[:masculine]
77
+
78
+ return gender_data[value - 1] if value.between?(1, gender_data.length)
79
+
80
+ if value > 31
81
+ thousands = (value / 100) * 100
82
+ last_two = value % 100
83
+ base_year = cardinal(thousands)
84
+ last_ordinal = gender_data[last_two - 1] || cardinal(last_two)
85
+
86
+ return [base_year, last_ordinal].join(" ")
87
+ end
88
+
89
+ cardinal(value)
90
+ end
91
+
92
+ def cardinal(number, feminine: false)
93
+ integer_value = Integer(number)
94
+ negative = integer_value.negative?
95
+ integer_value = integer_value.abs
96
+
97
+ return (feminine ? ONES_FEM[0] : ONES_MASC[0]) if integer_value.zero?
98
+
99
+ groups = integer_value.to_s
100
+ .chars.reverse.each_slice(3).map(&:reverse)
101
+ .map(&:join).map!(&:to_i).reverse
102
+
103
+ words = []
104
+ groups.each_with_index do |group_value, index|
105
+ scale_idx = groups.size - index - 1
106
+ group_feminine = feminine_group?(scale_idx) || feminine
107
+ words.concat triple_to_words(group_value, scale_idx, feminine: group_feminine)
108
+ end
109
+
110
+ words.unshift(minus_word) if negative
111
+ words.join(" ")
112
+ end
113
+
114
+ def pluralize(number, singular, few, plural)
115
+ number = number.abs
116
+ return plural if (11..14).include?(number % 100)
117
+
118
+ case number % 10
119
+ when 1 then singular
120
+ when 2..4 then few
121
+ else plural
122
+ end
123
+ end
23
124
  end
24
125
 
25
126
  register :be, BE