num2words 0.2.0 → 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 (108) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +113 -144
  4. data/config/locales/ar.yml +5 -1
  5. data/config/locales/be.yml +4 -0
  6. data/config/locales/bg.yml +4 -0
  7. data/config/locales/bn.yml +4 -0
  8. data/config/locales/cs.yml +6 -2
  9. data/config/locales/da.yml +4 -0
  10. data/config/locales/de.yml +8 -4
  11. data/config/locales/el.yml +4 -0
  12. data/config/locales/en.yml +4 -0
  13. data/config/locales/es.yml +4 -0
  14. data/config/locales/et.yml +9 -5
  15. data/config/locales/fa.yml +15 -11
  16. data/config/locales/fi.yml +8 -4
  17. data/config/locales/fr.yml +4 -0
  18. data/config/locales/gu.yml +4 -0
  19. data/config/locales/he.yml +6 -2
  20. data/config/locales/hi.yml +4 -0
  21. data/config/locales/hr.yml +8 -4
  22. data/config/locales/hu.yml +6 -2
  23. data/config/locales/id.yml +4 -0
  24. data/config/locales/it.yml +4 -0
  25. data/config/locales/ja.yml +10 -5
  26. data/config/locales/kn.yml +186 -10
  27. data/config/locales/ko.yml +184 -13
  28. data/config/locales/kz.yml +183 -17
  29. data/config/locales/lt.yml +184 -8
  30. data/config/locales/lv.yml +188 -12
  31. data/config/locales/ml.yml +189 -13
  32. data/config/locales/mr.yml +184 -8
  33. data/config/locales/ms.yml +184 -8
  34. data/config/locales/nb.yml +183 -17
  35. data/config/locales/nl.yml +184 -13
  36. data/config/locales/pa.yml +189 -13
  37. data/config/locales/pl.yml +184 -13
  38. data/config/locales/pt.yml +184 -13
  39. data/config/locales/ro.yml +183 -12
  40. data/config/locales/ru.yml +4 -0
  41. data/config/locales/sk.yml +184 -8
  42. data/config/locales/sl.yml +185 -9
  43. data/config/locales/sr.yml +186 -15
  44. data/config/locales/sv.yml +182 -13
  45. data/config/locales/sw.yml +184 -8
  46. data/config/locales/ta.yml +184 -8
  47. data/config/locales/te.yml +185 -9
  48. data/config/locales/th.yml +183 -12
  49. data/config/locales/tr.yml +183 -12
  50. data/config/locales/uk.yml +189 -18
  51. data/config/locales/ur.yml +187 -11
  52. data/config/locales/vi.yml +183 -12
  53. data/config/locales/zh.yml +191 -9
  54. data/lib/num2words/converter.rb +144 -35
  55. data/lib/num2words/locales/ar.rb +130 -0
  56. data/lib/num2words/locales/be.rb +101 -0
  57. data/lib/num2words/locales/bg.rb +127 -0
  58. data/lib/num2words/locales/bn.rb +112 -0
  59. data/lib/num2words/locales/cs.rb +106 -0
  60. data/lib/num2words/locales/da.rb +97 -0
  61. data/lib/num2words/locales/de.rb +119 -0
  62. data/lib/num2words/locales/el.rb +143 -0
  63. data/lib/num2words/locales/en.rb +90 -0
  64. data/lib/num2words/locales/es.rb +185 -0
  65. data/lib/num2words/locales/et.rb +104 -0
  66. data/lib/num2words/locales/fa.rb +104 -0
  67. data/lib/num2words/locales/fi.rb +104 -0
  68. data/lib/num2words/locales/fr.rb +121 -0
  69. data/lib/num2words/locales/gu.rb +112 -0
  70. data/lib/num2words/locales/he.rb +130 -0
  71. data/lib/num2words/locales/hi.rb +112 -0
  72. data/lib/num2words/locales/hr.rb +117 -0
  73. data/lib/num2words/locales/hu.rb +111 -0
  74. data/lib/num2words/locales/id.rb +104 -0
  75. data/lib/num2words/locales/it.rb +142 -0
  76. data/lib/num2words/locales/ja.rb +126 -0
  77. data/lib/num2words/locales/kn.rb +122 -2
  78. data/lib/num2words/locales/ko.rb +158 -2
  79. data/lib/num2words/locales/kz.rb +137 -2
  80. data/lib/num2words/locales/lt.rb +146 -2
  81. data/lib/num2words/locales/lv.rb +148 -2
  82. data/lib/num2words/locales/ml.rb +142 -2
  83. data/lib/num2words/locales/mr.rb +142 -2
  84. data/lib/num2words/locales/ms.rb +134 -2
  85. data/lib/num2words/locales/nb.rb +153 -0
  86. data/lib/num2words/locales/nl.rb +142 -2
  87. data/lib/num2words/locales/pa.rb +122 -2
  88. data/lib/num2words/locales/pl.rb +149 -2
  89. data/lib/num2words/locales/pt.rb +164 -2
  90. data/lib/num2words/locales/ro.rb +165 -2
  91. data/lib/num2words/locales/ru.rb +100 -0
  92. data/lib/num2words/locales/sk.rb +124 -2
  93. data/lib/num2words/locales/sl.rb +154 -2
  94. data/lib/num2words/locales/sr.rb +141 -3
  95. data/lib/num2words/locales/sv.rb +141 -2
  96. data/lib/num2words/locales/sw.rb +133 -2
  97. data/lib/num2words/locales/ta.rb +142 -2
  98. data/lib/num2words/locales/te.rb +142 -2
  99. data/lib/num2words/locales/th.rb +133 -2
  100. data/lib/num2words/locales/tr.rb +141 -2
  101. data/lib/num2words/locales/uk.rb +134 -2
  102. data/lib/num2words/locales/ur.rb +130 -2
  103. data/lib/num2words/locales/vi.rb +142 -2
  104. data/lib/num2words/locales/zh.rb +172 -2
  105. data/lib/num2words/version.rb +1 -1
  106. data/num2words.gemspec +2 -2
  107. metadata +6 -6
  108. data/lib/num2words/locales/no.rb +0 -19
@@ -57,23 +57,25 @@ module Num2words
57
57
 
58
58
  decimal_amount = decimal_currency_amount(amount)
59
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)
60
62
 
61
63
  parts = [
62
- to_words(major_value, locale: locale),
63
- pluralize(major_value, *currency_data[:major_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])
64
66
  ]
65
67
 
66
68
  include_minor = minor == :always || (minor == :nonzero && minor_value.positive?)
67
69
  if include_minor
68
70
  parts.concat([
69
- to_words(minor_value, locale: locale, feminine: true),
70
- pluralize(minor_value, *currency_data[:minor_unit])
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])
71
73
  ])
72
74
  end
73
75
 
74
- parts.unshift(Locales[locale]::GRAMMAR[:minus] || "minus") if decimal_amount.negative?
76
+ parts.unshift(locale_minus_word(Locales[locale])) if decimal_amount.negative?
75
77
 
76
- apply_case(parts.join(" ").strip, word_case)
78
+ apply_case(locale_join_currency_parts(Locales[locale], parts), word_case)
77
79
  end
78
80
 
79
81
  private
@@ -104,10 +106,103 @@ module Num2words
104
106
  end
105
107
  end
106
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
+
107
197
  # n — 0..999, scale_idx — индекс разряда (0 — единицы, 1 — тысячи, ...)
108
198
  # feminine: true — использовать женский род для единиц (нужно для тысяч/копеек)
109
- 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)
110
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
+
111
206
  words = []
112
207
 
113
208
  words << local_data::HUNDREDS[n / 100] if n >= 100
@@ -121,14 +216,14 @@ module Num2words
121
216
  words << (feminine ? local_data::ONES_FEM[ones] : local_data::ONES_MASC[ones]) if ones.positive?
122
217
  end
123
218
 
124
- 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?
125
220
  words.compact
126
221
  end
127
222
 
128
223
  def to_words_fractional(number, locale, feminine, locale_data, style: :fraction, joiner: :default)
129
- minus_word = locale_data::GRAMMAR[:minus] || "minus"
130
- conjunction_word = joiner.to_sym == :and ? "и" : locale_data::GRAMMAR[:conjunction] || "and"
131
- default_fraction = locale_data::GRAMMAR[:default_fraction] || "parts"
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)
132
227
  fractions_data = locale_data::FRACTIONS || {}
133
228
 
134
229
  negative = number.is_a?(String) ? number.start_with?("-") : number.negative?
@@ -146,24 +241,26 @@ module Num2words
146
241
 
147
242
  integer_words = to_words_integer(integer_value, locale, feminine, locale_data)
148
243
 
149
- if locale.to_sym == :en && style == :decimal
150
- fraction_digits = fraction_string.chars.map { |d| to_words_integer(d.to_i, locale, feminine, locale_data) }
151
- full_string = [sign_word, integer_words, "point", fraction_digits.join(" ")].reject(&:empty?).join(" ")
152
- 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])
153
247
  end
154
248
 
155
- 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)
156
251
 
157
252
  denom_forms = fractions_data[denominator] || fractions_data[denominator.to_s] # массив склонений
158
- 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
159
254
 
160
- [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])
161
256
  end
162
257
 
163
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
+
164
261
  integer_value = Integer(number)
165
262
 
166
- minus_word = locale_data::GRAMMAR[:minus] || "minus"
263
+ minus_word = locale_minus_word(locale_data)
167
264
  negative = integer_value.negative?
168
265
  integer_value = integer_value.abs
169
266
 
@@ -176,8 +273,8 @@ module Num2words
176
273
  words = []
177
274
  groups.each_with_index do |group_value, index|
178
275
  scale_index = groups.size - index - 1
179
- group_feminine = (scale_index == 1) || feminine
180
- 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)
181
278
  end
182
279
 
183
280
  words.unshift(minus_word) if negative
@@ -198,9 +295,17 @@ module Num2words
198
295
  raise ArgumentError, "Template not found for locale #{locale}" unless template
199
296
 
200
297
  day_gender = date_case.to_sym == :genitive ? :masculine : :neuter
201
- day_words = to_words_ordinal(day, locale, format, locale_data, gender: day_gender)
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
202
303
  month_words = months[month - 1]
203
- 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
204
309
 
205
310
  template % { day: day_words, month: month_words, year: year_words }
206
311
  end
@@ -236,18 +341,22 @@ module Num2words
236
341
  words = locale_data::TIME[:words]
237
342
  template = locale_data::TIME_TEMPLATE
238
343
 
239
- hours = [
240
- to_words_integer(time.hour, locale, false, locale_data),
241
- pluralize(time.hour, *words[:hour])
242
- ].join(" ")
243
- minutes = [
244
- to_words_integer(time.min, locale, true, locale_data),
245
- pluralize(time.min, *words[:minute])
246
- ].join(" ")
247
- seconds = [
248
- to_words_integer(time.sec, locale, true, locale_data),
249
- pluralize(time.sec, *words[:second])
250
- ].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
+ )
251
360
 
252
361
  format = if short
253
362
  time.min.zero? && time.sec.zero? ? :hours_only : :hours_minutes
@@ -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
@@ -20,6 +20,133 @@ module Num2words
20
20
  DATETIME_TEMPLATE = I18n.t("num2words.datetime.template", locale: :bg)
21
21
 
22
22
  ORDINALS = I18n.t("num2words.numbers.ordinals", locale: :bg)
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 integer_to_words(number, feminine: false)
47
+ cardinal(number, feminine: feminine)
48
+ end
49
+
50
+ def triple_to_words(number, scale_idx, feminine: false)
51
+ words = []
52
+
53
+ hundreds = number / 100
54
+ rest = number % 100
55
+
56
+ words << HUNDREDS[hundreds] if hundreds.positive?
57
+
58
+ if rest.between?(10, 19)
59
+ words << join_with_conjunction(words, TEENS[rest - 10])
60
+ else
61
+ tens = rest / 10
62
+ ones = rest % 10
63
+
64
+ if tens >= 2
65
+ tens_words = TENS[tens]
66
+ words << (ones.positive? ? "#{tens_words} и #{feminine ? ONES_FEM[ones] : ONES_MASC[ones]}" : tens_words)
67
+ elsif ones.positive?
68
+ words << join_with_conjunction(words, feminine ? ONES_FEM[ones] : ONES_MASC[ones])
69
+ end
70
+ end
71
+
72
+ words << pluralize(number, *SCALES[scale_idx]) unless scale_idx.zero?
73
+ words.compact
74
+ end
75
+
76
+ def date_day(day, format:, date_case:)
77
+ gender = date_case.to_sym == :genitive ? :masculine : :neuter
78
+ ordinal(day, :nominative, gender: gender)
79
+ end
80
+
81
+ def date_year(year, format:)
82
+ ordinal(year, :nominative)
83
+ end
84
+
85
+ def ordinal(value, format, gender: :masculine)
86
+ ordinals = ORDINALS[format]
87
+ gender_data = ordinals[gender] || ordinals[:masculine]
88
+
89
+ return gender_data[value - 1] if value.between?(1, gender_data.length)
90
+
91
+ if value > 31
92
+ thousands = (value / 100) * 100
93
+ last_two = value % 100
94
+ base_year = cardinal(thousands)
95
+ last_ordinal = gender_data[last_two - 1] || cardinal(last_two)
96
+
97
+ return [base_year, last_ordinal].join(" ")
98
+ end
99
+
100
+ cardinal(value)
101
+ end
102
+
103
+ def cardinal(number, feminine: false)
104
+ integer_value = Integer(number)
105
+ negative = integer_value.negative?
106
+ integer_value = integer_value.abs
107
+
108
+ return (feminine ? ONES_FEM[0] : ONES_MASC[0]) if integer_value.zero?
109
+
110
+ groups = integer_value.to_s
111
+ .chars.reverse.each_slice(3).map(&:reverse)
112
+ .map(&:join).map!(&:to_i).reverse
113
+
114
+ group_phrases = []
115
+ groups.each_with_index do |group_value, index|
116
+ next if group_value.zero?
117
+
118
+ scale_idx = groups.size - index - 1
119
+ group_feminine = feminine_group?(scale_idx) || feminine
120
+ group_words = triple_to_words(group_value, scale_idx, feminine: group_feminine)
121
+ group_words.shift if scale_idx == 1 && group_value == 1
122
+ group_phrases << [group_words.join(" "), scale_idx, group_value]
123
+ end
124
+
125
+ result = join_groups(group_phrases)
126
+ negative ? [minus_word, result].join(" ") : result
127
+ end
128
+
129
+ def pluralize(number, singular, few, plural)
130
+ number.abs == 1 ? singular : few
131
+ end
132
+
133
+ def join_with_conjunction(words, value)
134
+ words.empty? ? value : "и #{value}"
135
+ end
136
+
137
+ def join_groups(group_phrases)
138
+ return "" if group_phrases.empty?
139
+
140
+ phrases = group_phrases.map(&:first)
141
+ last_scale_idx = group_phrases[-1][1]
142
+ last_value = group_phrases[-1][2]
143
+
144
+ if group_phrases.size > 1 && last_scale_idx.zero? && last_value < 100
145
+ [phrases[0...-1].join(" "), phrases[-1]].join(" и ")
146
+ else
147
+ phrases.join(" ")
148
+ end
149
+ end
23
150
  end
24
151
 
25
152
  register :bg, BG