numbers_in_words 0.1.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +14 -0
  3. data/.gitignore +7 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +58 -0
  6. data/.travis.yml +15 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +47 -0
  10. data/Rakefile +2 -43
  11. data/bin/spec +2 -0
  12. data/lib/numbers_in_words.rb +55 -4
  13. data/lib/numbers_in_words/duck_punch.rb +23 -0
  14. data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
  15. data/lib/numbers_in_words/fraction.rb +136 -0
  16. data/lib/numbers_in_words/number_group.rb +64 -0
  17. data/lib/numbers_in_words/parsing/fraction_parsing.rb +34 -0
  18. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  19. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  20. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  21. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  22. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  23. data/lib/numbers_in_words/parsing/special.rb +67 -0
  24. data/lib/numbers_in_words/parsing/to_number.rb +77 -0
  25. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  26. data/lib/numbers_in_words/to_word.rb +84 -0
  27. data/lib/numbers_in_words/version.rb +5 -0
  28. data/lib/numbers_in_words/writer.rb +69 -0
  29. data/numbers_in_words.gemspec +20 -27
  30. data/spec/exceptional_numbers_spec.rb +26 -0
  31. data/spec/fraction_spec.rb +152 -0
  32. data/spec/fractions_spec.rb +31 -0
  33. data/spec/non_monkey_patch_spec.rb +51 -0
  34. data/spec/number_group_spec.rb +17 -0
  35. data/spec/number_parser_spec.rb +31 -0
  36. data/spec/numbers_in_words_spec.rb +69 -83
  37. data/spec/numerical_strings_spec.rb +35 -0
  38. data/spec/spec_helper.rb +26 -0
  39. data/spec/to_word_spec.rb +18 -0
  40. data/spec/words_in_numbers_spec.rb +137 -119
  41. data/spec/writer_spec.rb +26 -0
  42. data/spec/years_spec.rb +27 -0
  43. metadata +95 -45
  44. data/CHANGELOG +0 -1
  45. data/Manifest +0 -11
  46. data/README +0 -84
  47. data/examples/display_numbers_in_words.rb +0 -22
  48. data/init.rb +0 -8
  49. data/lib/numbers.rb +0 -260
  50. data/lib/words.rb +0 -221
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ class Fraction
5
+ attr_reader :denominator, :numerator, :attributes
6
+
7
+ def self.in_words(that)
8
+ r = that.rationalize(EPSILON)
9
+
10
+ NumbersInWords
11
+ .fraction(denominator: r.denominator, numerator: r.numerator)
12
+ .in_words
13
+ end
14
+
15
+ def initialize(denominator:, numerator: 1, attributes: nil)
16
+ @denominator = denominator
17
+ @numerator = numerator
18
+ @attributes = attributes || NumbersInWords::ExceptionalNumbers::DEFINITIONS[denominator] || {}
19
+ end
20
+
21
+ def lookup_keys
22
+ key = in_words
23
+ key2 = strip_punctuation(key.split(' ')).join(' ')
24
+
25
+ key3 = "a #{key}"
26
+ key4 = "an #{key}"
27
+ key5 = "a #{key2}"
28
+ key6 = "an #{key2}"
29
+ [key, key2, key3, key4, key5, key6].uniq
30
+ end
31
+
32
+ def in_words
33
+ NumbersInWords.in_words(numerator) + ' ' + fraction
34
+ end
35
+
36
+ def ordinal
37
+ pluralize? ? pluralized_ordinal_in_words : singular_ordinal_in_words
38
+ end
39
+
40
+ def fraction
41
+ if denominator == Float::INFINITY
42
+ # We've reached the limits of ruby's number system
43
+ # by the time we get to a googolplex (10 ** (10 ** 100))
44
+ return pluralize? ? 'infinitieths' : 'infinitieth'
45
+ end
46
+
47
+ pluralize? ? pluralized_fraction : singular_fraction
48
+ end
49
+
50
+ private
51
+
52
+ def strip_punctuation(words)
53
+ words.map { |w| w.gsub(/^a-z/, ' ') }
54
+ end
55
+
56
+ def pluralized_fraction
57
+ fraction_plural || pluralized_ordinal_in_words
58
+ end
59
+
60
+ def singular_fraction
61
+ fraction_singular || singular_ordinal_in_words
62
+ end
63
+
64
+ def pluralized_ordinal_in_words
65
+ pluralized_ordinal || denominator_ordinal_in_words
66
+ end
67
+
68
+ def singular_ordinal_in_words
69
+ singular_ordinal || denominator_ordinal_in_words
70
+ end
71
+
72
+ def singular_ordinal
73
+ attributes[:ordinal]
74
+ end
75
+
76
+ def pluralized_ordinal
77
+ singular_ordinal && singular_ordinal + 's'
78
+ end
79
+
80
+ def pluralize?
81
+ numerator > 1
82
+ end
83
+
84
+ def denominator_ordinal_in_words
85
+ if denominator > 100
86
+ # one hundred and second
87
+ with_remainder(100, ' and ')
88
+ elsif denominator > 19
89
+ # two thirty-fifths
90
+ with_remainder(10, '-')
91
+ else
92
+ # one seventh
93
+ singular = NumbersInWords.in_words(denominator) + 'th'
94
+ pluralize? ? singular + 's' : singular
95
+ end
96
+ end
97
+
98
+ def with_remainder(mod, join_word)
99
+ rest = denominator % mod
100
+ main = denominator - rest
101
+ main = NumbersInWords.in_words(main)
102
+
103
+ main = main.gsub(/^one /, '') if pluralize?
104
+
105
+ rest_zero(rest, main) || joined(main, rest, join_word)
106
+ end
107
+
108
+ def joined(main, rest, join_word)
109
+ main +
110
+ join_word +
111
+ self.class.new(numerator: numerator, denominator: rest).ordinal
112
+ end
113
+
114
+ def rest_zero(rest, main)
115
+ return unless rest.zero?
116
+
117
+ if pluralize?
118
+ main + 'ths'
119
+ else
120
+ main + 'th'
121
+ end
122
+ end
123
+
124
+ def exception
125
+ attributes[:fraction]
126
+ end
127
+
128
+ def fraction_singular
129
+ exception && exception[:singular]
130
+ end
131
+
132
+ def fraction_plural
133
+ exception && exception[:plural]
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ class NumberGroup
5
+ include Enumerable
6
+ attr_accessor :number
7
+
8
+ def self.groups_of(number, size)
9
+ new(number).groups(size)
10
+ end
11
+
12
+ def initialize(number)
13
+ @number = number
14
+ end
15
+
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)
23
+ @array.reverse!
24
+
25
+ # %w(1 432 765) => [1, 234, 567]
26
+ @array.map! { |group| group.reverse.join('').to_i }
27
+ @array.reverse! # put in ascending order of power of ten
28
+
29
+ power = 0
30
+
31
+ # [1, 234, 567] => {6 => 1, 3 => 234, 0 => 567}
32
+ @array.each_with_object({}) do |digits, o|
33
+ o[power] = digits
34
+ power += size
35
+ end
36
+ end
37
+
38
+ def split_decimals
39
+ return unless @number.is_a? Float
40
+
41
+ int, decimal = @number.to_s.split '.'
42
+
43
+ [int.to_i, decimal.split(//).map(&:to_i)]
44
+ end
45
+
46
+ def split_googols
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
62
+ end
63
+ end
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..]
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