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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +35 -1148
  5. data/.travis.yml +4 -4
  6. data/Gemfile +3 -1
  7. data/README.md +5 -43
  8. data/Rakefile +3 -1
  9. data/lib/numbers_in_words.rb +43 -19
  10. data/lib/numbers_in_words/duck_punch.rb +12 -8
  11. data/lib/numbers_in_words/exceptional_numbers.rb +119 -0
  12. data/lib/numbers_in_words/fraction.rb +151 -0
  13. data/lib/numbers_in_words/number_group.rb +34 -21
  14. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  15. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  16. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  17. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  18. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  19. data/lib/numbers_in_words/parsing/to_number.rb +159 -0
  20. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  21. data/lib/numbers_in_words/to_word.rb +78 -13
  22. data/lib/numbers_in_words/version.rb +3 -1
  23. data/lib/numbers_in_words/writer.rb +69 -0
  24. data/numbers_in_words.gemspec +14 -13
  25. data/spec/exceptional_numbers_spec.rb +26 -0
  26. data/spec/fraction_spec.rb +132 -0
  27. data/spec/fractions_spec.rb +31 -0
  28. data/spec/non_monkey_patch_spec.rb +39 -20
  29. data/spec/number_group_spec.rb +12 -12
  30. data/spec/number_parser_spec.rb +63 -0
  31. data/spec/numbers_in_words_spec.rb +56 -69
  32. data/spec/numerical_strings_spec.rb +28 -12
  33. data/spec/spec_helper.rb +2 -4
  34. data/spec/to_word_spec.rb +18 -0
  35. data/spec/words_in_numbers_spec.rb +130 -125
  36. data/spec/writer_spec.rb +26 -0
  37. data/spec/years_spec.rb +23 -13
  38. metadata +40 -25
  39. data/lib/numbers_in_words/english/constants.rb +0 -124
  40. data/lib/numbers_in_words/english/language_writer_english.rb +0 -116
  41. data/lib/numbers_in_words/language_writer.rb +0 -30
  42. data/lib/numbers_in_words/number_parser.rb +0 -135
  43. data/lib/numbers_in_words/to_number.rb +0 -88
  44. 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 number, size
8
+ def self.groups_of(number, size)
7
9
  new(number).groups(size)
8
10
  end
9
11
 
10
- def initialize number
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 size
18
- #1234567 => %w(765 432 1)
19
- @array = @number.to_s.reverse.split("").in_groups_of(size)
20
- #%w(765 432 1) => %w(1 432 765)
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
- #%w(1 432 765) => [1, 234, 567]
24
- @array.map! {|group| group.reverse.join("").to_i}
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.inject({}) do |o, digits|
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
- if @number.is_a? Float
39
- int, decimal = @number.to_s.split "."
39
+ return unless @number.is_a? Float
40
40
 
41
- return int.to_i, decimal.split(//).map(&:to_i)
42
- end
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 .. (-LENGTH_OF_GOOGOL)].to_i
47
- remainder = @number.to_s[(1-LENGTH_OF_GOOGOL) .. -1].to_i
48
- return googols, remainder
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