numbers_in_words 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +2 -0
  3. data/.gitignore +1 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +35 -1148
  6. data/.travis.yml +14 -4
  7. data/Gemfile +4 -1
  8. data/README.md +6 -43
  9. data/Rakefile +3 -1
  10. data/bin/spec +0 -0
  11. data/lib/numbers_in_words.rb +44 -19
  12. data/lib/numbers_in_words/duck_punch.rb +12 -8
  13. data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
  14. data/lib/numbers_in_words/fraction.rb +136 -0
  15. data/lib/numbers_in_words/number_group.rb +34 -25
  16. data/lib/numbers_in_words/parsing/fraction_parsing.rb +34 -0
  17. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  18. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  19. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  20. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  21. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  22. data/lib/numbers_in_words/parsing/special.rb +67 -0
  23. data/lib/numbers_in_words/parsing/to_number.rb +77 -0
  24. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  25. data/lib/numbers_in_words/to_word.rb +78 -13
  26. data/lib/numbers_in_words/version.rb +3 -1
  27. data/lib/numbers_in_words/writer.rb +69 -0
  28. data/numbers_in_words.gemspec +14 -13
  29. data/spec/exceptional_numbers_spec.rb +26 -0
  30. data/spec/fraction_spec.rb +152 -0
  31. data/spec/fractions_spec.rb +31 -0
  32. data/spec/non_monkey_patch_spec.rb +40 -15
  33. data/spec/number_group_spec.rb +12 -12
  34. data/spec/number_parser_spec.rb +31 -0
  35. data/spec/numbers_in_words_spec.rb +63 -70
  36. data/spec/numerical_strings_spec.rb +35 -0
  37. data/spec/spec_helper.rb +24 -4
  38. data/spec/to_word_spec.rb +18 -0
  39. data/spec/words_in_numbers_spec.rb +133 -116
  40. data/spec/writer_spec.rb +26 -0
  41. data/spec/years_spec.rb +27 -0
  42. metadata +49 -27
  43. data/lib/numbers_in_words/english/constants.rb +0 -93
  44. data/lib/numbers_in_words/english/language_writer_english.rb +0 -109
  45. data/lib/numbers_in_words/language_writer.rb +0 -31
  46. data/lib/numbers_in_words/number_parser.rb +0 -81
  47. data/lib/numbers_in_words/to_number.rb +0 -82
  48. data/spec/language_writer_spec.rb +0 -23
@@ -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
@@ -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.3.0"
4
+ VERSION = '1.0.0'
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
@@ -1,23 +1,24 @@
1
- # -*- encoding: utf-8 -*-
2
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
3
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
5
  require 'numbers_in_words/version'
5
6
 
6
7
  Gem::Specification.new do |gem|
7
- gem.name = "numbers_in_words"
8
- gem.description = "#in_words method for integers and #in_numbers for strings"
9
- gem.summary = "Example: 123.in_words # => \"one hundred and twenty three\", \"seventy-five point eight\".in_numbers # = > 75.8"
8
+ gem.name = 'numbers_in_words'
9
+ gem.description = 'convert written numbers into Integers and vice-versa'
10
+ gem.summary = 'Example: NumbersInWords.in_words(123) # => ' \
11
+ '"one hundred and twenty three", NumbersInWords.in_numbers("seventy-five point eight") # = > 75.8'
10
12
 
11
13
  gem.version = NumbersInWords::VERSION
12
- gem.authors = ["Mark Burns"]
13
- gem.email = ["markthedeveloper@gmail.com"]
14
- gem.homepage = "http://github.com/markburns/numbers_in_words"
14
+ gem.authors = ['Mark Burns', 'Dimid Duchovny']
15
+ gem.email = ['markthedeveloper@gmail.com', 'dimidd@gmail.com']
16
+ gem.homepage = 'http://github.com/markburns/numbers_in_words'
15
17
 
16
- gem.add_dependency "activesupport"
17
- gem.add_development_dependency "rspec", "~> 3.4.0"
18
+ gem.add_development_dependency 'rspec', '~> 3.4.0'
19
+ gem.add_development_dependency 'rubocop'
18
20
 
19
- gem.files = `git ls-files`.split($/)
20
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
21
22
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22
- gem.require_paths = ["lib"]
23
+ gem.require_paths = ['lib']
23
24
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './spec/spec_helper'
4
+
5
+ describe NumbersInWords::ExceptionalNumbers do
6
+ describe '#fraction' do
7
+ FRACTIONS = {
8
+ [1, 2] => 'one half',
9
+ [2, 2] => 'two halves',
10
+ [3, 2] => 'three halves',
11
+ [1, 3] => 'one third',
12
+ [1, 4] => 'one quarter',
13
+ [2, 17] => 'two seventeenths',
14
+ [1, 1_000] => 'one one thousandth',
15
+ [74, 101] => 'seventy-four hundred and firsts',
16
+ [13, 97] => 'thirteen ninety-sevenths',
17
+ [131, 1_000] => 'one hundred and thirty-one thousandths'
18
+ }.freeze
19
+
20
+ FRACTIONS.each do |(numerator, denominator), string|
21
+ it "#{numerator}/#{denominator} == #{string}" do
22
+ expect(subject.fraction(denominator: denominator, numerator: numerator).in_words).to eql(string)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe NumbersInWords::Fraction do # rubocop: disable Metrics/BlockLength
4
+ subject do
5
+ described_class.new(numerator: numerator, denominator: denominator, attributes: attributes)
6
+ end
7
+
8
+ let(:numerator) { 1 }
9
+
10
+ context 'halves' do
11
+ let(:denominator) { 2 }
12
+ let(:attributes) do
13
+ { number: 'two',
14
+ ordinal: 'second',
15
+ fraction: { singular: 'half', plural: 'halves' } }
16
+ end
17
+
18
+ it do
19
+ expect(subject.in_words).to eq 'one half'
20
+ end
21
+
22
+ context 'with plural' do
23
+ let(:numerator) { 2 }
24
+
25
+ it do
26
+ expect(subject.in_words).to eq 'two halves'
27
+ end
28
+ end
29
+ end
30
+
31
+ context 'quarters' do
32
+ let(:denominator) { 4 }
33
+ let(:attributes) do
34
+ {
35
+ number: 'four',
36
+ ordinal: 'fourth',
37
+ fraction: { singular: 'quarter' }
38
+ }
39
+ end
40
+
41
+ it do
42
+ expect(subject.in_words).to eq 'one quarter'
43
+ end
44
+ end
45
+
46
+ context 'fifths' do
47
+ let(:denominator) { 5 }
48
+ let(:attributes) do
49
+ { number: 'five', ordinal: 'fifth' }
50
+ end
51
+
52
+ it do
53
+ expect(subject.in_words).to eq 'one fifth'
54
+ end
55
+ end
56
+
57
+ context 'sixths' do
58
+ let(:denominator) { 6 }
59
+ let(:attributes) { {} }
60
+
61
+ it do
62
+ expect(subject.in_words).to eq 'one sixth'
63
+ end
64
+ end
65
+
66
+ context 'nineteenths' do
67
+ let(:denominator) { 19 }
68
+ let(:attributes) { {} }
69
+
70
+ it do
71
+ expect(subject.in_words).to eq 'one nineteenth'
72
+ expect(subject.fraction).to eq 'nineteenth'
73
+ end
74
+
75
+ context 'plural' do
76
+ let(:numerator) { 763 }
77
+
78
+ it do
79
+ expect(subject.in_words).to eq 'seven hundred and sixty-three nineteenths'
80
+ end
81
+ end
82
+ end
83
+
84
+ context 'one hundred and seconds' do
85
+ let(:denominator) { 102 }
86
+ let(:attributes) { {} }
87
+
88
+ it do
89
+ expect(subject.in_words).to eq 'one one hundred and second'
90
+ end
91
+ end
92
+
93
+ context 'one hundred and sixth' do
94
+ let(:denominator) { 106 }
95
+ let(:attributes) { {} }
96
+
97
+ it do
98
+ expect(subject.in_words).to eq 'one one hundred and sixth'
99
+ end
100
+ end
101
+
102
+ context 'one hundred and nineteenth' do
103
+ let(:denominator) { 119 }
104
+ let(:attributes) { {} }
105
+
106
+ it do
107
+ expect(subject.ordinal).to eq 'one hundred and nineteenth'
108
+ expect(subject.in_words).to eq 'one one hundred and nineteenth'
109
+ end
110
+ end
111
+
112
+ context 'one thousandth' do
113
+ let(:denominator) { 1000 }
114
+ let(:attributes) { {} }
115
+
116
+ it do
117
+ expect(subject.ordinal).to eq 'one thousandth'
118
+ expect(subject.in_words).to eq 'one one thousandth'
119
+ end
120
+
121
+ context 'plural' do
122
+ let(:numerator) { 2 }
123
+ let(:denominator) { 1000 }
124
+
125
+ it do
126
+ expect(subject.in_words).to eq 'two thousandths'
127
+ end
128
+ end
129
+ end
130
+
131
+ context 'googolplexths' do
132
+ let(:denominator) { Kernel.silence_warnings { 10**(10**100) } }
133
+
134
+ let(:attributes) do
135
+ { number: 'googolplex',
136
+ ordinal: 'googolplexth',
137
+ fraction: { singular: 'googolplexth', plural: 'googolplexths' } }
138
+ end
139
+
140
+ it do
141
+ expect(subject.in_words).to eq 'one infinitieth'
142
+ end
143
+
144
+ context 'with plural' do
145
+ let(:numerator) { 2 }
146
+
147
+ it do
148
+ expect(subject.in_words).to eq 'two infinitieths'
149
+ end
150
+ end
151
+ end
152
+ end