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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +14 -0
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +58 -0
- data/.travis.yml +17 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +74 -0
- data/LICENSE.txt +24 -0
- data/NUMBERS_IN_WORDS.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +3 -0
- data/bin/spec +2 -0
- data/castwide_numbers_in_words.gemspec +24 -0
- data/lib/numbers_in_words/duck_punch.rb +23 -0
- data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
- data/lib/numbers_in_words/fraction.rb +136 -0
- data/lib/numbers_in_words/number_group.rb +64 -0
- data/lib/numbers_in_words/parsing/fraction_parsing.rb +31 -0
- data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
- data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
- data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
- data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
- data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
- data/lib/numbers_in_words/parsing/special.rb +67 -0
- data/lib/numbers_in_words/parsing/to_number.rb +77 -0
- data/lib/numbers_in_words/powers_of_ten.rb +49 -0
- data/lib/numbers_in_words/to_word.rb +84 -0
- data/lib/numbers_in_words/version.rb +5 -0
- data/lib/numbers_in_words/writer.rb +69 -0
- data/lib/numbers_in_words.rb +58 -0
- data/spec/exceptional_numbers_spec.rb +26 -0
- data/spec/fraction_spec.rb +152 -0
- data/spec/fractions_spec.rb +31 -0
- data/spec/non_monkey_patch_spec.rb +51 -0
- data/spec/number_group_spec.rb +17 -0
- data/spec/number_parser_spec.rb +31 -0
- data/spec/numbers_in_words_spec.rb +99 -0
- data/spec/numerical_strings_spec.rb +35 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/to_word_spec.rb +18 -0
- data/spec/words_in_numbers_spec.rb +152 -0
- data/spec/writer_spec.rb +26 -0
- data/spec/years_spec.rb +27 -0
- metadata +124 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
module FractionParsing
|
5
|
+
def fraction(text)
|
6
|
+
return unless possible_fraction?(text)
|
7
|
+
|
8
|
+
NumbersInWords.exceptional_numbers.lookup_fraction(text)
|
9
|
+
end
|
10
|
+
|
11
|
+
def strip_punctuation(text)
|
12
|
+
text.downcase
|
13
|
+
.gsub(/[^a-z 0-9]/, ' ')
|
14
|
+
.gsub(/ +/, ' ')
|
15
|
+
end
|
16
|
+
|
17
|
+
def possible_fraction?(text)
|
18
|
+
words = text.split(' ')
|
19
|
+
result = words & NumbersInWords.exceptional_numbers.fraction_names
|
20
|
+
result.length.positive?
|
21
|
+
end
|
22
|
+
|
23
|
+
def text_including_punctuation
|
24
|
+
to_s.strip
|
25
|
+
end
|
26
|
+
|
27
|
+
def text
|
28
|
+
strip_punctuation text_including_punctuation
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'parse_fractions'
|
5
|
+
require_relative 'parse_status'
|
6
|
+
require_relative 'parse_individual_number'
|
7
|
+
require_relative 'pair_parsing'
|
8
|
+
|
9
|
+
module NumbersInWords
|
10
|
+
class NumberParser
|
11
|
+
# Example: 364,895,457,898
|
12
|
+
# three hundred and sixty four billion eight hundred and ninety five million
|
13
|
+
# four hundred and fifty seven thousand eight hundred and ninety eight
|
14
|
+
#
|
15
|
+
# 3 100 60 4 10^9, 8 100 90 5 10^6, 4 100 50 7 1000, 8 100 90 8
|
16
|
+
# memory answer
|
17
|
+
# x1. 3 add to memory because answer and memory are zero 3 0
|
18
|
+
# x2. memory * 100 (because memory<100) 300 0
|
19
|
+
# x3. 60 add to memory because memory > 60 360 0
|
20
|
+
# x3. 4 add to memory because memory > 4 364 0
|
21
|
+
# x4. multiply memory by 10^9 because memory < power of ten 364*10^9 0
|
22
|
+
# x5. add memory to answer (and reset)memory > 8 (memory pow of ten > 2) 0 364*10^9
|
23
|
+
# x6. 8 add to memory because not finished 8 ''
|
24
|
+
# x7. multiply memory by 100 because memory < 100 800 ''
|
25
|
+
# x8. add 90 to memory because memory > 90 890 ''
|
26
|
+
# x9. add 5 to memory because memory > 5 895 ''
|
27
|
+
# x10. multiply memory by 10^6 because memory < power of ten 895*10^6 ''
|
28
|
+
# x11. add memory to answer (and reset) because memory power ten > 2 0 364895 * 10^6
|
29
|
+
# x12. 4 add to memory because not finished 4 ''
|
30
|
+
# x13. memory * 100 because memory < 100 400 ''
|
31
|
+
# x14. memory + 50 because memory > 50 450 ''
|
32
|
+
# x15. memory + 7 because memory > 7 457 ''
|
33
|
+
# x16. memory * 1000 because memory < 1000 457000 ''
|
34
|
+
# x17. add memory to answer (and reset)memory > 8 (memory pow of ten > 2) 0 364895457000
|
35
|
+
# x18. 8 add to memory because not finished 8 ''
|
36
|
+
# x19. memory * 100 because memory < 100 800 ''
|
37
|
+
# x14. memory + 90 because memory > 90 890 ''
|
38
|
+
# x15. memory + 8 because memory > 8 898 ''
|
39
|
+
# 16. finished so add memory to answer
|
40
|
+
|
41
|
+
# Example
|
42
|
+
# 2001
|
43
|
+
# two thousand and one
|
44
|
+
# 2 1000 1
|
45
|
+
# memory answer
|
46
|
+
# 1. add 2 to memory because first 2 0
|
47
|
+
# 2. multiply memory by 1000 because memory < 1000 2000 0
|
48
|
+
# 3. add memory to answer,reset, because power of ten>2 0 2000
|
49
|
+
# 4. add 1 to memory 1 2000
|
50
|
+
# 5. finish - add memory to answer 0 2001
|
51
|
+
|
52
|
+
SCALES_N = [10**2, 10**3, 10**6, 10**9, 10**12, 10**100].freeze
|
53
|
+
|
54
|
+
def parse(nums, only_compress: false)
|
55
|
+
fractions(nums) ||
|
56
|
+
small_numbers(nums, only_compress) ||
|
57
|
+
pair_parsing(nums, only_compress) ||
|
58
|
+
parse_each(nums)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def fractions(nums)
|
64
|
+
ParseFractions.new(nums).call
|
65
|
+
end
|
66
|
+
|
67
|
+
# 7 0.066666666666667 => 0.46666666666666
|
68
|
+
|
69
|
+
# 15 => 15
|
70
|
+
def small_numbers(nums, only_compress)
|
71
|
+
return unless nums.length < 2
|
72
|
+
return nums if only_compress
|
73
|
+
|
74
|
+
nums.empty? ? 0 : nums[0]
|
75
|
+
end
|
76
|
+
|
77
|
+
# 15 75 => 1,575
|
78
|
+
def pair_parsing(nums, only_compress)
|
79
|
+
return if (SCALES_N & nums).any?
|
80
|
+
|
81
|
+
pair_parse(nums, only_compress)
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_each(nums)
|
85
|
+
status = ParseStatus.new
|
86
|
+
|
87
|
+
nums.each do |num|
|
88
|
+
ParseIndividualNumber.new(status, num).call
|
89
|
+
end
|
90
|
+
|
91
|
+
status.calculate
|
92
|
+
end
|
93
|
+
|
94
|
+
def pair_parse(nums, only_compress)
|
95
|
+
PairParsing.new(nums, only_compress).pair_parse
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class PairParsing
|
5
|
+
attr_accessor :ints
|
6
|
+
attr_reader :only_compress
|
7
|
+
|
8
|
+
def initialize(ints, only_compress)
|
9
|
+
@ints = ints
|
10
|
+
@only_compress = only_compress
|
11
|
+
end
|
12
|
+
|
13
|
+
# 15,16
|
14
|
+
# 85,16
|
15
|
+
def pair_parse
|
16
|
+
ints = compressed
|
17
|
+
return ints if only_compress
|
18
|
+
|
19
|
+
return ints[0] if ints.length == 1
|
20
|
+
|
21
|
+
sum = 0
|
22
|
+
|
23
|
+
ints.each do |n|
|
24
|
+
sum *= n >= 10 ? 100 : 10
|
25
|
+
sum += n
|
26
|
+
end
|
27
|
+
|
28
|
+
sum
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# [40, 2] => [42]
|
34
|
+
def compressed
|
35
|
+
return [] if ints.empty?
|
36
|
+
|
37
|
+
result = []
|
38
|
+
index = 0
|
39
|
+
|
40
|
+
index, result = compress_numbers(result, index)
|
41
|
+
|
42
|
+
result << ints[-1] if index < ints.length
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def compress_numbers(result, index)
|
48
|
+
while index < ints.length - 1
|
49
|
+
int, jump = compress_int(ints[index], ints[index + 1])
|
50
|
+
result << int
|
51
|
+
index += jump
|
52
|
+
end
|
53
|
+
|
54
|
+
[index, result]
|
55
|
+
end
|
56
|
+
|
57
|
+
def compress_int(int, next_int)
|
58
|
+
tens = (int % 10).zero? && int > 10
|
59
|
+
return [int + next_int, 2] if tens && next_int < 10
|
60
|
+
|
61
|
+
[int, 1]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class ParseFractions
|
5
|
+
attr_reader :nums
|
6
|
+
|
7
|
+
def initialize(nums)
|
8
|
+
@nums = nums.map(&:to_f)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
return if no_fractions?
|
13
|
+
|
14
|
+
just_fraction || calculate
|
15
|
+
end
|
16
|
+
|
17
|
+
def calculate
|
18
|
+
(parse(numbers) * parse(fractions)).rationalize(EPSILON).to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(numbers)
|
22
|
+
NumberParser.new.parse(numbers)
|
23
|
+
end
|
24
|
+
|
25
|
+
def numbers
|
26
|
+
nums[0..index_of_fraction - 1]
|
27
|
+
end
|
28
|
+
|
29
|
+
def fractions
|
30
|
+
nums[index_of_fraction..-1]
|
31
|
+
end
|
32
|
+
|
33
|
+
def just_fraction
|
34
|
+
return nums.first if index_of_fraction.zero?
|
35
|
+
end
|
36
|
+
|
37
|
+
def index_of_fraction
|
38
|
+
nums.index { |n| n < 1.0 }
|
39
|
+
end
|
40
|
+
|
41
|
+
def no_fractions?
|
42
|
+
nums.all? { |n| n.zero? || n >= 1.0 }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class ParseIndividualNumber
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :parse_status, :reset=, :memory=, :answer=, :reset, :memory, :answer
|
7
|
+
|
8
|
+
attr_reader :parse_status, :num
|
9
|
+
|
10
|
+
def initialize(parse_status, num)
|
11
|
+
@parse_status = parse_status
|
12
|
+
@num = num
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
if reset
|
17
|
+
clear
|
18
|
+
else
|
19
|
+
handle_power_of_ten
|
20
|
+
|
21
|
+
update_memory
|
22
|
+
end
|
23
|
+
|
24
|
+
[reset, memory, answer]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def clear
|
30
|
+
self.reset = false
|
31
|
+
self.memory += num
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle_power_of_ten
|
35
|
+
# x4. multiply memory by 10^9 because memory < power of ten
|
36
|
+
return unless power_of_ten?(num)
|
37
|
+
return unless power_of_ten(num) > 2
|
38
|
+
|
39
|
+
self.memory *= num
|
40
|
+
# 17. add memory to answer (and reset) (memory pow of ten > 2)
|
41
|
+
self.answer += memory
|
42
|
+
self.memory = 0
|
43
|
+
self.reset = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def update_memory
|
47
|
+
self.memory = new_memory
|
48
|
+
end
|
49
|
+
|
50
|
+
def new_memory
|
51
|
+
if memory < num
|
52
|
+
memory * num
|
53
|
+
else
|
54
|
+
memory + num
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def power_of_ten(integer)
|
59
|
+
Math.log10(integer)
|
60
|
+
end
|
61
|
+
|
62
|
+
def power_of_ten?(integer)
|
63
|
+
return true if integer.zero?
|
64
|
+
|
65
|
+
power_of_ten(integer) == power_of_ten(integer).to_i
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class ParseStatus
|
5
|
+
attr_accessor :reset, :memory, :answer
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@reset = true
|
9
|
+
@memory = 0
|
10
|
+
@answer = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def calculate
|
14
|
+
answer + memory
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'fraction_parsing'
|
4
|
+
|
5
|
+
module NumbersInWords
|
6
|
+
class Special
|
7
|
+
extend Forwardable
|
8
|
+
def_delegator :that, :to_s
|
9
|
+
|
10
|
+
include FractionParsing
|
11
|
+
|
12
|
+
attr_reader :that, :only_compress
|
13
|
+
|
14
|
+
def initialize(that, only_compress)
|
15
|
+
@that = that
|
16
|
+
@only_compress = only_compress
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
float ||
|
21
|
+
negative ||
|
22
|
+
fraction(that) ||
|
23
|
+
mixed_words_and_digits ||
|
24
|
+
one
|
25
|
+
end
|
26
|
+
|
27
|
+
def float
|
28
|
+
text_including_punctuation.to_f if text =~ /^-?\d+(.\d+)?$/
|
29
|
+
end
|
30
|
+
|
31
|
+
def negative
|
32
|
+
stripped = strip_minus text
|
33
|
+
return unless stripped
|
34
|
+
|
35
|
+
stripped_n = NumbersInWords.in_numbers(stripped, only_compress: only_compress)
|
36
|
+
only_compress ? stripped_n.map { |k| k * -1 } : -1 * stripped_n
|
37
|
+
end
|
38
|
+
|
39
|
+
def mixed_words_and_digits
|
40
|
+
return unless numeric?(that)
|
41
|
+
|
42
|
+
in_words = that.split(' ').map { |word| numeric?(word) ? NumbersInWords.in_words(word) : word }.join(' ')
|
43
|
+
ToNumber.new(in_words, only_compress).call
|
44
|
+
end
|
45
|
+
|
46
|
+
def numeric?(word)
|
47
|
+
word.match(/\d+/)
|
48
|
+
end
|
49
|
+
|
50
|
+
def strip_minus(txt)
|
51
|
+
txt.gsub(/^minus/, '') if txt =~ /^minus/
|
52
|
+
end
|
53
|
+
|
54
|
+
def one
|
55
|
+
one = check_one text
|
56
|
+
|
57
|
+
return unless one
|
58
|
+
|
59
|
+
res = NumbersInWords.in_numbers(one[1])
|
60
|
+
only_compress ? [res] : res
|
61
|
+
end
|
62
|
+
|
63
|
+
def check_one(txt)
|
64
|
+
txt.match(/^one (#{POWERS_RX})$/)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'special'
|
4
|
+
require_relative 'fraction_parsing'
|
5
|
+
|
6
|
+
module NumbersInWords
|
7
|
+
class ToNumber
|
8
|
+
include FractionParsing
|
9
|
+
extend Forwardable
|
10
|
+
def_delegator :that, :to_s
|
11
|
+
|
12
|
+
attr_reader :that, :only_compress
|
13
|
+
|
14
|
+
def initialize(that, only_compress)
|
15
|
+
@that = that
|
16
|
+
@only_compress = only_compress
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
special || decimal || as_numbers
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def special
|
26
|
+
Special.new(that, only_compress).call
|
27
|
+
end
|
28
|
+
|
29
|
+
def decimal
|
30
|
+
match = check_decimal text
|
31
|
+
return unless match
|
32
|
+
|
33
|
+
integer = NumbersInWords.in_numbers(match.pre_match)
|
34
|
+
decimal = NumbersInWords.in_numbers(match.post_match)
|
35
|
+
integer + "0.#{decimal}".to_f
|
36
|
+
end
|
37
|
+
|
38
|
+
def as_numbers
|
39
|
+
numbers = word_array_to_nums text.split(' ')
|
40
|
+
|
41
|
+
NumbersInWords::NumberParser.new.parse numbers, only_compress: only_compress
|
42
|
+
end
|
43
|
+
|
44
|
+
def word_array_to_nums(words)
|
45
|
+
words.map { |i| word_to_num(i) }.compact
|
46
|
+
end
|
47
|
+
|
48
|
+
# handles simple single word numbers
|
49
|
+
# e.g. one, seven, twenty, eight, thousand etc
|
50
|
+
def word_to_num(word)
|
51
|
+
text = canonize(word.to_s.chomp.strip)
|
52
|
+
|
53
|
+
NumbersInWords.exceptional_number(text) || fraction(text) || power(text)
|
54
|
+
end
|
55
|
+
|
56
|
+
def power(text)
|
57
|
+
power = NumbersInWords.power_of_ten(text)
|
58
|
+
|
59
|
+
10**power if power
|
60
|
+
end
|
61
|
+
|
62
|
+
def canonize(word)
|
63
|
+
aliases[word] || word
|
64
|
+
end
|
65
|
+
|
66
|
+
def aliases
|
67
|
+
{
|
68
|
+
'a' => 'one',
|
69
|
+
'oh' => 'zero'
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def check_decimal(txt)
|
74
|
+
txt.match(/\spoint\s/)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
GOOGOL = 10**100
|
5
|
+
|
6
|
+
POWERS_OF_TEN = {
|
7
|
+
0 => 'one',
|
8
|
+
1 => 'ten',
|
9
|
+
2 => 'hundred',
|
10
|
+
1 * 3 => 'thousand',
|
11
|
+
2 * 3 => 'million',
|
12
|
+
3 * 3 => 'billion',
|
13
|
+
4 * 3 => 'trillion',
|
14
|
+
5 * 3 => 'quadrillion',
|
15
|
+
6 * 3 => 'quintillion',
|
16
|
+
7 * 3 => 'sextillion',
|
17
|
+
8 * 3 => 'septillion',
|
18
|
+
9 * 3 => 'octillion',
|
19
|
+
10 * 3 => 'nonillion',
|
20
|
+
11 * 3 => 'decillion',
|
21
|
+
12 * 3 => 'undecillion',
|
22
|
+
13 * 3 => 'duodecillion',
|
23
|
+
14 * 3 => 'tredecillion',
|
24
|
+
15 * 3 => 'quattuordecillion',
|
25
|
+
16 * 3 => 'quindecillion',
|
26
|
+
17 * 3 => 'sexdecillion',
|
27
|
+
18 * 3 => 'septendecillion',
|
28
|
+
19 * 3 => 'octodecillion',
|
29
|
+
20 * 3 => 'novemdecillion',
|
30
|
+
21 * 3 => 'vigintillion',
|
31
|
+
22 * 3 => 'unvigintillion',
|
32
|
+
23 * 3 => 'duovigintillion',
|
33
|
+
24 * 3 => 'trevigintillion',
|
34
|
+
25 * 3 => 'quattuorvigintillion',
|
35
|
+
26 * 3 => 'quinvigintillion',
|
36
|
+
27 * 3 => 'sexvigintillion',
|
37
|
+
28 * 3 => 'septenvigintillion',
|
38
|
+
29 * 3 => 'octovigintillion',
|
39
|
+
30 * 3 => 'novemvigintillion',
|
40
|
+
31 * 3 => 'trigintillion',
|
41
|
+
32 * 3 => 'untrigintillion',
|
42
|
+
33 * 3 => 'duotrigintillion',
|
43
|
+
100 => 'googol',
|
44
|
+
101 * 3 => 'centillion',
|
45
|
+
GOOGOL => 'googolplex'
|
46
|
+
}.freeze
|
47
|
+
|
48
|
+
POWERS_RX = Regexp.union(POWERS_OF_TEN.values[1..-1]).freeze
|
49
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'writer'
|
4
|
+
require_relative 'number_group'
|
5
|
+
require_relative 'fraction'
|
6
|
+
|
7
|
+
module NumbersInWords
|
8
|
+
# Arbitrarily small number for rationalizing fractions
|
9
|
+
EPSILON = 0.0000000001
|
10
|
+
|
11
|
+
class ToWord
|
12
|
+
attr_reader :that
|
13
|
+
|
14
|
+
def initialize(that)
|
15
|
+
@that = that
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_i
|
19
|
+
that.to_i
|
20
|
+
end
|
21
|
+
|
22
|
+
def negative
|
23
|
+
return unless to_i.negative?
|
24
|
+
|
25
|
+
'minus ' + NumbersInWords.in_words(-@that)
|
26
|
+
end
|
27
|
+
|
28
|
+
def in_words(fraction: false)
|
29
|
+
as_fraction(fraction) ||
|
30
|
+
handle_exceptional_numbers ||
|
31
|
+
decimals ||
|
32
|
+
negative ||
|
33
|
+
output
|
34
|
+
end
|
35
|
+
|
36
|
+
def as_fraction(fraction)
|
37
|
+
return Fraction.in_words(that) if fraction
|
38
|
+
end
|
39
|
+
|
40
|
+
def decimals
|
41
|
+
int, decimals = NumberGroup.new(@that).split_decimals
|
42
|
+
return unless int
|
43
|
+
|
44
|
+
out = NumbersInWords.in_words(int) + ' point '
|
45
|
+
decimals.each do |decimal|
|
46
|
+
out = out + NumbersInWords.in_words(decimal.to_i) + ' '
|
47
|
+
end
|
48
|
+
out.strip
|
49
|
+
end
|
50
|
+
|
51
|
+
def output
|
52
|
+
output = if to_i.to_s.length == 2 # 20-99
|
53
|
+
handle_tens(to_i)
|
54
|
+
else
|
55
|
+
Writer.new(that).call # longer numbers
|
56
|
+
end
|
57
|
+
|
58
|
+
output.strip
|
59
|
+
end
|
60
|
+
|
61
|
+
def handle_tens(number)
|
62
|
+
output = ''
|
63
|
+
|
64
|
+
tens = (number / 10).round * 10 # write the tens
|
65
|
+
|
66
|
+
output += NumbersInWords.lookup(tens).to_s # e.g. eighty
|
67
|
+
|
68
|
+
digit = number - tens # write the digits
|
69
|
+
|
70
|
+
unless digit.zero?
|
71
|
+
join = number < 100 ? '-' : ' '
|
72
|
+
output = output + join + NumbersInWords.in_words(digit)
|
73
|
+
end
|
74
|
+
|
75
|
+
output
|
76
|
+
end
|
77
|
+
|
78
|
+
def handle_exceptional_numbers
|
79
|
+
return unless @that.is_a?(Integer)
|
80
|
+
|
81
|
+
NumbersInWords.exceptional_numbers.lookup(@that)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class Writer
|
5
|
+
def initialize(that)
|
6
|
+
@that = that
|
7
|
+
end
|
8
|
+
|
9
|
+
def call
|
10
|
+
length = @that.to_s.length
|
11
|
+
output =
|
12
|
+
if length == 3
|
13
|
+
# e.g. 113 splits into "one hundred" and "thirteen"
|
14
|
+
write_groups(2)
|
15
|
+
|
16
|
+
# more than one hundred less than one googol
|
17
|
+
elsif length < LENGTH_OF_GOOGOL
|
18
|
+
write_groups(3)
|
19
|
+
|
20
|
+
elsif length >= LENGTH_OF_GOOGOL
|
21
|
+
write_googols
|
22
|
+
end
|
23
|
+
output.strip
|
24
|
+
end
|
25
|
+
|
26
|
+
def group_words(size)
|
27
|
+
# 1000 and over Numbers are split into groups of three
|
28
|
+
groups = NumberGroup.groups_of @that, size
|
29
|
+
powers = groups.keys.sort.reverse # put in descending order
|
30
|
+
|
31
|
+
powers.each do |power|
|
32
|
+
name = NumbersInWords::POWERS_OF_TEN[power]
|
33
|
+
digits = groups[power]
|
34
|
+
yield power, name, digits
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def write_googols
|
41
|
+
googols, remainder = NumberGroup.new(@that).split_googols
|
42
|
+
output = ''
|
43
|
+
|
44
|
+
output = output + ' ' + NumbersInWords.in_words(googols) + ' googol'
|
45
|
+
if remainder.positive?
|
46
|
+
prefix = ' '
|
47
|
+
prefix += 'and ' if remainder < 100
|
48
|
+
output = output + prefix + NumbersInWords.in_words(remainder)
|
49
|
+
end
|
50
|
+
|
51
|
+
output
|
52
|
+
end
|
53
|
+
|
54
|
+
def write_groups(group)
|
55
|
+
# e.g. 113 splits into "one hundred" and "thirteen"
|
56
|
+
output = ''
|
57
|
+
group_words(group) do |power, name, digits|
|
58
|
+
if digits.positive?
|
59
|
+
prefix = ' '
|
60
|
+
# no and between thousands and hundreds
|
61
|
+
prefix += 'and ' if power.zero? && (digits < 100)
|
62
|
+
output = output + prefix + NumbersInWords.in_words(digits)
|
63
|
+
output = output + prefix + name unless power.zero?
|
64
|
+
end
|
65
|
+
end
|
66
|
+
output
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|