HumanNumber

HumanNumber is a Ruby gem that implements accurate number formatting based on international standards. It references Microsoft Globalization documentation and Unicode CLDR standards to provide human-readable number formats that respect the unique formatting conventions of each country and region.

Features

  • 🌍 International Standards Compliance: Based on Microsoft Globalization and Unicode CLDR standards
  • πŸ”’ Cultural Number Systems: Automatic selection of Western (K/M/B/T), East Asian (만/μ–΅/μ‘°), or Indian (lakh/crore) systems
  • πŸ’° Currency Formatting: ISO 4217 currency codes with native locale precision rules
  • πŸ—οΈ Intelligent Abbreviations: Culturally-appropriate large number simplification
  • πŸ”§ Rails Integration: Complete compatibility with Rails I18n infrastructure
  • πŸ“š rails-i18n Based: Leverages verified locale data

Quick Start

Add to your Gemfile and install:

gem 'human_number'
bundle install

Start formatting numbers:

require 'human_number'

# Human-readable numbers
HumanNumber.human_number(1_234_567)           #=> "1.2M"
HumanNumber.human_number(50_000, locale: :ko) #=> "5만"

# Currency formatting
HumanNumber.currency(1234.56, currency_code: 'USD', locale: :en) #=> "$1,234.56"
HumanNumber.human_currency(1_234_567, currency_code: 'USD', locale: :en) #=> "$1M"

Installation

Add this line to your application's Gemfile:

gem 'human_number'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install human_number

API Reference

Core Methods

HumanNumber.human_number(number, **options)

Formats numbers with intelligent, culturally-appropriate abbreviations.

# Basic usage
HumanNumber.human_number(1_234_567)           #=> "1.2M"
HumanNumber.human_number(50_000, locale: :ko) #=> "5만"
HumanNumber.human_number(50_000, locale: :ja) #=> "5δΈ‡"

# Significant digits control (default: max_digits: 2)  
HumanNumber.human_number(1_234_567, max_digits: 1)   #=> "1M"     # 1 significant digit
HumanNumber.human_number(1_234_567, max_digits: 3)   #=> "1.23M"  # 3 significant digits  
HumanNumber.human_number(1_234_567, max_digits: nil) #=> "1M 234K 567" # Complete breakdown

# Unit preferences (Western locales only)
HumanNumber.human_number(1_000_000, abbr_units: true)  #=> "1M"
HumanNumber.human_number(1_000_000, abbr_units: false) #=> "1 million"

# Minimum thresholds
HumanNumber.human_number(5_000, min_unit: 10_000)  #=> "5,000"
HumanNumber.human_number(50_000, min_unit: 10_000) #=> "50K"

# Zero trimming (default: true)
HumanNumber.human_number(1_000_000, trim_zeros: true)  #=> "1M"
HumanNumber.human_number(1_000_000, trim_zeros: false) #=> "1.0M"

Parameters:

  • locale (Symbol): Target locale for cultural number systems
  • max_digits (Integer|nil): Maximum significant digits (default: 2, nil for complete mode)
  • abbr_units (Boolean): Use abbreviated vs full units (default: true)
  • min_unit (Integer): Minimum unit threshold for abbreviation (default: nil)
  • trim_zeros (Boolean): Remove trailing decimal zeros (default: true)

HumanNumber.currency(number, currency_code:, locale:)

Standard currency formatting with native locale precision rules.

HumanNumber.currency(1234.56, currency_code: 'USD', locale: :en) #=> "$1,234.56"
HumanNumber.currency(50_000, currency_code: 'KRW', locale: :ko)  #=> "50,000원"
HumanNumber.currency(1234.99, currency_code: 'JPY', locale: :ja) #=> "1,235円"

# Cross-locale consistency (USD always 2 decimals, JPY always 0)
HumanNumber.currency(1234.56, currency_code: 'USD', locale: :ko) #=> "$1,234.56"  
HumanNumber.currency(1234.56, currency_code: 'JPY', locale: :en) #=> "1,235円"

HumanNumber.human_currency(number, currency_code:, locale:, **options)

Human-readable currency formatting with cultural abbreviations.

HumanNumber.human_currency(1_234_567, currency_code: 'USD', locale: :en) #=> "$1M"
HumanNumber.human_currency(50_000, currency_code: 'KRW', locale: :ko)    #=> "5λ§Œμ›"

# Combined options
HumanNumber.human_currency(1_234_567, currency_code: 'USD', locale: :en, max_digits: 3) #=> "$1.23M"
HumanNumber.human_currency(1_234_567, currency_code: 'USD', locale: :en, max_digits: nil) #=> "$1M 234K 567"

System Detection Methods

HumanNumber.number_system(locale:)

Determines which number system is used for a given locale. Returns a symbol indicating the cultural number formatting system.

# Western/Default system (K/M/B/T)
HumanNumber.number_system(locale: :en)    #=> :default
HumanNumber.number_system(locale: :fr)    #=> :default
HumanNumber.number_system(locale: :de)    #=> :default

# East Asian system (만/μ–΅/μ‘° or δΈ‡/ε„„/ε…†)  
HumanNumber.number_system(locale: :ko)    #=> :east_asian
HumanNumber.number_system(locale: :ja)    #=> :east_asian
HumanNumber.number_system(locale: :zh)    #=> :east_asian

# Indian system (thousand/lakh/crore)
HumanNumber.number_system(locale: :hi)    #=> :indian
HumanNumber.number_system(locale: :'en-IN') #=> :indian

# Defaults to current I18n.locale when not specified
HumanNumber.number_system #=> (uses I18n.locale)

Return Values:

  • :default - Western system using K/M/B/T (thousand/million/billion/trillion)
  • :east_asian - Asian system using 만/μ–΅/μ‘° or δΈ‡/ε„„/ε…† patterns
  • :indian - South Asian system using thousand/lakh/crore

HumanNumber.currency_number_system(currency_code:)

Determines which number system is used for a given currency code. Returns a symbol indicating the culturally appropriate formatting system for that currency's region.

# Default system currencies
HumanNumber.currency_number_system(currency_code: 'USD') #=> :default
HumanNumber.currency_number_system(currency_code: 'EUR') #=> :default
HumanNumber.currency_number_system(currency_code: 'GBP') #=> :default

# East Asian system currencies
HumanNumber.currency_number_system(currency_code: 'KRW') #=> :east_asian  # Korean Won
HumanNumber.currency_number_system(currency_code: 'JPY') #=> :east_asian  # Japanese Yen
HumanNumber.currency_number_system(currency_code: 'CNY') #=> :east_asian  # Chinese Yuan

# Indian system currencies
HumanNumber.currency_number_system(currency_code: 'INR') #=> :indian      # Indian Rupee

# Case-insensitive and handles whitespace
HumanNumber.currency_number_system(currency_code: 'krw')   #=> :east_asian
HumanNumber.currency_number_system(currency_code: ' USD ') #=> :default

Practical Usage: These methods are useful for understanding which formatting system will be applied, or for building custom formatting logic:

# Check system before formatting
system = HumanNumber.number_system(locale: :ko)
if system == :east_asian
  puts "Will use 만/μ–΅/μ‘° units"
  result = HumanNumber.human_number(50_000, locale: :ko) #=> "5만"
end

# Currency-aware system detection
currency_system = HumanNumber.currency_number_system(currency_code: 'KRW')
locale_system = HumanNumber.number_system(locale: :ko)
puts "Consistent systems!" if currency_system == locale_system #=> true

Rails Integration

In Rails applications, helper methods are automatically available:

<%= human_number(1_234_567) %>                         <!-- 1.2M -->
<%= human_currency(1_234_567, currency_code: 'USD') %> <!-- $1M -->
<%= currency(1234.56, currency_code: 'USD') %>         <!-- $1,234.56 -->

<!-- With options -->
<%= human_number(1_234_567, max_digits: 3, locale: :ko) %> <!-- 123만 -->
<%= number_to_human_size(1_234_567) %>                     <!-- 1.2M (legacy) -->

Cultural Number Systems

Different cultures have fundamentally different concepts for large numbers:

Western System (K/M/B/T)

Used by: English, German, French, Spanish, Italian, Portuguese, Russian, Dutch, Swedish, Danish, Norwegian

HumanNumber.human_number(1_234_567, locale: :en) #=> "1.2M"
HumanNumber.human_number(1_000_000_000, locale: :en) #=> "1B"

Units: thousand (1,000), million (1,000,000), billion (1,000,000,000), trillion (1,000,000,000,000)

East Asian System (千/δΈ‡/ε„„/ε…†)

Used by: Korean (ko), Japanese (ja), Chinese (zh, zh-CN, zh-TW)

HumanNumber.human_number(1_234_567, locale: :ko) #=> "120만"  
HumanNumber.human_number(1_234_567, locale: :ja) #=> "120δΈ‡"
HumanNumber.human_number(100_000_000, locale: :ko) #=> "1μ–΅"

# Complete mode shows cultural spacing differences
HumanNumber.human_number(12_345_678, locale: :ko, max_digits: nil) #=> "1234만 5678"  # Korean: spaces
HumanNumber.human_number(12_345_678, locale: :ja, max_digits: nil) #=> "1234δΈ‡5678"   # Japanese: no spaces

Units: 만/δΈ‡ (ten thousand), μ–΅/ε„„ (hundred million), μ‘°/ε…† (trillion)
Cultural spacing: Japanese uses no spaces between units, Korean/Chinese use spaces (configurable via locale files)

Indian System (lakh/crore)

Used by: Hindi (hi), Urdu (ur), Bengali (bn), Indian English (en-IN)

HumanNumber.human_number(100_000, locale: :hi) #=> "1 lakh"
HumanNumber.human_number(10_000_000, locale: :hi) #=> "1 crore"

Units: thousand (1,000), lakh (100,000), crore (10,000,000)

The gem automatically selects the appropriate system based on locale, ensuring cultural accuracy.

Architecture & Design Philosophy

Direct Method Design

HumanNumber uses direct class methods rather than instance-based objects for simplicity:

# Direct approach (current)
HumanNumber.human_number(1234567, locale: :ko, max_digits: 2)

# Instance approach (rejected)
formatter = HumanNumber::Formatter.new(locale: :ko, max_digits: 2)
formatter.format(1234567)

Why Direct Methods?

  • Simplicity: Clean, straightforward API
  • Thread Safety: No shared mutable state
  • Rails Compatibility: Matches Rails helper patterns

Separation of Concerns

HumanNumber uses a two-stage formatting process:

  1. Number Formatting: Converts numbers to human-readable strings with cultural units
  2. Currency Application: Applies currency symbols and format strings
# Internal flow for HumanNumber.human_currency(1234567, currency_code: 'USD', locale: :en)
formatted_number = Formatters::Number.format(1234567, locale: :en, max_digits: 2)     #=> "1.2M"
final_result = Formatters::Currency.format("1.2M", currency_code: 'USD', locale: :en) #=> "$1.2M"

This separation enables:

  • Independent testing of number vs currency logic
  • Reusability of number formatting for non-currency contexts
  • Maintainability when adding new currencies or number systems

Centralized Locale Logic

Currency-locale relationships are managed centrally in LocaleSupport:

CURRENCY_NATIVE_LOCALES = {
  "USD" => [:en], "EUR" => i[de fr es it nl], 
  "KRW" => [:ko], "JPY" => [:ja]
  # ... 40+ currencies
}

This ensures:

  • Consistent precision rules: USD always shows 2 decimals, JPY shows 0
  • Smart fallbacks: When user locale doesn't match currency's native locale
  • Easy maintenance: New currencies require only one mapping entry

Configuration-Free Design

HumanNumber deliberately avoids runtime configuration in favor of explicit parameters:

# No global configuration
HumanNumber.human_number(1234567, locale: :ko, max_digits: 2)

# Application defaults handled at application level
class ApplicationController
  def format_money(amount, currency)
    HumanNumber.human_currency(amount, currency_code: currency, locale: I18n.locale, max_digits: 2)
  end
end

Benefits:

  • No shared state or thread safety concerns
  • Predictable behavior (each call is independent)
  • Easier testing (no configuration setup/teardown)

Development

After checking out the repo:

$ cd human_number
$ bundle install
$ rake spec

Run tests:

$ rake spec

Run linting:

$ rake rubocop

Run all quality checks:

$ rake  # Runs both spec and rubocop

Contributing

  1. Fork it (https://github.com/ether-moon/human_number/fork)
  2. Create your feature branch (git checkout -b feature/my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin feature/my-new-feature)
  5. Create a new Pull Request

Contribution Guidelines

  • Please write tests when adding new features
  • Follow RuboCop style guidelines
  • Write clear and descriptive commit messages
  • Update documentation for API changes
  • Ensure 100% test coverage for new features

Adding New Locales

To add support for a new locale:

  1. Add locale to appropriate system mapping in lib/human_number/number_system.rb:
    • Update LOCALE_SYSTEM_MAPPING to include the new locale
    • If needed, add currency mapping to CURRENCY_SYSTEM_MAPPING
  2. Create locale file in config/locales/ with unit translations following rails-i18n structure
  3. Add currency precision mapping to CURRENCY_NATIVE_LOCALES in lib/human_number/locale_support.rb if applicable
  4. Add comprehensive tests covering the new locale in both spec/human_number_spec.rb and spec/human_number/number_system_spec.rb

Example: Adding Portuguese (Brazil) support:

# In lib/human_number/number_system.rb
LOCALE_SYSTEM_MAPPING = {
  east_asian: i[ko ja zh zh-CN zh-TW].freeze,
  indian: i[hi ur bn en-IN].freeze,
  # Add pt-BR to default system (no entry needed)
}.freeze

# In config/locales/pt-BR.yml  
pt-BR:
  number:
    human:
      decimal_units:
        abbr_units:
          thousand: "K"
          million: "M" 
          billion: "B"
          trillion: "T"

License

MIT License. See the LICENSE file for details.

Standards Reference

This project is based on the following international standards and documentation:

Changelog

See CHANGELOG.md for details.

Issues

Please report bugs and feature requests at GitHub Issues.