numbers_in_words 0.3.0 → 1.0.0

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 (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