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
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
133 changes: 3 additions & 130 deletions lib/humanize.rb
Original file line number Diff line number Diff line change
@@ -1,131 +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 :'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: locale,
decimals_as: decimals_as)
end
end

class Integer
include Humanize
end

class Float
include Humanize
end

class BigDecimal
include Humanize
end
120 changes: 120 additions & 0 deletions lib/humanize/module.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# frozen_string_literal: true

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)
Humanize.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 ealphabetical 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 :'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

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.

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
significant_digits.to_i.humanize(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
46 changes: 46 additions & 0 deletions spec/core_ext_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
require 'spec_helper'

RSpec.describe "Humanize core extensions to Ruby" do
after do
Humanize.reset_config
end

describe 'both options work together' do
it 'work together' do
expect(
0.42.humanize(locale: :fr, decimals_as: :number)
).to eql('zéro virgule quarante-deux')
end
end

describe 'when called on instances of Rational, Complex, and Date::Infinity' do
it 'will raise NoMethodError' do
expect { Rational(1, 3).humanize }.to raise_error(NoMethodError, /humanize/)
expect { Complex(1 + 2i).humanize }.to raise_error(NoMethodError, /humanize/)
if defined? Date::Infinity
expect do
Date::Infinity.new.humanize
end.to raise_error(NoMethodError, /humanize/)
end
end
end

describe 'when called on conceptual number' do
it 'reads correctly' do
inf = Float::INFINITY
neg_inf = -inf
nan = inf + neg_inf

expect(inf.humanize).to eql('infinity')
expect(neg_inf.humanize).to eql('negative infinity')
expect(nan.humanize).to eql('undefined')
end
end

describe 'when called on bigdecimal with decimal fractions' do
it 'reads the decimal digits' do
expect(BigDecimal('123').humanize).to eql('one hundred and twenty-three')
expect(BigDecimal('123.45').humanize).to eql('one hundred and twenty-three point four five')
end
end
end
Loading
Loading