numbers_in_words 0.2.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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,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..]).freeze
49
+ end
@@ -1,19 +1,84 @@
1
- class NumbersInWords::ToWord
2
- def initialize that, language=NumbersInWords.language
3
- @that = that
4
- @language = language
5
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'writer'
4
+ require_relative 'number_group'
5
+ require_relative 'fraction'
6
6
 
7
- def in_words language=nil
8
- language ||= @language
7
+ module NumbersInWords
8
+ # Arbitrarily small number for rationalizing fractions
9
+ EPSILON = 0.0000000001
9
10
 
10
- case language
11
- when "English" #allow for I18n
12
- in_english
11
+ class ToWord
12
+ attr_reader :that
13
+
14
+ def initialize(that)
15
+ @that = that
13
16
  end
14
- end
15
17
 
16
- def in_english
17
- NumbersInWords::English::LanguageWriterEnglish.new(@that).in_words
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 << 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) # e.g. eighty
67
+
68
+ digit = number - tens # write the digits
69
+
70
+ unless digit.zero?
71
+ join = number < 100 ? '-' : ' '
72
+ 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
18
83
  end
19
84
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NumbersInWords
2
- VERSION = "0.2.0"
4
+ VERSION = '0.5.1'
3
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