Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Separate core_ext #88

Merged
merged 11 commits into from
May 10, 2024
60 changes: 54 additions & 6 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ All the way up to 156 digit numbers:

If you are dealing with numbers larger than 156 digits, we accept patches. Word list is sourced from: [ Wordlist.source ]

## Usage
## Usage (Extends Ruby Core)
Note: this extends to Ruby core classes `Integer`, `Float`, and `BigDecimal`.
See below for usage without extending Ruby core.

### Install the gem using RubyGems

Expand Down Expand Up @@ -53,29 +55,75 @@ require 'humanize'
0.001.humanize => "zero point zero zero one"
```

## Usage (Without Extending Ruby Core)
Note: this usage does not extend Ruby core.
Instead it relies on explicitly calling the pure function `Humanize.format`.

### Install the gem using RubyGems

```bash
gem install humanize
```

or:

### Add it to your Gemfile

```ruby
gem 'humanize', require: false
```

### Include the module in your program

```ruby
require 'humanize/module' # does not extend Ruby corex
```

### Call the format function on the numbers

```ruby
Humanize.format(100) => "one hundred"
Humanize.format(1001) => "one thousand and one"
Humanize.format(0.001) => "zero point zero zero one"
```

## Configuration

```ruby
Humanize.configure do |config|
config.default_locale = :en # [:en, :es, :fr, :tr, :de, :id], default: :en
config.default_locale = :en # [:en, :es, :fr, :'fr-CH', :tr, :az, :de, :id, :th, :ru, :pt, :ms, :jp, :vi, :'zh-tw'], default: :en
config.decimals_as = :digits # [:digits, :number], default: :digits
end
```

Default values can be overriden:
The default locale can be overridden by passing an explicit `locale:`:

```ruby
42.humanize(locale: :fr) # => "quarante-deux"
1666.humanize(locale: :tr) # => "bin altı yüz altmış altı"
```

## Decimals
Or, functionally:

You can choose how you want to display decimals:
```ruby
Humanize.format(42, locale: :fr) # => "quarante-deux"
Humanize.format(1666, locale: :tr) # => "bin altı yüz altmış altı"
```

### Decimals

Similarly, the default display of decimals can be overridden by passing an explicit `decimals_as:` value:

```ruby
0.42.humanize(decimals_as: :digits) # => "zero point four two"
0.42.humanize(decimals_as: :number) # => "zero point fourty-two"
0.42.humanize(decimals_as: :number) # => "zero point forty-two"
```

Or, functionally:

```ruby
Humanize.format(0.42, decimals_as: :digits) # => "zero point four two"
Humanize.format(0.42, decimals_as: :number) # => "zero point forty-two"
```

## I18n
Expand Down
2 changes: 1 addition & 1 deletion humanize.gemspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Gem::Specification.new do |s|
s.name = "humanize"
s.version = "3.0.0"
s.version = "3.1.0"

s.required_ruby_version = '>= 3.1'
s.require_paths = ["lib"]
Expand Down
135 changes: 3 additions & 132 deletions lib/humanize.rb
Original file line number Diff line number Diff line change
@@ -1,133 +1,4 @@
require 'bigdecimal'
require_relative 'humanize/locales'
# frozen_string_literal: true

module Humanize
SPACE = ' '.freeze
EMPTY = ''.freeze
# Big numbers are big: http://wiki.answers.com/Q/What_number_is_after_vigintillion&src=ansTT

def humanize(locale: Humanize.config.default_locale,
decimals_as: Humanize.config.decimals_as)
locale_class, spacer = Humanize.for_locale(locale)

return locale_class::SUB_ONE_GROUPING[0] if zero?

infinity = to_f.infinite?
if infinity
infinity_word = locale_class::INFINITY
return infinity == 1 ? infinity_word : "#{locale_class::NEGATIVE}#{spacer}#{infinity_word}"
elsif is_a?(Float) && nan?
return locale_class::UNDEFINED
end

sign = locale_class::NEGATIVE if negative?

parts = locale_class.new.humanize(abs)
process_decimals(locale_class, locale, parts, decimals_as, spacer)
Humanize.stringify(parts, sign, spacer)
end

def self.for_locale(locale)
case locale.to_sym
# NOTE: add locales here in ealphabetical order
when :az, :de, :en, :es, :fr, :id, :ms, :pt, :ru, :vi
[Object.const_get("Humanize::#{locale.capitalize}"), SPACE]
when :th
[Humanize::Th, EMPTY]
when :tr
[Humanize::Tr, SPACE]
when :jp
[Humanize::Jp, EMPTY]
when :'zh-tw'
[Humanize::ZhTw, EMPTY]
when :'fr-CH'
[Humanize::FrCh, SPACE]
else
raise "Unsupported humanize locale: #{locale}"
end
end

def self.stringify(parts, sign, spacer)
output = parts.reverse.join(spacer).squeeze(spacer)
if locale_is?(:es) && sign
"#{output}#{spacer}#{sign}"
elsif sign
"#{sign}#{spacer}#{output}"
else
output
end
end

def self.locale_is?(locale)
Humanize.config.default_locale == locale
end

def process_decimals(locale_class, locale, parts, decimals_as, spacer)
return unless is_a?(Float) || is_a?(BigDecimal)

# Why 15?
# (byebug) BigDecimal.new(number, 15)
# 0.8000015e1
# (byebug) BigDecimal.new(number, 16)
# 0.8000014999999999e1
decimal = BigDecimal(self, 15) - BigDecimal(to_i)

_sign, significant_digits, _base, exponent = decimal.split
return if significant_digits == "0"

grouping = locale_class::SUB_ONE_GROUPING
leading_zeroes = [grouping[0]] * exponent.abs
decimals_as = :digits if leading_zeroes.any?

decimals_as_words =
case decimals_as
when :digits
digits = significant_digits.chars.map do |num|
grouping[num.to_i]
end

(leading_zeroes + digits).join(spacer)
when :number
significant_digits.to_i.humanize(locale:)
end

parts.insert(0, decimals_as_words, locale_class::POINT)
end

class << self
attr_writer :config
end

def self.config
@config ||= Configuration.new
end

def self.reset_config
@config = Configuration.new
end

def self.configure
yield(config)
end

class Configuration
attr_accessor :default_locale, :decimals_as

def initialize
@default_locale = :en
@decimals_as = :digits
end
end
end

class Integer
include Humanize
end

class Float
include Humanize
end

class BigDecimal
include Humanize
end
require "humanize/module" # require just this if you don't need the core_ext extensions to Integer, Float, BigDecimal
require "humanize/core_ext"
20 changes: 20 additions & 0 deletions lib/humanize/core_ext.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
module Humanize
def humanize(locale: Humanize.config.default_locale,
decimals_as: Humanize.config.decimals_as)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the glue code that converts from implicit self arg to explicit self passed as the number arg.

Humanize.format(self,
locale:,
decimals_as:)
end
end

class Integer
include Humanize
end

class Float
include Humanize
end

class BigDecimal
include Humanize
end
122 changes: 122 additions & 0 deletions lib/humanize/module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
require 'bigdecimal'
require_relative 'locales'

module Humanize
# Big numbers are big: http://wiki.answers.com/Q/What_number_is_after_vigintillion&src=ansTT

class << self
def format(number,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this changed from humanize with (where self was mixed in as the implicit number) to format with an explicit number arg.

locale: Humanize.config.default_locale,
decimals_as: Humanize.config.decimals_as)
locale_class, spacer = Humanize.for_locale(locale)

return locale_class::SUB_ONE_GROUPING[0] if number.zero?

infinity = number.to_f.infinite?
if infinity
infinity_word = locale_class::INFINITY
return infinity == 1 ? infinity_word : "#{locale_class::NEGATIVE}#{spacer}#{infinity_word}"
elsif number.is_a?(Float) && number.nan?
return locale_class::UNDEFINED
end

sign = locale_class::NEGATIVE if number.negative?

parts = locale_class.new.humanize(number.abs)
process_decimals(number, locale_class, locale, parts, decimals_as, spacer)
Humanize.stringify(parts, sign, spacer)
end

def for_locale(locale)
case locale.to_sym
# NOTE: add locales here in alphabetical order
when :az, :de, :en, :es, :fr, :id, :ms, :pt, :ru, :vi
[Object.const_get("Humanize::#{locale.capitalize}"), ' ']
when :th
[Humanize::Th, '']
when :tr
[Humanize::Tr, ' ']
when :jp
[Humanize::Jp, '']
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is missing the recent zh-TW addition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@radar Oops, I didn't mean to make any changes to this locale list. I just looked over in master and didn't find any reference to zh. Can you point me to it?

And I'm looking at the build failures.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The build failures were rubocop suggestions. All addressed here: 0abcfc9

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@radar Oops, I didn't mean to make any changes to this locale list. I just looked over in master and didn't find any reference to zh. Can you point me to it?

And I'm looking at the build failures.

Getting ahead of myself. It's in #87.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, merged #87. So can we now get that in here? I think it's just the two lines for the when + the config.

Copy link
Contributor Author

@ColinDKelley ColinDKelley May 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've merged latest master containing that PR into this branch.

when :'zh-tw'
[Humanize::ZhTw, '']
when :'fr-CH'
[Humanize::FrCh, ' ']
else
raise "Unsupported humanize locale: #{locale}"
end
end

def stringify(parts, sign, spacer)
output = parts.reverse.join(spacer).squeeze(spacer)
if locale_is?(:es) && sign
"#{output}#{spacer}#{sign}"
elsif sign
"#{sign}#{spacer}#{output}"
else
output
end
end

def locale_is?(locale)
Humanize.config.default_locale == locale
end

# rubocop:disable Metrics/ParameterLists
def process_decimals(number, locale_class, locale, parts, decimals_as, spacer)
Copy link
Contributor Author

@ColinDKelley ColinDKelley May 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this previously polluted the mixin namespace (it became a public method of Integer, Float, and BigDecimal). Now it's pushed down into the module...where it has an explicit number arg.

# rubocop:enable Metrics/ParameterLists
return unless number.is_a?(Float) || number.is_a?(BigDecimal)

# Why 15?
# (byebug) BigDecimal.new(number, 15)
# 0.8000015e1
# (byebug) BigDecimal.new(number, 16)
# 0.8000014999999999e1
decimal = BigDecimal(number, 15) - BigDecimal(number.to_i)

_sign, significant_digits, _base, exponent = decimal.split
return if significant_digits == "0"

grouping = locale_class::SUB_ONE_GROUPING
leading_zeroes = [grouping[0]] * exponent.abs
decimals_as = :digits if leading_zeroes.any?

decimals_as_words =
case decimals_as
when :digits
digits = significant_digits.chars.map do |num|
grouping[num.to_i]
end

(leading_zeroes + digits).join(spacer)
when :number
Humanize.format(significant_digits.to_i, locale:)
end

parts.insert(0, decimals_as_words, locale_class::POINT)
end

attr_writer :config

def config
@config ||= Configuration.new
end

def reset_config
@config = Configuration.new
end

def configure
yield(config)
end
end

class Configuration
attr_accessor :default_locale, :decimals_as

def initialize
@default_locale = :en
@decimals_as = :digits
end
end
end
Loading