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
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'numbers_in_words/version'
4
+ require 'numbers_in_words/exceptional_numbers'
5
+ require 'numbers_in_words/parsing/number_parser'
6
+ require 'numbers_in_words/to_word'
7
+ require 'numbers_in_words/parsing/to_number'
8
+
9
+ module NumbersInWords
10
+ LENGTH_OF_GOOGOL = 101 # length of the string i.e. one with 100 zeros
11
+ Error = ::Class.new(::StandardError)
12
+ AmbiguousParsingError = ::Class.new(Error)
13
+ DivideByZeroError = ::Class.new(Error)
14
+ InvalidNumber = ::Class.new(Error)
15
+
16
+ class << self
17
+ extend Forwardable
18
+ def_delegators :exceptional_numbers, :fraction
19
+
20
+ def in_words(num, fraction: false)
21
+ ToWord.new(num).in_words(fraction: fraction)
22
+ end
23
+
24
+ def in_numbers(words, only_compress: false)
25
+ ToNumber.new(words, only_compress).call
26
+ end
27
+
28
+ def exceptional_numbers
29
+ @exceptional_numbers ||= ExceptionalNumbers.new
30
+ end
31
+
32
+ def lookup(number)
33
+ exceptional_numbers.lookup(number)
34
+ end
35
+
36
+ def exceptional_number(text)
37
+ exceptional_numbers_to_i[text]
38
+ end
39
+
40
+ def power_of_ten(text)
41
+ powers_of_ten_to_i[text]
42
+ end
43
+
44
+ private
45
+
46
+ def exceptional_numbers_to_i
47
+ @exceptional_numbers_to_i ||= swap_keys exceptional_numbers.to_h
48
+ end
49
+
50
+ def powers_of_ten_to_i
51
+ @powers_of_ten_to_i ||= swap_keys POWERS_OF_TEN
52
+ end
53
+
54
+ def swap_keys(hash)
55
+ hash.each_with_object({}) { |(k, v), h| h[v] = k }
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './spec/spec_helper'
4
+
5
+ describe NumbersInWords::ExceptionalNumbers do
6
+ describe '#fraction' do
7
+ FRACTIONS = {
8
+ [1, 2] => 'one half',
9
+ [2, 2] => 'two halves',
10
+ [3, 2] => 'three halves',
11
+ [1, 3] => 'one third',
12
+ [1, 4] => 'one quarter',
13
+ [2, 17] => 'two seventeenths',
14
+ [1, 1_000] => 'one one thousandth',
15
+ [74, 101] => 'seventy-four hundred and firsts',
16
+ [13, 97] => 'thirteen ninety-sevenths',
17
+ [131, 1_000] => 'one hundred and thirty-one thousandths'
18
+ }.freeze
19
+
20
+ FRACTIONS.each do |(numerator, denominator), string|
21
+ it "#{numerator}/#{denominator} == #{string}" do
22
+ expect(subject.fraction(denominator: denominator, numerator: numerator).in_words).to eql(string)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe NumbersInWords::Fraction do # rubocop: disable Metrics/BlockLength
4
+ subject do
5
+ described_class.new(numerator: numerator, denominator: denominator, attributes: attributes)
6
+ end
7
+
8
+ let(:numerator) { 1 }
9
+
10
+ context 'halves' do
11
+ let(:denominator) { 2 }
12
+ let(:attributes) do
13
+ { number: 'two',
14
+ ordinal: 'second',
15
+ fraction: { singular: 'half', plural: 'halves' } }
16
+ end
17
+
18
+ it do
19
+ expect(subject.in_words).to eq 'one half'
20
+ end
21
+
22
+ context 'with plural' do
23
+ let(:numerator) { 2 }
24
+
25
+ it do
26
+ expect(subject.in_words).to eq 'two halves'
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'quarters' do
32
+ let(:denominator) { 4 }
33
+ let(:attributes) do
34
+ {
35
+ number: 'four',
36
+ ordinal: 'fourth',
37
+ fraction: { singular: 'quarter' }
38
+ }
39
+ end
40
+
41
+ it do
42
+ expect(subject.in_words).to eq 'one quarter'
43
+ end
44
+ end
45
+
46
+ context 'fifths' do
47
+ let(:denominator) { 5 }
48
+ let(:attributes) do
49
+ { number: 'five', ordinal: 'fifth' }
50
+ end
51
+
52
+ it do
53
+ expect(subject.in_words).to eq 'one fifth'
54
+ end
55
+ end
56
+
57
+ context 'sixths' do
58
+ let(:denominator) { 6 }
59
+ let(:attributes) { {} }
60
+
61
+ it do
62
+ expect(subject.in_words).to eq 'one sixth'
63
+ end
64
+ end
65
+
66
+ context 'nineteenths' do
67
+ let(:denominator) { 19 }
68
+ let(:attributes) { {} }
69
+
70
+ it do
71
+ expect(subject.in_words).to eq 'one nineteenth'
72
+ expect(subject.fraction).to eq 'nineteenth'
73
+ end
74
+
75
+ context 'plural' do
76
+ let(:numerator) { 763 }
77
+
78
+ it do
79
+ expect(subject.in_words).to eq 'seven hundred and sixty-three nineteenths'
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'one hundred and seconds' do
85
+ let(:denominator) { 102 }
86
+ let(:attributes) { {} }
87
+
88
+ it do
89
+ expect(subject.in_words).to eq 'one one hundred and second'
90
+ end
91
+ end
92
+
93
+ context 'one hundred and sixth' do
94
+ let(:denominator) { 106 }
95
+ let(:attributes) { {} }
96
+
97
+ it do
98
+ expect(subject.in_words).to eq 'one one hundred and sixth'
99
+ end
100
+ end
101
+
102
+ context 'one hundred and nineteenth' do
103
+ let(:denominator) { 119 }
104
+ let(:attributes) { {} }
105
+
106
+ it do
107
+ expect(subject.ordinal).to eq 'one hundred and nineteenth'
108
+ expect(subject.in_words).to eq 'one one hundred and nineteenth'
109
+ end
110
+ end
111
+
112
+ context 'one thousandth' do
113
+ let(:denominator) { 1000 }
114
+ let(:attributes) { {} }
115
+
116
+ it do
117
+ expect(subject.ordinal).to eq 'one thousandth'
118
+ expect(subject.in_words).to eq 'one one thousandth'
119
+ end
120
+
121
+ context 'plural' do
122
+ let(:numerator) { 2 }
123
+ let(:denominator) { 1000 }
124
+
125
+ it do
126
+ expect(subject.in_words).to eq 'two thousandths'
127
+ end
128
+ end
129
+ end
130
+
131
+ context 'googolplexths' do
132
+ let(:denominator) { Float::INFINITY }
133
+
134
+ let(:attributes) do
135
+ { number: 'googolplex',
136
+ ordinal: 'googolplexth',
137
+ fraction: { singular: 'googolplexth', plural: 'googolplexths' } }
138
+ end
139
+
140
+ it do
141
+ expect(subject.in_words).to eq 'one infinitieth'
142
+ end
143
+
144
+ context 'with plural' do
145
+ let(:numerator) { 2 }
146
+
147
+ it do
148
+ expect(subject.in_words).to eq 'two infinitieths'
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe NumbersInWords do
4
+ FRACTION_EXAMPLES = {
5
+ 'half' => 0.5,
6
+ 'a half' => 0.5,
7
+ 'one half' => 0.5,
8
+ 'two halves' => 1.0,
9
+ 'three halves' => 1.5,
10
+ 'three quarters' => 0.75,
11
+ 'three fifths' => 3 / 5.0,
12
+ 'seven fifteenths' => 7 / 15.0
13
+ }.freeze
14
+
15
+ PENDING = {
16
+ 'twenty and three fifteenths' => 20 + 3 / 15.0,
17
+ 'three ninety-sevenths' => 3 / 97.0
18
+ }.freeze
19
+
20
+ FRACTION_EXAMPLES.each do |string, float|
21
+ it "#{string} == #{float}" do
22
+ expect(string.in_numbers).to eql(float)
23
+ end
24
+ end
25
+
26
+ PENDING.each do |string, float|
27
+ pending "#{string} == #{float}" do
28
+ expect(string.in_numbers).to eql(float)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'NumbersInWords' do # rubocop: disable Metrics/BlockLength
6
+ describe '.in_words' do
7
+ it do
8
+ expect(NumbersInWords.in_words(100)).to eq 'one hundred'
9
+ expect(NumbersInWords.in_words(-100)).to eq 'minus one hundred'
10
+ expect(NumbersInWords.in_words(24)).to eq 'twenty-four'
11
+ expect(NumbersInWords.in_words(1.2)).to eq 'one point two'
12
+ expect(NumbersInWords.in_words(100 * 10**100)).to eq 'one hundred googol'
13
+ expect(NumbersInWords.in_words(30 + 100 * 10**100)).to eq 'one hundred googol and thirty'
14
+ end
15
+ end
16
+
17
+ FRACTION_TO_WORDS = {
18
+ [1, 2] => 'one half',
19
+ [3, 2] => 'three halves',
20
+ [1, 3] => 'one third',
21
+ [1, 4] => 'one quarter',
22
+ [1, 5] => 'one fifth',
23
+ [1, 19] => 'one nineteenth',
24
+ [2, 17] => 'two seventeenths',
25
+ [1, 21] => 'one twenty-first',
26
+ [1, 32] => 'one thirty-second',
27
+ [1, 101] => 'one one hundred and first',
28
+ [3, 101] => 'three hundred and firsts',
29
+ [73, 102] => 'seventy-three hundred and seconds',
30
+ [13, 97] => 'thirteen ninety-sevenths'
31
+ }.freeze
32
+
33
+ FRACTION_TO_WORDS.each do |(numerator, denominator), string|
34
+ it "#{numerator}/#{denominator} == #{string}" do
35
+ expect(NumbersInWords.in_words(numerator.to_f / denominator, fraction: true)).to eql(string)
36
+ end
37
+ end
38
+
39
+ describe '.in_numbers' do
40
+ it do
41
+ expect(NumbersInWords.in_numbers('hundred')).to eq 100
42
+ expect(NumbersInWords.in_numbers('one hundred')).to eq 100
43
+
44
+ expect(NumbersInWords.in_numbers('minus one hundred')).to eq(-100)
45
+ expect(NumbersInWords.in_numbers('twenty four')).to eq 24
46
+ expect(NumbersInWords.in_numbers('one point two')).to eq 1.2
47
+ expect(NumbersInWords.in_numbers('one hundred googol')).to eq 100 * 10**100
48
+ expect(NumbersInWords.in_numbers('one hundred googol and thirty')).to eq 30 + 100 * 10**100
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './spec/spec_helper'
4
+ require 'numbers_in_words/number_group'
5
+
6
+ describe NumbersInWords::NumberGroup do
7
+ it 'should split into group of three digit numbers' do
8
+ g = described_class
9
+ expect(g.groups_of(1, 3)).to eq({ 0 => 1 })
10
+ expect(g.groups_of(12, 3)).to eq({ 0 => 12 })
11
+ expect(g.groups_of(123, 3)).to eq({ 0 => 123 })
12
+ expect(g.groups_of(1111, 3)).to eq({ 3 => 1, 0 => 111 })
13
+ expect(g.groups_of(87_654, 3)).to eq({ 3 => 87, 0 => 654 })
14
+ expect(g.groups_of(1_234_567, 3)).to eq({ 6 => 1, 3 => 234, 0 => 567 })
15
+ expect(g.groups_of(123_456_789_101_112, 3)).to eq({ 12 => 123, 9 => 456, 6 => 789, 3 => 101, 0 => 112 })
16
+ end
17
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './spec/spec_helper'
4
+ require 'numbers_in_words/parsing/number_parser'
5
+
6
+ describe NumbersInWords::NumberParser do
7
+ subject do
8
+ super().parse(number)
9
+ end
10
+
11
+ context 'with single digit' do
12
+ let(:number) { [9] }
13
+ it { expect(subject).to eq 9 }
14
+ end
15
+
16
+ context 'with thousands' do
17
+ let(:number) { [20, 1000] }
18
+ it { expect(subject).to eq 20_000 }
19
+ end
20
+
21
+ context 'with thousands with a leading one' do
22
+ let(:number) { [1, 1000, 6, 100] }
23
+ it { expect(subject).to eq 1600 }
24
+ end
25
+
26
+ context 'with hundreds' do
27
+ let(:number) { [2, 100, 45] }
28
+
29
+ it { expect(subject).to eq 245 }
30
+ end
31
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './spec/spec_helper'
4
+
5
+ describe NumbersInWords do # rubocop: disable Metrics/BlockLength
6
+ describe '.in_words' do
7
+ it do
8
+ expect(NumbersInWords.in_words(100)).to eq 'one hundred'
9
+ end
10
+ end
11
+
12
+ describe '.in_numbers' do
13
+ it do
14
+ expect(NumbersInWords.in_numbers('one-hundred')).to eq 100
15
+ end
16
+ end
17
+
18
+ it 'should print the digits 0-9 correctly' do
19
+ numbers = %w[zero one two three four five six seven eight nine]
20
+
21
+ 10.times { |i| expect(i.in_words).to eq(numbers[i]) }
22
+ end
23
+
24
+ it 'should print the digits 11-19 correctly' do
25
+ words = %w[eleven twelve thirteen fourteen fifteen sixteen seventeen eighteen nineteen]
26
+ numbers = [11, 12, 13, 14, 15, 16, 17, 18, 19]
27
+ numbers.each_with_index do |number, index|
28
+ expect(number.in_words).to eq(words[index])
29
+ end
30
+ end
31
+
32
+ it 'should handle two digit numbers' do
33
+ expect(21.in_words).to eq('twenty-one')
34
+ end
35
+
36
+ it 'should handle three digit numbers' do
37
+ expect(113.in_words).to eq('one hundred and thirteen')
38
+ expect(299.in_words).to eq('two hundred and ninety-nine')
39
+ expect(300.in_words).to eq('three hundred')
40
+ expect(101.in_words).to eq('one hundred and one')
41
+ end
42
+
43
+ it 'should print out some random examples correctly' do
44
+ expect(2999.in_words).to eq('two thousand nine hundred and ninety-nine')
45
+ expect(99_999.in_words).to eq('ninety-nine thousand nine hundred and ninety-nine')
46
+ expect(999_999.in_words).to eq('nine hundred and ninety-nine thousand nine hundred and ninety-nine')
47
+ expect(123_456.in_words).to eq('one hundred and twenty-three thousand four hundred and fifty-six')
48
+ expect(24_056.in_words).to eq('twenty-four thousand and fifty-six')
49
+ expect(17_054.in_words).to eq('seventeen thousand and fifty-four')
50
+ expect(11_004.in_words).to eq('eleven thousand and four')
51
+ expect(470_154.in_words).to eq('four hundred and seventy thousand one hundred and fifty-four')
52
+ expect(417_155.in_words).to eq('four hundred and seventeen thousand one hundred and fifty-five')
53
+ expect(999_999.in_words).to eq('nine hundred and ninety-nine thousand nine hundred and ninety-nine')
54
+ expect(1_000_000.in_words).to eq('one million')
55
+ expect(1_000_001.in_words).to eq('one million and one')
56
+ expect(112.in_words).to eq('one hundred and twelve')
57
+ end
58
+
59
+ it 'should handle edge cases' do
60
+ expect(1_000_001.in_words).to eq('one million and one')
61
+ expect((10 * 10**12 + 10**6 + 1).in_words).to eq('ten trillion one million and one')
62
+ expect((10**75).in_words).to eq('one quattuorvigintillion')
63
+ expect(10_001_001.in_words).to eq('ten million one thousand and one')
64
+ end
65
+
66
+ it 'should handle a googol and larger' do
67
+ googol = 10**100
68
+ expect((googol + 1).in_words).to eq('one googol and one')
69
+ expect((42 * googol + 16_777_216).in_words)
70
+ .to eq('forty-two googol sixteen million seven hundred and seventy-seven thousand two hundred and sixteen')
71
+ expect((42 * googol * googol).in_words).to eq('forty-two googol googol')
72
+ end
73
+
74
+ it 'should handle negative numbers' do
75
+ expect(-1.in_words).to eq('minus one')
76
+ expect(-9.in_words).to eq('minus nine')
77
+ expect(-10.in_words).to eq('minus ten')
78
+ expect(-15.in_words).to eq('minus fifteen')
79
+ expect(-100.in_words).to eq('minus one hundred')
80
+ expect((-1 * (10**100)).in_words).to eq('minus one googol')
81
+
82
+ expect(-123_456_789.in_words)
83
+ .to eq('minus one hundred and twenty-three million four hundred and ' \
84
+ 'fifty-six thousand seven hundred and eighty-nine')
85
+ end
86
+
87
+ it 'should handle decimals' do
88
+ # because of lack of absolute accuracy with floats
89
+ # the output won't be exactly as you might expect
90
+ # so we will match rather than find equivalents
91
+ expect(1.1.in_words).to match(/one point one/)
92
+ expect(1.2345678.in_words).to match(/one point two three four five six seven eight/)
93
+ expect(1000.2345678.in_words).to match(/one thousand point two three four five six seven eight/)
94
+ expect(12_345.2345678.in_words)
95
+ .to match(/twelve thousand three hundred and forty-five point two three four five six seven eight/)
96
+
97
+ expect((10**9 + 0.1).in_words).to match(/one billion point one/)
98
+ end
99
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './spec/spec_helper'
4
+
5
+ describe NumbersInWords do
6
+ it 'recognizes numerical strings' do
7
+ arr = %w[8 56 100 5789 3435356]
8
+ arr.each { |s| expect(s.in_numbers).to eql(s.to_f) }
9
+ end
10
+
11
+ MIXED = {
12
+ '19 hundred' => 1_900,
13
+ '20 thousand' => 20_000,
14
+ '100 million' => 100_000_000,
15
+ '7 billion' => 7_000_000_000,
16
+ '42 trillion' => 42_000_000_000_000,
17
+ '20 thousand and 4' => 20_004
18
+ }.freeze
19
+
20
+ MIXED.each do |k, v|
21
+ it do
22
+ expect(k.in_numbers).to eql(v)
23
+ end
24
+ end
25
+
26
+ PENDING_MIXED = {
27
+ '19 zero five' => 1_905
28
+ }.freeze
29
+
30
+ PENDING_MIXED.each do |k, v|
31
+ pending do
32
+ expect(k.in_numbers).to eql(v)
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ENV['CC_TEST_REPORTER_ID']
4
+ require 'simplecov'
5
+ SimpleCov.start
6
+ end
7
+
8
+ unless Kernel.method_defined?(:silence_warnings)
9
+ module Kernel
10
+ def silence_warnings
11
+ with_warnings(nil) { yield }
12
+ end
13
+
14
+ def with_warnings(flag)
15
+ old_verbose = $VERBOSE
16
+ $VERBOSE = flag
17
+ yield
18
+ ensure
19
+ $VERBOSE = old_verbose
20
+ end
21
+ end
22
+ end
23
+
24
+ require 'numbers_in_words'
25
+ require 'numbers_in_words/duck_punch'
26
+ require 'byebug'
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe NumbersInWords::ToWord do
4
+ let(:number) { 2_111 }
5
+ let(:writer) { described_class.new(number) }
6
+
7
+ describe '#in_words' do
8
+ let(:number) { 99 }
9
+
10
+ subject do
11
+ writer.in_words
12
+ end
13
+
14
+ it do
15
+ expect(subject).to eq 'ninety-nine'
16
+ end
17
+ end
18
+ end