numbers_in_words 0.4.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) 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 +14 -4
  6. data/Gemfile +4 -1
  7. data/README.md +6 -44
  8. data/Rakefile +3 -1
  9. data/lib/numbers_in_words.rb +44 -19
  10. data/lib/numbers_in_words/duck_punch.rb +12 -8
  11. data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
  12. data/lib/numbers_in_words/fraction.rb +136 -0
  13. data/lib/numbers_in_words/number_group.rb +34 -21
  14. data/lib/numbers_in_words/parsing/fraction_parsing.rb +34 -0
  15. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  16. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  17. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  18. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  19. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  20. data/lib/numbers_in_words/parsing/special.rb +67 -0
  21. data/lib/numbers_in_words/parsing/to_number.rb +77 -0
  22. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  23. data/lib/numbers_in_words/to_word.rb +78 -13
  24. data/lib/numbers_in_words/version.rb +3 -1
  25. data/lib/numbers_in_words/writer.rb +69 -0
  26. data/numbers_in_words.gemspec +14 -13
  27. data/spec/exceptional_numbers_spec.rb +26 -0
  28. data/spec/fraction_spec.rb +152 -0
  29. data/spec/fractions_spec.rb +31 -0
  30. data/spec/non_monkey_patch_spec.rb +39 -20
  31. data/spec/number_group_spec.rb +12 -12
  32. data/spec/number_parser_spec.rb +31 -0
  33. data/spec/numbers_in_words_spec.rb +63 -70
  34. data/spec/numerical_strings_spec.rb +28 -12
  35. data/spec/spec_helper.rb +23 -4
  36. data/spec/to_word_spec.rb +18 -0
  37. data/spec/words_in_numbers_spec.rb +132 -125
  38. data/spec/writer_spec.rb +26 -0
  39. data/spec/years_spec.rb +23 -13
  40. metadata +43 -27
  41. data/lib/numbers_in_words/english/constants.rb +0 -124
  42. data/lib/numbers_in_words/english/language_writer_english.rb +0 -116
  43. data/lib/numbers_in_words/language_writer.rb +0 -30
  44. data/lib/numbers_in_words/number_parser.rb +0 -135
  45. data/lib/numbers_in_words/to_number.rb +0 -88
  46. 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)..-1].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,34 @@
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 = text.downcase.gsub(/[^a-z 0-9]/, ' ')
13
+ to_remove = true
14
+
15
+ to_remove = text.gsub! ' ', ' ' while to_remove
16
+
17
+ text
18
+ end
19
+
20
+ def possible_fraction?(text)
21
+ words = text.split(' ')
22
+ result = words & NumbersInWords.exceptional_numbers.fraction_names
23
+ result.length.positive?
24
+ end
25
+
26
+ def text_including_punctuation
27
+ to_s.strip
28
+ end
29
+
30
+ def text
31
+ strip_punctuation text_including_punctuation
32
+ end
33
+ end
34
+ 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