numbers_in_words 0.4.0 → 0.4.1

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 (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