numbers_in_words 0.4.0 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -1148
- data/.travis.yml +4 -4
- data/Gemfile +3 -1
- data/README.md +5 -43
- data/Rakefile +3 -1
- data/lib/numbers_in_words.rb +43 -19
- data/lib/numbers_in_words/duck_punch.rb +12 -8
- data/lib/numbers_in_words/exceptional_numbers.rb +119 -0
- data/lib/numbers_in_words/fraction.rb +151 -0
- data/lib/numbers_in_words/number_group.rb +34 -21
- 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/to_number.rb +159 -0
- data/lib/numbers_in_words/powers_of_ten.rb +49 -0
- data/lib/numbers_in_words/to_word.rb +78 -13
- data/lib/numbers_in_words/version.rb +3 -1
- data/lib/numbers_in_words/writer.rb +69 -0
- data/numbers_in_words.gemspec +14 -13
- data/spec/exceptional_numbers_spec.rb +26 -0
- data/spec/fraction_spec.rb +132 -0
- data/spec/fractions_spec.rb +31 -0
- data/spec/non_monkey_patch_spec.rb +39 -20
- data/spec/number_group_spec.rb +12 -12
- data/spec/number_parser_spec.rb +63 -0
- data/spec/numbers_in_words_spec.rb +56 -69
- data/spec/numerical_strings_spec.rb +28 -12
- data/spec/spec_helper.rb +2 -4
- data/spec/to_word_spec.rb +18 -0
- data/spec/words_in_numbers_spec.rb +130 -125
- data/spec/writer_spec.rb +26 -0
- data/spec/years_spec.rb +23 -13
- metadata +40 -25
- data/lib/numbers_in_words/english/constants.rb +0 -124
- data/lib/numbers_in_words/english/language_writer_english.rb +0 -116
- data/lib/numbers_in_words/language_writer.rb +0 -30
- data/lib/numbers_in_words/number_parser.rb +0 -135
- data/lib/numbers_in_words/to_number.rb +0 -88
- data/spec/language_writer_spec.rb +0 -23
@@ -1,51 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module NumbersInWords
|
2
4
|
class NumberGroup
|
3
5
|
include Enumerable
|
4
6
|
attr_accessor :number
|
5
7
|
|
6
|
-
def self.groups_of
|
8
|
+
def self.groups_of(number, size)
|
7
9
|
new(number).groups(size)
|
8
10
|
end
|
9
11
|
|
10
|
-
def initialize
|
12
|
+
def initialize(number)
|
11
13
|
@number = number
|
12
14
|
end
|
13
15
|
|
14
|
-
#split into groups this gives us 1234567 => 123 456 7
|
15
|
-
#so we need to reverse first
|
16
|
-
#in stages
|
17
|
-
def groups
|
18
|
-
#1234567 => %w(765 432 1)
|
19
|
-
@array = @number.to_s.reverse.split(
|
20
|
-
|
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)
|
21
23
|
@array.reverse!
|
22
24
|
|
23
|
-
|
24
|
-
@array.map! {|group| group.reverse.join(
|
25
|
+
# %w(1 432 765) => [1, 234, 567]
|
26
|
+
@array.map! { |group| group.reverse.join('').to_i }
|
25
27
|
@array.reverse! # put in ascending order of power of ten
|
26
28
|
|
27
29
|
power = 0
|
28
30
|
|
29
|
-
#[1, 234, 567] => {6 => 1, 3 => 234, 0 => 567}
|
30
|
-
@array.
|
31
|
+
# [1, 234, 567] => {6 => 1, 3 => 234, 0 => 567}
|
32
|
+
@array.each_with_object({}) do |digits, o|
|
31
33
|
o[power] = digits
|
32
34
|
power += size
|
33
|
-
o
|
34
35
|
end
|
35
36
|
end
|
36
37
|
|
37
38
|
def split_decimals
|
38
|
-
|
39
|
-
int, decimal = @number.to_s.split "."
|
39
|
+
return unless @number.is_a? Float
|
40
40
|
|
41
|
-
|
42
|
-
|
41
|
+
int, decimal = @number.to_s.split '.'
|
42
|
+
|
43
|
+
[int.to_i, decimal.split(//).map(&:to_i)]
|
43
44
|
end
|
44
45
|
|
45
46
|
def split_googols
|
46
|
-
googols = @number.to_s[0
|
47
|
-
remainder = @number.to_s[(1-LENGTH_OF_GOOGOL)
|
48
|
-
|
47
|
+
googols = @number.to_s[0..-LENGTH_OF_GOOGOL].to_i
|
48
|
+
remainder = @number.to_s[(1 - LENGTH_OF_GOOGOL)..].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
|
49
62
|
end
|
50
63
|
end
|
51
64
|
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..]
|
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,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class ToNumber
|
5
|
+
extend Forwardable
|
6
|
+
def_delegator :that, :to_s
|
7
|
+
|
8
|
+
attr_reader :that, :only_compress
|
9
|
+
|
10
|
+
def initialize(that, only_compress)
|
11
|
+
@that = that
|
12
|
+
@only_compress = only_compress
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
special || decimal || as_numbers
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def special
|
22
|
+
float ||
|
23
|
+
negative ||
|
24
|
+
fraction(that) ||
|
25
|
+
mixed_words_and_digits ||
|
26
|
+
one ||
|
27
|
+
mixed
|
28
|
+
end
|
29
|
+
|
30
|
+
def float
|
31
|
+
return text_including_punctuation.to_f if text =~ /^-?\d+(.\d+)?$/
|
32
|
+
end
|
33
|
+
|
34
|
+
def text_including_punctuation
|
35
|
+
to_s.strip
|
36
|
+
end
|
37
|
+
|
38
|
+
def text
|
39
|
+
strip_punctuation text_including_punctuation
|
40
|
+
end
|
41
|
+
|
42
|
+
def negative
|
43
|
+
stripped = strip_minus text
|
44
|
+
return unless stripped
|
45
|
+
|
46
|
+
stripped_n = NumbersInWords.in_numbers(stripped, only_compress: only_compress)
|
47
|
+
only_compress ? stripped_n.map { |k| k * -1 } : -1 * stripped_n
|
48
|
+
end
|
49
|
+
|
50
|
+
def mixed_words_and_digits
|
51
|
+
return unless numeric?(that)
|
52
|
+
|
53
|
+
in_words = that.split(' ').map { |word| numeric?(word) ? NumbersInWords.in_words(word) : word }.join(' ')
|
54
|
+
self.class.new(in_words, only_compress).call
|
55
|
+
end
|
56
|
+
|
57
|
+
def numeric?(word)
|
58
|
+
word.match(/\d+/)
|
59
|
+
end
|
60
|
+
|
61
|
+
def strip_punctuation(text)
|
62
|
+
text = text.downcase.gsub(/[^a-z 0-9]/, ' ')
|
63
|
+
to_remove = true
|
64
|
+
|
65
|
+
to_remove = text.gsub! ' ', ' ' while to_remove
|
66
|
+
|
67
|
+
text
|
68
|
+
end
|
69
|
+
|
70
|
+
def one
|
71
|
+
one = check_one text
|
72
|
+
|
73
|
+
return unless one
|
74
|
+
|
75
|
+
res = NumbersInWords.in_numbers(one[1])
|
76
|
+
only_compress ? [res] : res
|
77
|
+
end
|
78
|
+
|
79
|
+
def mixed
|
80
|
+
check_mixed text
|
81
|
+
end
|
82
|
+
|
83
|
+
def decimal
|
84
|
+
match = check_decimal text
|
85
|
+
return unless match
|
86
|
+
|
87
|
+
integer = NumbersInWords.in_numbers(match.pre_match)
|
88
|
+
decimal = NumbersInWords.in_numbers(match.post_match)
|
89
|
+
integer + "0.#{decimal}".to_f
|
90
|
+
end
|
91
|
+
|
92
|
+
def as_numbers
|
93
|
+
numbers = word_array_to_nums text.split(' ')
|
94
|
+
|
95
|
+
NumbersInWords::NumberParser.new.parse numbers, only_compress: only_compress
|
96
|
+
end
|
97
|
+
|
98
|
+
def word_array_to_nums(words)
|
99
|
+
words.map { |i| word_to_num(i) }.compact
|
100
|
+
end
|
101
|
+
|
102
|
+
# handles simple single word numbers
|
103
|
+
# e.g. one, seven, twenty, eight, thousand etc
|
104
|
+
def word_to_num(word)
|
105
|
+
text = canonize(word.to_s.chomp.strip)
|
106
|
+
|
107
|
+
NumbersInWords.exceptional_number(text) || fraction(text) || power(text)
|
108
|
+
end
|
109
|
+
|
110
|
+
def fraction(text)
|
111
|
+
return unless possible_fraction?(text)
|
112
|
+
|
113
|
+
NumbersInWords.exceptional_numbers.lookup_fraction(text)
|
114
|
+
end
|
115
|
+
|
116
|
+
def possible_fraction?(text)
|
117
|
+
words = text.split(' ')
|
118
|
+
result = words & NumbersInWords.exceptional_numbers.fraction_names
|
119
|
+
result.length.positive?
|
120
|
+
end
|
121
|
+
|
122
|
+
def power(text)
|
123
|
+
power = NumbersInWords.power_of_ten(text)
|
124
|
+
|
125
|
+
10**power if power
|
126
|
+
end
|
127
|
+
|
128
|
+
def canonize(word)
|
129
|
+
aliases[word] || word
|
130
|
+
end
|
131
|
+
|
132
|
+
def aliases
|
133
|
+
{
|
134
|
+
'a' => 'one',
|
135
|
+
'oh' => 'zero'
|
136
|
+
}
|
137
|
+
end
|
138
|
+
|
139
|
+
def check_mixed(txt)
|
140
|
+
mixed = txt.match(/^(-?\d+(.\d+)?) (#{POWERS_RX}s?)$/)
|
141
|
+
return unless mixed && mixed[1] && mixed[3]
|
142
|
+
|
143
|
+
matches = [mixed[1], mixed[3]].map { |m| NumbersInWords.in_numbers m }
|
144
|
+
matches.reduce(&:*)
|
145
|
+
end
|
146
|
+
|
147
|
+
def check_one(txt)
|
148
|
+
txt.match(/^one (#{POWERS_RX})$/)
|
149
|
+
end
|
150
|
+
|
151
|
+
def strip_minus(txt)
|
152
|
+
txt.gsub(/^minus/, '') if txt =~ /^minus/
|
153
|
+
end
|
154
|
+
|
155
|
+
def check_decimal(txt)
|
156
|
+
txt.match(/\spoint\s/)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|