numbers_in_words 0.2.0 → 0.5.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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +14 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +58 -0
  6. data/.travis.yml +15 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +50 -26
  9. data/README.md +20 -70
  10. data/Rakefile +3 -1
  11. data/bin/spec +2 -0
  12. data/lib/numbers_in_words.rb +49 -15
  13. data/lib/numbers_in_words/duck_punch.rb +12 -8
  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 +34 -25
  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 +78 -13
  27. data/lib/numbers_in_words/version.rb +3 -1
  28. data/lib/numbers_in_words/writer.rb +69 -0
  29. data/numbers_in_words.gemspec +15 -14
  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 +12 -12
  35. data/spec/number_parser_spec.rb +31 -0
  36. data/spec/numbers_in_words_spec.rb +74 -54
  37. data/spec/numerical_strings_spec.rb +35 -0
  38. data/spec/spec_helper.rb +24 -0
  39. data/spec/to_word_spec.rb +18 -0
  40. data/spec/words_in_numbers_spec.rb +135 -117
  41. data/spec/writer_spec.rb +26 -0
  42. data/spec/years_spec.rb +27 -0
  43. metadata +61 -59
  44. data/lib/numbers_in_words/english/constants.rb +0 -93
  45. data/lib/numbers_in_words/english/language_writer_english.rb +0 -107
  46. data/lib/numbers_in_words/language_writer.rb +0 -31
  47. data/lib/numbers_in_words/number_parser.rb +0 -81
  48. data/lib/numbers_in_words/to_number.rb +0 -82
  49. data/spec/language_writer_spec.rb +0 -23
@@ -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
@@ -1,55 +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 each
7
- @array.each { |item| yield item}
8
+ def self.groups_of(number, size)
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
43
- end
41
+ int, decimal = @number.to_s.split '.'
44
42
 
45
- def self.groups_of number, size
46
- new(number).groups(size)
43
+ [int.to_i, decimal.split(//).map(&:to_i)]
47
44
  end
48
45
 
49
46
  def split_googols
50
- googols = @number.to_s[0 .. (-LENGTH_OF_GOOGOL)].to_i
51
- remainder = @number.to_s[(1-LENGTH_OF_GOOGOL) .. -1].to_i
52
- 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
53
62
  end
54
63
  end
55
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