human_number 0.1.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.
@@ -0,0 +1,462 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../locale_support"
4
+
5
+ module HumanNumber
6
+ module Formatters
7
+ class Number
8
+ extend ActionView::Helpers::NumberHelper
9
+
10
+ # Constants for string literals and magic numbers
11
+ ZERO_STRING = "0"
12
+ EMPTY_SEPARATOR = ""
13
+ SPACE_SEPARATOR = " "
14
+ DECIMAL_FORMAT_TEMPLATE = "%.%df"
15
+ MINIMUM_UNIT_COUNT = 1.0
16
+ SINGLE_DIGIT_LIMIT = 1
17
+
18
+ # I18n key templates
19
+ I18N_DECIMAL_UNITS_KEY = "number.human.decimal_units"
20
+ I18N_ABBR_UNITS_SECTION = "abbr_units"
21
+ I18N_UNITS_SECTION = "units"
22
+
23
+ class << self
24
+ # Internal formatter for numbers with intelligent abbreviations.
25
+ # All parameters are required - use HumanNumber.human_number for user-friendly interface.
26
+ #
27
+ # @param number [Numeric] Number to format
28
+ # @param locale [Symbol, String] Locale for unit system selection
29
+ # @param abbr_units [Boolean] Use abbreviated symbols vs full words
30
+ # @param max_digits [Integer, nil] Max digits after decimal, nil for complete mode
31
+ # @param min_unit [Integer, nil] Minimum unit threshold for abbreviation
32
+ # @param trim_zeros [Boolean] Remove trailing decimal zeros
33
+ # @return [String] Formatted number string
34
+ #
35
+ # @api private
36
+ def format(number, locale:, abbr_units:, max_digits:, min_unit:, trim_zeros:)
37
+ locale = locale.to_sym
38
+
39
+ # Direct delegation to appropriate system class
40
+ system_class = determine_system_class(locale)
41
+ system_class.format_number(
42
+ number,
43
+ locale:,
44
+ abbr_units:,
45
+ max_digits:,
46
+ min_unit:,
47
+ trim_zeros:
48
+ )
49
+ end
50
+
51
+ # Default options for human number formatting
52
+ def default_options
53
+ {
54
+ abbr_units: true,
55
+ max_digits: 2,
56
+ min_unit: nil,
57
+ trim_zeros: true,
58
+ }.freeze
59
+ end
60
+
61
+ # Default options for currency number formatting (tighter display)
62
+ def default_currency_number_options
63
+ {
64
+ abbr_units: true,
65
+ max_digits: 1,
66
+ min_unit: nil,
67
+ trim_zeros: true,
68
+ }.freeze
69
+ end
70
+
71
+ # Formats a number using Rails' currency precision rules for the given currency.
72
+ # This ensures consistent precision regardless of display locale.
73
+ #
74
+ # @param number [Numeric] The number to format
75
+ # @param currency_code [String] ISO 4217 currency code
76
+ # @param locale [Symbol] Display locale (for context)
77
+ # @return [String] Formatted number with currency-appropriate precision
78
+ def format_with_currency_precision(number, currency_code:, locale:)
79
+ locale = locale.to_sym
80
+
81
+ # Use currency's native locale for precision rules
82
+ precision_locale = LocaleSupport.currency_precision_locale(currency_code, locale)
83
+
84
+ I18n.with_locale(precision_locale) do
85
+ number_to_currency(number, format: "%n", locale: precision_locale)
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def determine_system_class(locale)
92
+ locale_sym = locale.to_sym
93
+
94
+ case locale_sym
95
+ when *EastAsianSystem::TARGET_LOCALES
96
+ EastAsianSystem
97
+ when *IndianSystem::TARGET_LOCALES
98
+ IndianSystem
99
+ else
100
+ DefaultSystem
101
+ end
102
+ end
103
+ end
104
+
105
+ # Default number system (thousand, million, billion, trillion)
106
+ class DefaultSystem
107
+ TARGET_LOCALES = %i[en es fr de it pt ru nl sv da no].freeze
108
+
109
+ UNIT_DEFINITIONS = [
110
+ { key: :trillion, divisor: 1_000_000_000_000 },
111
+ { key: :billion, divisor: 1_000_000_000 },
112
+ { key: :million, divisor: 1_000_000 },
113
+ { key: :thousand, divisor: 1_000 },
114
+ ].freeze
115
+
116
+ class << self
117
+ def format_number(number, locale:, abbr_units: true, min_unit: nil, max_digits: 2, trim_zeros: true)
118
+ return ZERO_STRING if number.zero?
119
+
120
+ parts = if max_digits.nil?
121
+ format_in_complete_mode(number, locale, abbr_units, min_unit)
122
+ else
123
+ format_in_abbreviated_mode(number, locale, abbr_units, max_digits, trim_zeros, min_unit)
124
+ end
125
+
126
+ return number.to_s if parts.nil?
127
+
128
+ finalize_result(number, parts)
129
+ end
130
+
131
+ def format_in_complete_mode(number, locale, abbr_units, min_unit)
132
+ # Complete mode shows all units: "1M 234K 567"
133
+ unit_breakdown = break_down_into_all_units(number)
134
+ units_above_threshold = filter_by_minimum_unit_threshold(unit_breakdown, min_unit)
135
+ return nil if units_above_threshold.empty?
136
+
137
+ format_all_unit_parts(units_above_threshold, locale, abbr_units)
138
+ end
139
+
140
+ def format_in_abbreviated_mode(number, locale, abbr_units, max_digits, trim_zeros, min_unit)
141
+ # Abbreviated mode shows single largest unit: "1.2M"
142
+ unit_breakdown = break_down_into_largest_unit(number)
143
+ units_above_threshold = filter_by_minimum_unit_threshold(unit_breakdown, min_unit)
144
+ return nil if units_above_threshold.empty?
145
+
146
+ format_abbreviated_unit_parts(units_above_threshold, locale, abbr_units, max_digits, trim_zeros)
147
+ end
148
+
149
+ private
150
+
151
+ def breakdown_number(number)
152
+ result = []
153
+ remaining = number.abs
154
+
155
+ unit_definitions.each do |unit|
156
+ next unless remaining >= unit[:divisor]
157
+
158
+ count = remaining.to_f / unit[:divisor]
159
+ result << { unit_key: unit[:key], count: count, divisor: unit[:divisor] }
160
+ break # Only use the largest applicable unit
161
+ end
162
+
163
+ # If no unit matched, use the raw number
164
+ result << { unit_key: :units, count: remaining, divisor: 1 } if result.empty?
165
+
166
+ result
167
+ end
168
+
169
+ # Find the largest applicable unit for abbreviated display
170
+ # Example: 1,234,567 -> [{million: 1.234567}]
171
+ def break_down_into_largest_unit(number)
172
+ absolute_amount = number.abs
173
+
174
+ # Find the first (largest) unit where the count would be >= 1.0
175
+ unit_definitions.each do |unit|
176
+ unit_count = absolute_amount.to_f / unit[:divisor]
177
+
178
+ return [create_unit_part(unit[:key], unit_count, unit[:divisor])] if unit_count >= MINIMUM_UNIT_COUNT
179
+ end
180
+
181
+ # If no unit is applicable, return the raw number
182
+ [create_unit_part(:units, absolute_amount, 1)]
183
+ end
184
+
185
+ def unit_definitions
186
+ UNIT_DEFINITIONS
187
+ end
188
+
189
+ # Helper method to create consistent unit part structure
190
+ def create_unit_part(unit_key, count, divisor)
191
+ { unit_key: unit_key, count: count, divisor: divisor }
192
+ end
193
+
194
+ # Filter out units below the minimum threshold
195
+ def filter_by_minimum_unit_threshold(breakdown, min_unit)
196
+ return breakdown unless min_unit
197
+
198
+ breakdown.select { |unit_info| unit_info[:divisor] >= min_unit }
199
+ end
200
+
201
+ # Format unit parts for abbreviated display (e.g., "1.2M")
202
+ def format_abbreviated_unit_parts(breakdown, locale, abbr_units, max_digits, trim_zeros)
203
+ breakdown.map do |unit_info|
204
+ if unit_info[:unit_key] == :units
205
+ # Raw numbers don't need significant digits rounding
206
+ format_raw_number_count(unit_info[:count], max_digits, trim_zeros)
207
+ else
208
+ # Format with unit symbol (e.g., "1.2" + "M" = "1.2M")
209
+ format_number_with_unit_symbol(unit_info, locale, abbr_units, max_digits, trim_zeros)
210
+ end
211
+ end
212
+ end
213
+
214
+ # Format raw number without unit symbols
215
+ def format_raw_number_count(count, max_digits, trim_zeros)
216
+ format_unit_count(count, max_digits:, trim_zeros:, apply_rounding: false)
217
+ end
218
+
219
+ # Format number with unit symbol (e.g., "1.2M")
220
+ def format_number_with_unit_symbol(unit_info, locale, abbr_units, max_digits, trim_zeros)
221
+ symbol = lookup_unit_symbol(locale, unit_info[:unit_key], abbr_units)
222
+ format_with_symbol(unit_info[:count], symbol, abbr_units, max_digits:, trim_zeros:)
223
+ end
224
+
225
+ # Format unit with symbol for complete mode (e.g., "234K")
226
+ def format_complete_unit_with_symbol(unit_info, locale, abbr_units)
227
+ symbol = lookup_unit_symbol(locale, unit_info[:unit_key], abbr_units)
228
+ count_text = unit_info[:count].to_i.to_s
229
+ separator = determine_unit_value_separator(abbr_units)
230
+ formatted_symbol = apply_symbol_formatting_rules(symbol, abbr_units)
231
+ "#{count_text}#{separator}#{formatted_symbol}"
232
+ end
233
+
234
+ def format_unit_count(count, max_digits: 2, trim_zeros: true, apply_rounding: true)
235
+ return count.to_s unless count.is_a?(Numeric)
236
+
237
+ rounded = apply_rounding ? round_to_significant_digits(count, max_digits) : count
238
+
239
+ format_rounded_count(rounded, max_digits, trim_zeros, apply_rounding, count)
240
+ end
241
+
242
+ def format_rounded_count(rounded, max_digits, trim_zeros, apply_rounding, original_count)
243
+ if trim_zeros
244
+ format_with_trimmed_zeros(rounded)
245
+ elsif apply_rounding
246
+ format_with_fixed_decimals(rounded, max_digits)
247
+ else
248
+ original_count.to_s
249
+ end
250
+ end
251
+
252
+ def format_with_trimmed_zeros(rounded)
253
+ if rounded == rounded.to_i
254
+ rounded.to_i.to_s
255
+ else
256
+ rounded.to_s
257
+ end
258
+ end
259
+
260
+ def format_with_fixed_decimals(rounded, max_digits)
261
+ integer_digits = rounded.to_i.to_s.length
262
+ decimal_places = [0, max_digits - integer_digits].max
263
+ format("%.#{decimal_places}f", rounded)
264
+ end
265
+
266
+ def round_to_significant_digits(number, digits)
267
+ return handle_edge_cases(number, digits) if should_handle_edge_case?(number, digits)
268
+
269
+ abs_num = number.abs
270
+ int_digits = count_integer_digits(abs_num.to_i)
271
+
272
+ result = calculate_significant_digits_result(abs_num, digits, int_digits)
273
+ apply_sign(number, result)
274
+ end
275
+
276
+ def should_handle_edge_case?(number, digits)
277
+ number.zero? || digits <= 0
278
+ end
279
+
280
+ def handle_edge_cases(number, _digits)
281
+ return 0.0 if number.zero?
282
+
283
+ number
284
+ end
285
+
286
+ def calculate_significant_digits_result(abs_num, digits, int_digits)
287
+ if int_digits >= digits
288
+ truncate_to_digits(abs_num.to_i, digits)
289
+ else
290
+ round_with_decimals(abs_num, digits, int_digits)
291
+ end
292
+ end
293
+
294
+ def apply_sign(original_number, result)
295
+ original_number.negative? ? -result : result
296
+ end
297
+
298
+ def count_integer_digits(int_part)
299
+ int_part.zero? ? SINGLE_DIGIT_LIMIT : int_part.to_s.length
300
+ end
301
+
302
+ def truncate_to_digits(int_part, digits)
303
+ int_part.to_s[0, digits].to_i.to_f
304
+ end
305
+
306
+ def round_with_decimals(abs_num, digits, int_digits)
307
+ decimal_places = digits - int_digits
308
+ (abs_num * (10**decimal_places)).round / (10**decimal_places).to_f
309
+ end
310
+
311
+ # Look up the localized symbol for a unit (e.g., 'M' for million)
312
+ def lookup_unit_symbol(locale, unit_key, abbr_units)
313
+ section = abbr_units ? I18N_ABBR_UNITS_SECTION : I18N_UNITS_SECTION
314
+
315
+ I18n.t("#{I18N_DECIMAL_UNITS_KEY}.#{section}.#{unit_key}", locale:, default: nil) ||
316
+ I18n.t("#{I18N_DECIMAL_UNITS_KEY}.#{section}.#{unit_key}", locale: :en, default: nil) ||
317
+ unit_key.to_s.upcase
318
+ end
319
+
320
+ def format_with_symbol(value, symbol, abbr_units, max_digits: 2, trim_zeros: true)
321
+ # Apply significant digits rounding to unit values
322
+ formatted_count = format_unit_count(value, max_digits:, trim_zeros:, apply_rounding: true)
323
+ separator = determine_unit_value_separator(abbr_units)
324
+ formatted_symbol = apply_symbol_formatting_rules(symbol, abbr_units)
325
+ "#{formatted_count}#{separator}#{formatted_symbol}"
326
+ end
327
+
328
+ # Determine separator between number and unit ("1M" vs "1 million")
329
+ def determine_unit_value_separator(abbr_units)
330
+ abbr_units ? EMPTY_SEPARATOR : SPACE_SEPARATOR
331
+ end
332
+
333
+ # Apply locale-specific formatting rules to unit symbols
334
+ def apply_symbol_formatting_rules(symbol, abbr_units)
335
+ return symbol if abbr_units || symbol.length <= 1
336
+
337
+ symbol.downcase # Western lowercase rule for full words
338
+ end
339
+
340
+ # Break down number into all applicable units for complete display
341
+ # Example: 1,234,567 -> [{million: 1}, {thousand: 234}, {units: 567}]
342
+ def break_down_into_all_units(number)
343
+ unit_breakdown = UnitBreakdown.new(number.abs, unit_definitions)
344
+ unit_breakdown.extract_all_units
345
+ end
346
+
347
+ # Helper class to manage unit breakdown state
348
+ class UnitBreakdown
349
+ def initialize(amount, unit_definitions)
350
+ @remaining_amount = amount
351
+ @unit_definitions = unit_definitions
352
+ @unit_parts = []
353
+ end
354
+
355
+ def extract_all_units
356
+ extract_major_units
357
+ add_remaining_as_units
358
+ @unit_parts
359
+ end
360
+
361
+ private
362
+
363
+ def extract_major_units
364
+ @unit_definitions.each do |unit|
365
+ extract_single_unit(unit) if @remaining_amount >= unit[:divisor]
366
+ end
367
+ end
368
+
369
+ def extract_single_unit(unit)
370
+ unit_count = (@remaining_amount / unit[:divisor]).to_i
371
+ return if unit_count.zero?
372
+
373
+ @unit_parts << create_unit_part(unit[:key], unit_count, unit[:divisor])
374
+ @remaining_amount %= unit[:divisor]
375
+ end
376
+
377
+ def add_remaining_as_units
378
+ return unless @remaining_amount.positive?
379
+
380
+ @unit_parts << create_unit_part(:units, @remaining_amount, 1)
381
+ end
382
+
383
+ def create_unit_part(unit_key, count, divisor)
384
+ { unit_key: unit_key, count: count, divisor: divisor }
385
+ end
386
+ end
387
+
388
+ # Format unit parts for complete display (e.g., "1M 234K 567")
389
+ def format_all_unit_parts(breakdown, locale, abbr_units)
390
+ breakdown.map do |unit_info|
391
+ if unit_info[:unit_key] == :units
392
+ # Raw numbers shown as integers in complete mode
393
+ unit_info[:count].to_i.to_s
394
+ else
395
+ # Format each unit part with its symbol
396
+ format_complete_unit_with_symbol(unit_info, locale, abbr_units)
397
+ end
398
+ end
399
+ end
400
+
401
+ # Apply sign and join all parts into final result
402
+ def finalize_result(number, formatted_parts)
403
+ combined_result = formatted_parts.join(SPACE_SEPARATOR)
404
+ number.negative? ? "-#{combined_result}" : combined_result
405
+ end
406
+ end
407
+ end
408
+
409
+ # East Asian number system (thousand, ten_thousand, hundred_million, trillion)
410
+ class EastAsianSystem < DefaultSystem
411
+ TARGET_LOCALES = %i[ko ja zh zh-CN zh-TW].freeze
412
+
413
+ UNIT_DEFINITIONS = [
414
+ { key: :trillion, divisor: 1_000_000_000_000 },
415
+ { key: :hundred_million, divisor: 100_000_000 },
416
+ { key: :ten_thousand, divisor: 10_000 },
417
+ { key: :thousand, divisor: 1_000 },
418
+ ].freeze
419
+
420
+ class << self
421
+ private
422
+
423
+ def unit_definitions
424
+ UNIT_DEFINITIONS
425
+ end
426
+
427
+ def format_with_symbol(value, symbol, _abbr_units, max_digits: 2, trim_zeros: true)
428
+ # East Asian: no separator regardless of abbr_units
429
+ # Apply significant digits rounding to unit values
430
+ formatted_count = format_unit_count(value, max_digits:, trim_zeros:, apply_rounding: true)
431
+ formatted_symbol = apply_symbol_formatting_rules(symbol, true)
432
+ "#{formatted_count}#{formatted_symbol}"
433
+ end
434
+
435
+ # Keep original case for Asian characters (만, 억, 조)
436
+ def apply_symbol_formatting_rules(symbol, _abbr_units)
437
+ symbol
438
+ end
439
+ end
440
+ end
441
+
442
+ # Indian subcontinent number system (thousand, lakh, crore)
443
+ class IndianSystem < DefaultSystem
444
+ TARGET_LOCALES = %i[hi ur bn en-IN].freeze
445
+
446
+ UNIT_DEFINITIONS = [
447
+ { key: :crore, divisor: 10_000_000 },
448
+ { key: :lakh, divisor: 100_000 },
449
+ { key: :thousand, divisor: 1_000 },
450
+ ].freeze
451
+
452
+ class << self
453
+ private
454
+
455
+ def unit_definitions
456
+ UNIT_DEFINITIONS
457
+ end
458
+ end
459
+ end
460
+ end
461
+ end
462
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HumanNumber
4
+ # Shared locale support logic for both Number and Currency formatters.
5
+ #
6
+ # This module provides centralized currency-to-locale mappings and locale
7
+ # resolution logic to ensure consistent precision and formatting rules
8
+ # across different formatters.
9
+ module LocaleSupport
10
+ # Mapping of currency codes to their primary native locales
11
+ # Used to determine appropriate precision and formatting rules
12
+ CURRENCY_NATIVE_LOCALES = {
13
+ "KRW" => [:ko],
14
+ "JPY" => [:ja],
15
+ "USD" => [:en],
16
+ "EUR" => %i[de fr es it nl],
17
+ "GBP" => %i[en en-GB],
18
+ "CAD" => %i[en en-CA fr fr-CA],
19
+ "CNY" => %i[zh zh-CN],
20
+ "CHF" => %i[de fr it],
21
+ "INR" => %i[hi en],
22
+ "RUB" => [:ru],
23
+ "BRL" => %i[pt pt-BR],
24
+ "MXN" => [:es],
25
+ "AUD" => %i[en en-AU],
26
+ "NZD" => %i[en en-NZ],
27
+ "SEK" => [:sv],
28
+ "NOK" => %i[no nb],
29
+ "DKK" => [:da],
30
+ "SGD" => %i[en en-SG zh zh-SG],
31
+ "HKD" => %i[zh zh-HK en en-HK],
32
+ "ZAR" => %i[en en-ZA af],
33
+ "TRY" => [:tr],
34
+ "ARS" => %i[es es-AR],
35
+ "IDR" => [:id],
36
+ "THB" => [:th],
37
+ "MYR" => %i[ms en en-MY],
38
+ "PHP" => %i[en en-PH tl],
39
+ "VND" => [:vi],
40
+ }.freeze
41
+
42
+ # Keys supported for native currency formatting
43
+ SUPPORTED_NATIVE_CURRENCY_KEYS = %i[unit format native_format].freeze
44
+
45
+ module_function
46
+
47
+ # Returns the primary locale for displaying a currency.
48
+ # If the user's locale matches a native locale for the currency, use it.
49
+ # Otherwise, use the first native locale for the currency.
50
+ #
51
+ # @param currency_code [String] ISO 4217 currency code (e.g., 'USD', 'EUR')
52
+ # @param user_locale [Symbol] User's preferred locale
53
+ # @return [Symbol] Primary locale for currency display
54
+ def primary_locale_for_currency(currency_code, user_locale)
55
+ locales = native_locales_for_currency(currency_code)
56
+ (locales.include?(user_locale.to_sym) ? user_locale.to_sym : locales.first) || :en
57
+ end
58
+
59
+ # Returns the locale to use for currency precision rules in standard formatting.
60
+ # Always returns the currency's primary native locale to ensure consistent
61
+ # precision (e.g., USD always shows 2 decimals, JPY shows 0 decimals).
62
+ #
63
+ # @param currency_code [String] ISO 4217 currency code
64
+ # @param user_locale [Symbol] User's preferred locale (for context)
65
+ # @return [Symbol] Locale for precision rules
66
+ def currency_precision_locale(currency_code, _user_locale = nil)
67
+ native_locales_for_currency(currency_code).first || :en
68
+ end
69
+
70
+ # Checks if a locale is considered native for a given currency.
71
+ #
72
+ # @param currency_code [String] ISO 4217 currency code
73
+ # @param locale [Symbol] Locale to check
74
+ # @return [Boolean] Whether the locale is native for the currency
75
+ def native_locale?(currency_code, locale)
76
+ return false unless currency_code && locale
77
+
78
+ native_locales_for_currency(currency_code).include?(locale.to_sym)
79
+ end
80
+
81
+ # Returns all native locales for a given currency.
82
+ #
83
+ # @param currency_code [String] ISO 4217 currency code
84
+ # @return [Array<Symbol>] Array of native locales for the currency
85
+ def native_locales_for_currency(currency_code)
86
+ currency_code = currency_code&.upcase
87
+ CURRENCY_NATIVE_LOCALES[currency_code] || []
88
+ end
89
+
90
+ # Generates the I18n key for native currency formatting.
91
+ #
92
+ # @param key [Symbol] Currency format key (:unit, :format, etc.)
93
+ # @return [String] I18n key for native currency formatting
94
+ def native_currency_key(key)
95
+ "number.currency.format.native_#{key}"
96
+ end
97
+
98
+ # Generates the I18n key for standard currency formatting.
99
+ #
100
+ # @param key [Symbol] Currency format key (:unit, :format, etc.)
101
+ # @return [String] I18n key for standard currency formatting
102
+ def currency_key(key)
103
+ "number.currency.format.#{key}"
104
+ end
105
+
106
+ # Looks up currency-related I18n values with native locale fallback.
107
+ #
108
+ # @param key [Symbol] Currency format key to look up
109
+ # @param locale [Symbol] Display locale
110
+ # @param currency_code [String] ISO 4217 currency code
111
+ # @return [String, nil] I18n value or nil if not found
112
+ def lookup_currency_i18n_value(key, locale:, currency_code:)
113
+ # Return nil for unknown currencies so formatters can fall back appropriately
114
+ return nil if native_locales_for_currency(currency_code).empty?
115
+
116
+ if SUPPORTED_NATIVE_CURRENCY_KEYS.include?(key) && native_locale?(currency_code, locale)
117
+ native_key = native_currency_key(key)
118
+
119
+ return I18n.t(native_key, locale:) if I18n.exists?(native_key, locale)
120
+ end
121
+
122
+ I18n.t(currency_key(key), locale: primary_locale_for_currency(currency_code, locale))
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module HumanNumber
4
+ module Rails
5
+ # Rails view helpers for human-readable number and currency formatting.
6
+ #
7
+ # These helpers provide convenient access to HumanNumber functionality
8
+ # within Rails views and controllers with Rails-friendly parameter handling.
9
+ module Helpers
10
+ # Formats a number with intelligent, locale-aware abbreviations.
11
+ #
12
+ # This is a Rails helper wrapper around HumanNumber.human_number that provides
13
+ # Rails-friendly parameter handling with optional locale detection.
14
+ #
15
+ # @param number [Numeric] The number to format
16
+ # @param locale [Symbol, String, nil] The locale for formatting (default: current I18n locale)
17
+ # @param options [Hash] Additional formatting options
18
+ # @option options [Boolean] :abbr_units (true) Use abbreviated unit symbols
19
+ # @option options [Integer, nil] :max_digits (1) Maximum significant digits
20
+ # @option options [Integer, nil] :min_unit (nil) Minimum unit threshold
21
+ # @option options [Boolean] :trim_zeros (true) Remove trailing decimal zeros
22
+ #
23
+ # @return [String] The formatted number string
24
+ #
25
+ # @example Basic usage in views
26
+ # <%= human_number(1234567) %> #=> "1.2M"
27
+ # <%= human_number(50000, locale: :ko) %> #=> "5만"
28
+ # <%= human_number(1234567, max_digits: 2) %> #=> "1.23M"
29
+ #
30
+ # @see HumanNumber.human_number Main implementation
31
+ def human_number(number, locale: nil, **options)
32
+ locale ||= I18n.locale
33
+ HumanNumber.human_number(number, locale:, **options)
34
+ end
35
+
36
+ # Formats a currency amount using standard Rails currency formatting.
37
+ #
38
+ # This helper provides Rails-friendly parameter handling for currency formatting
39
+ # with automatic locale detection and parameter normalization.
40
+ #
41
+ # @param number [Numeric] The amount to format
42
+ # @param currency_code [String] ISO 4217 currency code (e.g., 'USD', 'EUR')
43
+ # @param locale [Symbol, String, nil] Display locale (default: current I18n locale)
44
+ #
45
+ # @return [String] The formatted currency string
46
+ #
47
+ # @example Basic usage in views
48
+ # <%= currency(1234.56, currency_code: 'USD') %> #=> "$1,234.56"
49
+ # <%= currency(50000, currency_code: 'KRW', locale: :ko) %> #=> "50,000원"
50
+ # <%= currency(1234.99, currency_code: 'JPY') %> #=> "1,235円"
51
+ #
52
+ # @see HumanNumber.currency Main implementation
53
+ def currency(number, currency_code:, locale: nil)
54
+ locale ||= I18n.locale
55
+ HumanNumber.currency(number, currency_code:, locale:)
56
+ end
57
+
58
+ # Formats a currency amount with intelligent, locale-aware abbreviations.
59
+ #
60
+ # This helper combines human-readable number formatting with currency symbols,
61
+ # providing Rails-friendly access to abbreviated currency formatting.
62
+ #
63
+ # @param number [Numeric] The amount to format
64
+ # @param currency_code [String] ISO 4217 currency code (e.g., 'USD', 'EUR')
65
+ # @param locale [Symbol, String, nil] Locale for formatting (default: current I18n locale)
66
+ # @param options [Hash] Additional formatting options
67
+ # @option options [Boolean] :abbr_units (true) Use abbreviated unit symbols
68
+ # @option options [Integer, nil] :max_digits (1) Maximum significant digits
69
+ # @option options [Integer, nil] :min_unit (nil) Minimum unit threshold
70
+ # @option options [Boolean] :trim_zeros (true) Remove trailing decimal zeros
71
+ #
72
+ # @return [String] The formatted currency string with abbreviations
73
+ #
74
+ # @example Basic usage in views
75
+ # <%= human_currency(1234567, currency_code: 'USD') %> #=> "$1.2M"
76
+ # <%= human_currency(50000, currency_code: 'KRW', locale: :ko) %> #=> "5만원"
77
+ # <%= human_currency(1234567, currency_code: 'USD', max_digits: 2) %> #=> "$1.23M"
78
+ # <%= human_currency(1000000, currency_code: 'USD', abbr_units: false) %> #=> "$1 million"
79
+ #
80
+ # @see HumanNumber.human_currency Main implementation
81
+ def human_currency(number, currency_code:, locale: nil, **options)
82
+ locale ||= I18n.locale
83
+ HumanNumber.human_currency(number, currency_code:, locale:, **options)
84
+ end
85
+
86
+ # Legacy alias for human_number to maintain backward compatibility.
87
+ #
88
+ # @deprecated Use {#human_number} instead
89
+ # @param number [Numeric] The number to format
90
+ # @param options [Hash] Formatting options (Rails-style hash)
91
+ # @return [String] The formatted number string
92
+ def number_to_human_size(number, **options)
93
+ locale = options.delete(:locale) || I18n.locale
94
+ human_number(number, locale:, **options)
95
+ end
96
+ end
97
+ end
98
+ end