castwide_numbers_in_words 1.0.2

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 (45) 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 +17 -0
  7. data/Gemfile +8 -0
  8. data/Gemfile.lock +74 -0
  9. data/LICENSE.txt +24 -0
  10. data/NUMBERS_IN_WORDS.txt +22 -0
  11. data/README.md +47 -0
  12. data/Rakefile +3 -0
  13. data/bin/spec +2 -0
  14. data/castwide_numbers_in_words.gemspec +24 -0
  15. data/lib/numbers_in_words/duck_punch.rb +23 -0
  16. data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
  17. data/lib/numbers_in_words/fraction.rb +136 -0
  18. data/lib/numbers_in_words/number_group.rb +64 -0
  19. data/lib/numbers_in_words/parsing/fraction_parsing.rb +31 -0
  20. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  21. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  22. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  23. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  24. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  25. data/lib/numbers_in_words/parsing/special.rb +67 -0
  26. data/lib/numbers_in_words/parsing/to_number.rb +77 -0
  27. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  28. data/lib/numbers_in_words/to_word.rb +84 -0
  29. data/lib/numbers_in_words/version.rb +5 -0
  30. data/lib/numbers_in_words/writer.rb +69 -0
  31. data/lib/numbers_in_words.rb +58 -0
  32. data/spec/exceptional_numbers_spec.rb +26 -0
  33. data/spec/fraction_spec.rb +152 -0
  34. data/spec/fractions_spec.rb +31 -0
  35. data/spec/non_monkey_patch_spec.rb +51 -0
  36. data/spec/number_group_spec.rb +17 -0
  37. data/spec/number_parser_spec.rb +31 -0
  38. data/spec/numbers_in_words_spec.rb +99 -0
  39. data/spec/numerical_strings_spec.rb +35 -0
  40. data/spec/spec_helper.rb +26 -0
  41. data/spec/to_word_spec.rb +18 -0
  42. data/spec/words_in_numbers_spec.rb +152 -0
  43. data/spec/writer_spec.rb +26 -0
  44. data/spec/years_spec.rb +27 -0
  45. metadata +124 -0
@@ -0,0 +1,31 @@
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.downcase
13
+ .gsub(/[^a-z 0-9]/, ' ')
14
+ .gsub(/ +/, ' ')
15
+ end
16
+
17
+ def possible_fraction?(text)
18
+ words = text.split(' ')
19
+ result = words & NumbersInWords.exceptional_numbers.fraction_names
20
+ result.length.positive?
21
+ end
22
+
23
+ def text_including_punctuation
24
+ to_s.strip
25
+ end
26
+
27
+ def text
28
+ strip_punctuation text_including_punctuation
29
+ end
30
+ end
31
+ 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
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'special'
4
+ require_relative 'fraction_parsing'
5
+
6
+ module NumbersInWords
7
+ class ToNumber
8
+ include FractionParsing
9
+ extend Forwardable
10
+ def_delegator :that, :to_s
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
+ special || decimal || as_numbers
21
+ end
22
+
23
+ private
24
+
25
+ def special
26
+ Special.new(that, only_compress).call
27
+ end
28
+
29
+ def decimal
30
+ match = check_decimal text
31
+ return unless match
32
+
33
+ integer = NumbersInWords.in_numbers(match.pre_match)
34
+ decimal = NumbersInWords.in_numbers(match.post_match)
35
+ integer + "0.#{decimal}".to_f
36
+ end
37
+
38
+ def as_numbers
39
+ numbers = word_array_to_nums text.split(' ')
40
+
41
+ NumbersInWords::NumberParser.new.parse numbers, only_compress: only_compress
42
+ end
43
+
44
+ def word_array_to_nums(words)
45
+ words.map { |i| word_to_num(i) }.compact
46
+ end
47
+
48
+ # handles simple single word numbers
49
+ # e.g. one, seven, twenty, eight, thousand etc
50
+ def word_to_num(word)
51
+ text = canonize(word.to_s.chomp.strip)
52
+
53
+ NumbersInWords.exceptional_number(text) || fraction(text) || power(text)
54
+ end
55
+
56
+ def power(text)
57
+ power = NumbersInWords.power_of_ten(text)
58
+
59
+ 10**power if power
60
+ end
61
+
62
+ def canonize(word)
63
+ aliases[word] || word
64
+ end
65
+
66
+ def aliases
67
+ {
68
+ 'a' => 'one',
69
+ 'oh' => 'zero'
70
+ }
71
+ end
72
+
73
+ def check_decimal(txt)
74
+ txt.match(/\spoint\s/)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ GOOGOL = 10**100
5
+
6
+ POWERS_OF_TEN = {
7
+ 0 => 'one',
8
+ 1 => 'ten',
9
+ 2 => 'hundred',
10
+ 1 * 3 => 'thousand',
11
+ 2 * 3 => 'million',
12
+ 3 * 3 => 'billion',
13
+ 4 * 3 => 'trillion',
14
+ 5 * 3 => 'quadrillion',
15
+ 6 * 3 => 'quintillion',
16
+ 7 * 3 => 'sextillion',
17
+ 8 * 3 => 'septillion',
18
+ 9 * 3 => 'octillion',
19
+ 10 * 3 => 'nonillion',
20
+ 11 * 3 => 'decillion',
21
+ 12 * 3 => 'undecillion',
22
+ 13 * 3 => 'duodecillion',
23
+ 14 * 3 => 'tredecillion',
24
+ 15 * 3 => 'quattuordecillion',
25
+ 16 * 3 => 'quindecillion',
26
+ 17 * 3 => 'sexdecillion',
27
+ 18 * 3 => 'septendecillion',
28
+ 19 * 3 => 'octodecillion',
29
+ 20 * 3 => 'novemdecillion',
30
+ 21 * 3 => 'vigintillion',
31
+ 22 * 3 => 'unvigintillion',
32
+ 23 * 3 => 'duovigintillion',
33
+ 24 * 3 => 'trevigintillion',
34
+ 25 * 3 => 'quattuorvigintillion',
35
+ 26 * 3 => 'quinvigintillion',
36
+ 27 * 3 => 'sexvigintillion',
37
+ 28 * 3 => 'septenvigintillion',
38
+ 29 * 3 => 'octovigintillion',
39
+ 30 * 3 => 'novemvigintillion',
40
+ 31 * 3 => 'trigintillion',
41
+ 32 * 3 => 'untrigintillion',
42
+ 33 * 3 => 'duotrigintillion',
43
+ 100 => 'googol',
44
+ 101 * 3 => 'centillion',
45
+ GOOGOL => 'googolplex'
46
+ }.freeze
47
+
48
+ POWERS_RX = Regexp.union(POWERS_OF_TEN.values[1..-1]).freeze
49
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'writer'
4
+ require_relative 'number_group'
5
+ require_relative 'fraction'
6
+
7
+ module NumbersInWords
8
+ # Arbitrarily small number for rationalizing fractions
9
+ EPSILON = 0.0000000001
10
+
11
+ class ToWord
12
+ attr_reader :that
13
+
14
+ def initialize(that)
15
+ @that = that
16
+ end
17
+
18
+ def to_i
19
+ that.to_i
20
+ end
21
+
22
+ def negative
23
+ return unless to_i.negative?
24
+
25
+ 'minus ' + NumbersInWords.in_words(-@that)
26
+ end
27
+
28
+ def in_words(fraction: false)
29
+ as_fraction(fraction) ||
30
+ handle_exceptional_numbers ||
31
+ decimals ||
32
+ negative ||
33
+ output
34
+ end
35
+
36
+ def as_fraction(fraction)
37
+ return Fraction.in_words(that) if fraction
38
+ end
39
+
40
+ def decimals
41
+ int, decimals = NumberGroup.new(@that).split_decimals
42
+ return unless int
43
+
44
+ out = NumbersInWords.in_words(int) + ' point '
45
+ decimals.each do |decimal|
46
+ out = out + NumbersInWords.in_words(decimal.to_i) + ' '
47
+ end
48
+ out.strip
49
+ end
50
+
51
+ def output
52
+ output = if to_i.to_s.length == 2 # 20-99
53
+ handle_tens(to_i)
54
+ else
55
+ Writer.new(that).call # longer numbers
56
+ end
57
+
58
+ output.strip
59
+ end
60
+
61
+ def handle_tens(number)
62
+ output = ''
63
+
64
+ tens = (number / 10).round * 10 # write the tens
65
+
66
+ output += NumbersInWords.lookup(tens).to_s # e.g. eighty
67
+
68
+ digit = number - tens # write the digits
69
+
70
+ unless digit.zero?
71
+ join = number < 100 ? '-' : ' '
72
+ output = output + join + NumbersInWords.in_words(digit)
73
+ end
74
+
75
+ output
76
+ end
77
+
78
+ def handle_exceptional_numbers
79
+ return unless @that.is_a?(Integer)
80
+
81
+ NumbersInWords.exceptional_numbers.lookup(@that)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ VERSION = '1.0.2'
5
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ class Writer
5
+ def initialize(that)
6
+ @that = that
7
+ end
8
+
9
+ def call
10
+ length = @that.to_s.length
11
+ output =
12
+ if length == 3
13
+ # e.g. 113 splits into "one hundred" and "thirteen"
14
+ write_groups(2)
15
+
16
+ # more than one hundred less than one googol
17
+ elsif length < LENGTH_OF_GOOGOL
18
+ write_groups(3)
19
+
20
+ elsif length >= LENGTH_OF_GOOGOL
21
+ write_googols
22
+ end
23
+ output.strip
24
+ end
25
+
26
+ def group_words(size)
27
+ # 1000 and over Numbers are split into groups of three
28
+ groups = NumberGroup.groups_of @that, size
29
+ powers = groups.keys.sort.reverse # put in descending order
30
+
31
+ powers.each do |power|
32
+ name = NumbersInWords::POWERS_OF_TEN[power]
33
+ digits = groups[power]
34
+ yield power, name, digits
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def write_googols
41
+ googols, remainder = NumberGroup.new(@that).split_googols
42
+ output = ''
43
+
44
+ output = output + ' ' + NumbersInWords.in_words(googols) + ' googol'
45
+ if remainder.positive?
46
+ prefix = ' '
47
+ prefix += 'and ' if remainder < 100
48
+ output = output + prefix + NumbersInWords.in_words(remainder)
49
+ end
50
+
51
+ output
52
+ end
53
+
54
+ def write_groups(group)
55
+ # e.g. 113 splits into "one hundred" and "thirteen"
56
+ output = ''
57
+ group_words(group) do |power, name, digits|
58
+ if digits.positive?
59
+ prefix = ' '
60
+ # no and between thousands and hundreds
61
+ prefix += 'and ' if power.zero? && (digits < 100)
62
+ output = output + prefix + NumbersInWords.in_words(digits)
63
+ output = output + prefix + name unless power.zero?
64
+ end
65
+ end
66
+ output
67
+ end
68
+ end
69
+ end