castwide_numbers_in_words 1.0.2

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +14 -0
  3. data/.gitignore +7 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +58 -0
  6. data/.travis.yml +17 -0
  7. data/Gemfile +8 -0
  8. data/Gemfile.lock +74 -0
  9. data/LICENSE.txt +24 -0
  10. data/NUMBERS_IN_WORDS.txt +22 -0
  11. data/README.md +47 -0
  12. data/Rakefile +3 -0
  13. data/bin/spec +2 -0
  14. data/castwide_numbers_in_words.gemspec +24 -0
  15. data/lib/numbers_in_words/duck_punch.rb +23 -0
  16. data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
  17. data/lib/numbers_in_words/fraction.rb +136 -0
  18. data/lib/numbers_in_words/number_group.rb +64 -0
  19. data/lib/numbers_in_words/parsing/fraction_parsing.rb +31 -0
  20. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  21. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  22. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  23. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  24. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  25. data/lib/numbers_in_words/parsing/special.rb +67 -0
  26. data/lib/numbers_in_words/parsing/to_number.rb +77 -0
  27. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  28. data/lib/numbers_in_words/to_word.rb +84 -0
  29. data/lib/numbers_in_words/version.rb +5 -0
  30. data/lib/numbers_in_words/writer.rb +69 -0
  31. data/lib/numbers_in_words.rb +58 -0
  32. data/spec/exceptional_numbers_spec.rb +26 -0
  33. data/spec/fraction_spec.rb +152 -0
  34. data/spec/fractions_spec.rb +31 -0
  35. data/spec/non_monkey_patch_spec.rb +51 -0
  36. data/spec/number_group_spec.rb +17 -0
  37. data/spec/number_parser_spec.rb +31 -0
  38. data/spec/numbers_in_words_spec.rb +99 -0
  39. data/spec/numerical_strings_spec.rb +35 -0
  40. data/spec/spec_helper.rb +26 -0
  41. data/spec/to_word_spec.rb +18 -0
  42. data/spec/words_in_numbers_spec.rb +152 -0
  43. data/spec/writer_spec.rb +26 -0
  44. data/spec/years_spec.rb +27 -0
  45. metadata +124 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7d91888df88e8fc7610f529b7cbe3871f2d655094becfaf76ce0f20a520d1c22
4
+ data.tar.gz: 2071d799552a79eaa52c891e0508d0835832b9bea52d870391cd78bd0b17e535
5
+ SHA512:
6
+ metadata.gz: 286ad2a50878696c6841879653f2ee86bc0e0f3b1a7e5dc5e46f0e962cbc29fbf26385fadfdab81088336e435aec658e7d3884d1bf1bab8c6e9e3434b7ab9bbf
7
+ data.tar.gz: 3e94b737d38930ab1fb5559c455d24207d7b6226133155bf9558252e2695018cb414018c38fe3ed866c2b0e648fcc8433e2d6c261ad385609bd112ff768ed644
data/.codeclimate.yml ADDED
@@ -0,0 +1,14 @@
1
+ ---
2
+ engines:
3
+ fixme:
4
+ enabled: true
5
+ rubocop:
6
+ enabled: true
7
+ ratings:
8
+ paths:
9
+ - Gemfile.lock
10
+ - "**.rb"
11
+ exclude_paths:
12
+ - spec/**/*
13
+ - .codeclimate.yml
14
+ - .rubocop.yml
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.swp
2
+ *.swo
3
+ *.html
4
+ tmp
5
+ pkg/
6
+ Gemfile.lock
7
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,58 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+ Layout/EmptyLinesAroundAttributeAccessor:
12
+ Enabled: true
13
+ Layout/SpaceAroundMethodCallOperator:
14
+ Enabled: true
15
+ Lint/DeprecatedOpenSSLConstant:
16
+ Enabled: true
17
+ Lint/MixedRegexpCaptureTypes:
18
+ Enabled: true
19
+ Lint/RaiseException:
20
+ Enabled: true
21
+ Lint/StructNewOverride:
22
+ Enabled: true
23
+ Style/ExponentialNotation:
24
+ Enabled: true
25
+ Style/HashEachMethods:
26
+ Enabled: true
27
+ Style/HashTransformKeys:
28
+ Enabled: true
29
+ Style/HashTransformValues:
30
+ Enabled: true
31
+ Style/RedundantFetchBlock:
32
+ Enabled: true
33
+ Style/RedundantRegexpCharacterClass:
34
+ Enabled: true
35
+ Style/RedundantRegexpEscape:
36
+ Enabled: true
37
+ Style/SlicingWithRange:
38
+ Enabled: true
39
+ Lint/DuplicateElsifCondition:
40
+ Enabled: true
41
+ Style/AccessorGrouping:
42
+ Enabled: true
43
+ Style/ArrayCoercion:
44
+ Enabled: true
45
+ Style/BisectedAttrAccessor:
46
+ Enabled: true
47
+ Style/CaseLikeIf:
48
+ Enabled: true
49
+ Style/Documentation:
50
+ Enabled: false
51
+ Style/HashAsLastArrayItem:
52
+ Enabled: true
53
+ Style/HashLikeCase:
54
+ Enabled: true
55
+ Style/RedundantAssignment:
56
+ Enabled: true
57
+ Style/RedundantFileExtensionInRequire:
58
+ Enabled: true
data/.travis.yml ADDED
@@ -0,0 +1,17 @@
1
+ env:
2
+ global:
3
+ - CC_TEST_REPORTER_ID=9a548b48c1c0804d8fb9083ae8e9693a5d5197eb86e8cf1b63a89d18f33e0464
4
+ sudo: false
5
+ language: ruby
6
+ rvm:
7
+ - "2.4.10"
8
+ - "2.5.8"
9
+ - "2.6.6"
10
+ - "2.7.1"
11
+ before_script:
12
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
13
+ - chmod +x ./cc-test-reporter
14
+ - ./cc-test-reporter before-build
15
+ script: ./bin/spec
16
+ after_script:
17
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gem 'byebug'
6
+ gem 'simplecov', '< 0.18.0', require: false, group: :test
7
+ # Specify your gem's dependencies in numbers_in_words.gemspec
8
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,74 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ castwide_numbers_in_words (1.0.1)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.3)
10
+ byebug (12.0.0)
11
+ diff-lcs (1.6.2)
12
+ docile (1.4.1)
13
+ json (2.13.2)
14
+ language_server-protocol (3.17.0.5)
15
+ lint_roller (1.1.0)
16
+ parallel (1.27.0)
17
+ parser (3.3.9.0)
18
+ ast (~> 2.4.1)
19
+ racc
20
+ prism (1.4.0)
21
+ racc (1.8.1)
22
+ rainbow (3.1.1)
23
+ regexp_parser (2.10.0)
24
+ rspec (3.4.0)
25
+ rspec-core (~> 3.4.0)
26
+ rspec-expectations (~> 3.4.0)
27
+ rspec-mocks (~> 3.4.0)
28
+ rspec-core (3.4.4)
29
+ rspec-support (~> 3.4.0)
30
+ rspec-expectations (3.4.0)
31
+ diff-lcs (>= 1.2.0, < 2.0)
32
+ rspec-support (~> 3.4.0)
33
+ rspec-mocks (3.4.1)
34
+ diff-lcs (>= 1.2.0, < 2.0)
35
+ rspec-support (~> 3.4.0)
36
+ rspec-support (3.4.1)
37
+ rubocop (1.79.0)
38
+ json (~> 2.3)
39
+ language_server-protocol (~> 3.17.0.2)
40
+ lint_roller (~> 1.1.0)
41
+ parallel (~> 1.10)
42
+ parser (>= 3.3.0.2)
43
+ rainbow (>= 2.2.2, < 4.0)
44
+ regexp_parser (>= 2.9.3, < 3.0)
45
+ rubocop-ast (>= 1.46.0, < 2.0)
46
+ ruby-progressbar (~> 1.7)
47
+ tsort (>= 0.2.0)
48
+ unicode-display_width (>= 2.4.0, < 4.0)
49
+ rubocop-ast (1.46.0)
50
+ parser (>= 3.3.7.2)
51
+ prism (~> 1.4)
52
+ ruby-progressbar (1.13.0)
53
+ simplecov (0.17.1)
54
+ docile (~> 1.1)
55
+ json (>= 1.8, < 3)
56
+ simplecov-html (~> 0.10.0)
57
+ simplecov-html (0.10.2)
58
+ tsort (0.2.0)
59
+ unicode-display_width (3.1.4)
60
+ unicode-emoji (~> 4.0, >= 4.0.4)
61
+ unicode-emoji (4.0.4)
62
+
63
+ PLATFORMS
64
+ x64-mingw-ucrt
65
+
66
+ DEPENDENCIES
67
+ byebug
68
+ castwide_numbers_in_words!
69
+ rspec (~> 3.4.0)
70
+ rubocop
71
+ simplecov (< 0.18.0)
72
+
73
+ BUNDLED WITH
74
+ 2.6.9
data/LICENSE.txt ADDED
@@ -0,0 +1,24 @@
1
+ Adapted from numbers_in_words by Mark Burns (see NUMBERS_IN_WORDS.txt)
2
+
3
+ Copyright (c) 2025 by Fred Snyder
4
+
5
+ MIT License
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining
8
+ a copy of this software and associated documentation files (the
9
+ "Software"), to deal in the Software without restriction, including
10
+ without limitation the rights to use, copy, modify, merge, publish,
11
+ distribute, sublicense, and/or sell copies of the Software, and to
12
+ permit persons to whom the Software is furnished to do so, subject to
13
+ the following conditions:
14
+
15
+ The above copyright notice and this permission notice shall be
16
+ included in all copies or substantial portions of the Software.
17
+
18
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Mark Burns
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,47 @@
1
+ **This is a fork of the [numbers_in_words gem by Mark Burns](https://github.com/markburns/numbers_in_words).** The original license is reproduced in [NUMBERS_IN_WORDS.txt](NUMBERS_IN_WORDS.txt).
2
+
3
+ Changes in this fork:
4
+ * Compatibility with [Opal](https://github.com/opal/opal)
5
+ * Updates for Ruby 3
6
+
7
+ Installation
8
+ ============
9
+
10
+ ```ruby
11
+ gem 'castwide_numbers_in_words'
12
+ ```
13
+
14
+ Usage
15
+ =========
16
+
17
+ ```ruby
18
+ require 'numbers_in_words'
19
+
20
+ NumbersInWords.in_words(112)
21
+ #=> one hundred and twelve
22
+
23
+ NumbersInWords.in_numbers("one googol")
24
+ #=>10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
25
+
26
+ NumbersInWords.in_numbers("Seventy million, five-hundred and fifty six thousand point eight nine three")
27
+ #=> 70556000.893
28
+
29
+ NumbersInWords.in_numbers("nineteen sixty five")
30
+ #=> 1965
31
+ ```
32
+
33
+
34
+ Monkey patch version
35
+
36
+ ```ruby
37
+ require 'numbers_in_words'
38
+ require 'numbers_in_words/duck_punch'
39
+ 112.in_words
40
+ #=> one hundred and twelve
41
+
42
+ "one googol".in_numbers
43
+ #=>10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
44
+
45
+ "Seventy million, five-hundred and fifty six thousand point eight nine three".in_numbers
46
+ #=> 70556000.893
47
+ ```
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
data/bin/spec ADDED
@@ -0,0 +1,2 @@
1
+ bundle exec rspec spec/non_monkey_patch_spec.rb
2
+ bundle exec rspec spec
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'numbers_in_words/version'
6
+
7
+ Gem::Specification.new do |gem|
8
+ gem.name = 'castwide_numbers_in_words'
9
+ gem.description = 'convert written numbers into Integers and vice-versa'
10
+ gem.summary = 'Example: NumbersInWords.in_words(123) # => ' \
11
+ '"one hundred and twenty three", NumbersInWords.in_numbers("seventy-five point eight") # = > 75.8'
12
+
13
+ gem.version = NumbersInWords::VERSION
14
+ gem.authors = ['Fred Snyder']
15
+ gem.email = ['castwide@gmail.com']
16
+ gem.homepage = 'http://github.com/castwide/castwide_numbers_in_words'
17
+
18
+ gem.add_development_dependency 'rspec', '~> 3.4.0'
19
+ gem.add_development_dependency 'rubocop'
20
+
21
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
22
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
23
+ gem.require_paths = ['lib']
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ module NumericExtension
5
+ def in_words(fraction: false)
6
+ NumbersInWords::ToWord.new(self).in_words(fraction: fraction)
7
+ end
8
+ end
9
+
10
+ module StringExtension
11
+ def in_numbers(only_compress: false)
12
+ NumbersInWords::ToNumber.new(self, only_compress).call
13
+ end
14
+ end
15
+ end
16
+
17
+ class String
18
+ include NumbersInWords::StringExtension
19
+ end
20
+
21
+ class Numeric
22
+ include NumbersInWords::NumericExtension
23
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require_relative 'fraction'
6
+ require_relative 'powers_of_ten'
7
+
8
+ module NumbersInWords
9
+ class ExceptionalNumbers
10
+ extend Forwardable
11
+
12
+ DEFINITIONS = {
13
+ 0 => { number: 'zero', ordinal: 'zeroth' },
14
+ 1 => { number: 'one', ordinal: 'first' },
15
+ 2 => { number: 'two', ordinal: 'second', fraction: { singular: 'half', plural: 'halves' } },
16
+ 3 => { number: 'three', ordinal: 'third' },
17
+ 4 => { number: 'four', ordinal: 'fourth', fraction: { singular: 'quarter', plural: 'quarters' } },
18
+ 5 => { number: 'five', ordinal: 'fifth' },
19
+ 6 => { number: 'six' },
20
+ 7 => { number: 'seven' },
21
+ 8 => { number: 'eight', ordinal: 'eighth' },
22
+ 9 => { number: 'nine', ordinal: 'ninth' },
23
+ 10 => { number: 'ten' },
24
+ 11 => { number: 'eleven' },
25
+ 12 => { number: 'twelve', ordinal: 'twelfth' },
26
+ 13 => { number: 'thirteen' },
27
+ 14 => { number: 'fourteen' },
28
+ 15 => { number: 'fifteen' },
29
+ 16 => { number: 'sixteen' },
30
+ 17 => { number: 'seventeen' },
31
+ 18 => { number: 'eighteen' },
32
+ 19 => { number: 'nineteen' },
33
+ 20 => { number: 'twenty', ordinal: 'twentieth' },
34
+ 30 => { number: 'thirty', ordinal: 'thirtieth' },
35
+ 40 => { number: 'forty', ordinal: 'fortieth' },
36
+ 50 => { number: 'fifty', ordinal: 'fiftieth' },
37
+ 60 => { number: 'sixty', ordinal: 'sixtieth' },
38
+ 70 => { number: 'seventy', ordinal: 'seventieth' },
39
+ 80 => { number: 'eighty', ordinal: 'eightieth' },
40
+ 90 => { number: 'ninety', ordinal: 'ninetieth' }
41
+ }.freeze
42
+
43
+ def fraction_names
44
+ @fraction_names ||= determine_fraction_names
45
+ end
46
+
47
+ def lookup_fraction(words)
48
+ fraction_lookup[words]
49
+ end
50
+
51
+ def fraction_lookup
52
+ @fraction_lookup ||= generate_fraction_lookup
53
+ end
54
+
55
+ def lookup(number)
56
+ to_h[number]
57
+ end
58
+
59
+ def fraction(denominator: nil, numerator: nil, word: nil)
60
+ raise unless denominator || word
61
+
62
+ numerator ||= 1
63
+
64
+ denominator ||= NumbersInWords.in_numbers(word)
65
+
66
+ Fraction.new(denominator: denominator, numerator: numerator, attributes: DEFINITIONS[denominator])
67
+ end
68
+
69
+ def to_h
70
+ @to_h ||= DEFINITIONS.transform_values do |h|
71
+ h[:number]
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def generate_fraction_lookup
78
+ named_fractions.each_with_object({}) do |f, result|
79
+ f.lookup_keys.each do |k|
80
+ key = k.split(' ').last
81
+ result[key] = 1.0 / f.denominator.to_f
82
+ end
83
+ end
84
+ end
85
+
86
+ def named_fractions
87
+ @named_fractions ||= numbers.flat_map do |n|
88
+ [
89
+ Fraction.new(denominator: n, numerator: 1),
90
+ Fraction.new(denominator: n, numerator: 2)
91
+ ]
92
+ end
93
+ end
94
+
95
+ def numbers
96
+ (2..100).to_a + powers_of_ten_skipping_googolplex.map { |p| 10**p }
97
+ end
98
+
99
+ def powers_of_ten_skipping_googolplex
100
+ POWERS_OF_TEN.keys[0..-2]
101
+ end
102
+
103
+ def determine_fraction_names
104
+ names = named_fractions.map(&:in_words)
105
+
106
+ words = names.map(&:split).map(&:last)
107
+ words += strip_punctuation(words)
108
+ words.uniq
109
+ end
110
+
111
+ def strip_punctuation(words)
112
+ words.map { |w| w.gsub(/^a-z/, ' ') }
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ class Fraction
5
+ attr_reader :denominator, :numerator, :attributes
6
+
7
+ def self.in_words(that)
8
+ r = that.rationalize(EPSILON)
9
+
10
+ NumbersInWords
11
+ .fraction(denominator: r.denominator, numerator: r.numerator)
12
+ .in_words
13
+ end
14
+
15
+ def initialize(denominator:, numerator: 1, attributes: nil)
16
+ @denominator = denominator
17
+ @numerator = numerator
18
+ @attributes = attributes || NumbersInWords::ExceptionalNumbers::DEFINITIONS[denominator] || {}
19
+ end
20
+
21
+ def lookup_keys
22
+ key = in_words
23
+ key2 = strip_punctuation(key.split(' ')).join(' ')
24
+
25
+ key3 = "a #{key}"
26
+ key4 = "an #{key}"
27
+ key5 = "a #{key2}"
28
+ key6 = "an #{key2}"
29
+ [key, key2, key3, key4, key5, key6].uniq
30
+ end
31
+
32
+ def in_words
33
+ NumbersInWords.in_words(numerator) + ' ' + fraction
34
+ end
35
+
36
+ def ordinal
37
+ pluralize? ? pluralized_ordinal_in_words : singular_ordinal_in_words
38
+ end
39
+
40
+ def fraction
41
+ if denominator == Float::INFINITY
42
+ # We've reached the limits of ruby's number system
43
+ # by the time we get to a googolplex (10 ** (10 ** 100))
44
+ return pluralize? ? 'infinitieths' : 'infinitieth'
45
+ end
46
+
47
+ pluralize? ? pluralized_fraction : singular_fraction
48
+ end
49
+
50
+ private
51
+
52
+ def strip_punctuation(words)
53
+ words.map { |w| w.gsub(/^a-z/, ' ') }
54
+ end
55
+
56
+ def pluralized_fraction
57
+ fraction_plural || pluralized_ordinal_in_words
58
+ end
59
+
60
+ def singular_fraction
61
+ fraction_singular || singular_ordinal_in_words
62
+ end
63
+
64
+ def pluralized_ordinal_in_words
65
+ pluralized_ordinal || denominator_ordinal_in_words
66
+ end
67
+
68
+ def singular_ordinal_in_words
69
+ singular_ordinal || denominator_ordinal_in_words
70
+ end
71
+
72
+ def singular_ordinal
73
+ attributes[:ordinal]
74
+ end
75
+
76
+ def pluralized_ordinal
77
+ singular_ordinal && singular_ordinal + 's'
78
+ end
79
+
80
+ def pluralize?
81
+ numerator > 1
82
+ end
83
+
84
+ def denominator_ordinal_in_words
85
+ if denominator > 100
86
+ # one hundred and second
87
+ with_remainder(100, ' and ')
88
+ elsif denominator > 19
89
+ # two thirty-fifths
90
+ with_remainder(10, '-')
91
+ else
92
+ # one seventh
93
+ singular = NumbersInWords.in_words(denominator) + 'th'
94
+ pluralize? ? singular + 's' : singular
95
+ end
96
+ end
97
+
98
+ def with_remainder(mod, join_word)
99
+ rest = denominator % mod
100
+ main = denominator - rest
101
+ main = NumbersInWords.in_words(main)
102
+
103
+ main = main.gsub(/^one /, '') if pluralize?
104
+
105
+ rest_zero(rest, main) || joined(main, rest, join_word)
106
+ end
107
+
108
+ def joined(main, rest, join_word)
109
+ main +
110
+ join_word +
111
+ self.class.new(numerator: numerator, denominator: rest).ordinal
112
+ end
113
+
114
+ def rest_zero(rest, main)
115
+ return unless rest.zero?
116
+
117
+ if pluralize?
118
+ main + 'ths'
119
+ else
120
+ main + 'th'
121
+ end
122
+ end
123
+
124
+ def exception
125
+ attributes[:fraction]
126
+ end
127
+
128
+ def fraction_singular
129
+ exception && exception[:singular]
130
+ end
131
+
132
+ def fraction_plural
133
+ exception && exception[:plural]
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ class NumberGroup
5
+ include Enumerable
6
+ attr_accessor :number
7
+
8
+ def self.groups_of(number, size)
9
+ new(number).groups(size)
10
+ end
11
+
12
+ def initialize(number)
13
+ @number = number
14
+ end
15
+
16
+ # split into groups this gives us 1234567 => 123 456 7
17
+ # so we need to reverse first
18
+ # in stages
19
+ def groups(size)
20
+ # 1234567 => %w(765 432 1)
21
+ @array = in_groups_of(@number.to_s.reverse.split(''), size)
22
+ # %w(765 432 1) => %w(1 432 765)
23
+ @array.reverse!
24
+
25
+ # %w(1 432 765) => [1, 234, 567]
26
+ @array.map! { |group| group.reverse.join('').to_i }
27
+ @array.reverse! # put in ascending order of power of ten
28
+
29
+ power = 0
30
+
31
+ # [1, 234, 567] => {6 => 1, 3 => 234, 0 => 567}
32
+ @array.each_with_object({}) do |digits, o|
33
+ o[power] = digits
34
+ power += size
35
+ end
36
+ end
37
+
38
+ def split_decimals
39
+ return unless @number.to_s.include?('.')
40
+
41
+ int, decimal = @number.to_s.split '.'
42
+
43
+ [int.to_i, decimal.split(//).map(&:to_i)]
44
+ end
45
+
46
+ def split_googols
47
+ googols = @number.to_s[0..-LENGTH_OF_GOOGOL].to_i
48
+ remainder = @number.to_s[(1 - LENGTH_OF_GOOGOL)..-1].to_i
49
+ [googols, remainder]
50
+ end
51
+
52
+ private
53
+
54
+ def in_groups_of(array, number, fill_with = nil)
55
+ # size % number gives how many extra we have;
56
+ # subtracting from number gives how many to add;
57
+ # modulo number ensures we don't add group of just fill.
58
+ padding = (number - array.size % number) % number
59
+ collection = array.dup.concat(Array.new(padding, fill_with))
60
+
61
+ collection.each_slice(number).to_a
62
+ end
63
+ end
64
+ end