numbers_in_words 0.4.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +35 -1148
  5. data/.travis.yml +4 -4
  6. data/Gemfile +3 -1
  7. data/README.md +5 -43
  8. data/Rakefile +3 -1
  9. data/lib/numbers_in_words.rb +43 -19
  10. data/lib/numbers_in_words/duck_punch.rb +12 -8
  11. data/lib/numbers_in_words/exceptional_numbers.rb +119 -0
  12. data/lib/numbers_in_words/fraction.rb +151 -0
  13. data/lib/numbers_in_words/number_group.rb +34 -21
  14. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  15. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  16. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  17. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  18. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  19. data/lib/numbers_in_words/parsing/to_number.rb +159 -0
  20. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  21. data/lib/numbers_in_words/to_word.rb +78 -13
  22. data/lib/numbers_in_words/version.rb +3 -1
  23. data/lib/numbers_in_words/writer.rb +69 -0
  24. data/numbers_in_words.gemspec +14 -13
  25. data/spec/exceptional_numbers_spec.rb +26 -0
  26. data/spec/fraction_spec.rb +132 -0
  27. data/spec/fractions_spec.rb +31 -0
  28. data/spec/non_monkey_patch_spec.rb +39 -20
  29. data/spec/number_group_spec.rb +12 -12
  30. data/spec/number_parser_spec.rb +63 -0
  31. data/spec/numbers_in_words_spec.rb +56 -69
  32. data/spec/numerical_strings_spec.rb +28 -12
  33. data/spec/spec_helper.rb +2 -4
  34. data/spec/to_word_spec.rb +18 -0
  35. data/spec/words_in_numbers_spec.rb +130 -125
  36. data/spec/writer_spec.rb +26 -0
  37. data/spec/years_spec.rb +23 -13
  38. metadata +40 -25
  39. data/lib/numbers_in_words/english/constants.rb +0 -124
  40. data/lib/numbers_in_words/english/language_writer_english.rb +0 -116
  41. data/lib/numbers_in_words/language_writer.rb +0 -30
  42. data/lib/numbers_in_words/number_parser.rb +0 -135
  43. data/lib/numbers_in_words/to_number.rb +0 -88
  44. data/spec/language_writer_spec.rb +0 -23
@@ -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.4.0"
4
+ VERSION = '0.4.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
@@ -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) # => "one hundred and twenty three", NumbersInWords.in_numbers("seventy-five point eight") # = > 75.8'
10
11
 
11
12
  gem.version = NumbersInWords::VERSION
12
- gem.authors = ["Mark Burns", "Dimid Duchovny"]
13
- gem.email = ["markthedeveloper@gmail.com", "dimidd@gmail.com"]
14
- gem.homepage = "http://github.com/markburns/numbers_in_words"
13
+ gem.authors = ['Mark Burns', 'Dimid Duchovny']
14
+ gem.email = ['markthedeveloper@gmail.com', 'dimidd@gmail.com']
15
+ gem.homepage = 'http://github.com/markburns/numbers_in_words'
15
16
 
16
- gem.add_dependency "activesupport"
17
- gem.add_development_dependency "rspec", "~> 3.4.0"
17
+ gem.add_development_dependency 'rspec', '~> 3.4.0'
18
+ gem.add_development_dependency 'rubocop'
18
19
 
19
- gem.files = `git ls-files`.split($/)
20
- gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
21
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
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,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ RSpec.describe Fraction do
5
+ subject do
6
+ described_class.new(numerator: numerator, denominator: denominator, numerator: numerator, attributes: attributes)
7
+ end
8
+
9
+ let(:numerator) { 1 }
10
+
11
+ context 'halves' do
12
+ let(:denominator) { 2 }
13
+ let(:attributes) do
14
+ { number: 'two',
15
+ ordinal: 'second',
16
+ fraction: { singular: 'half', plural: 'halves' } }
17
+ end
18
+
19
+ it do
20
+ expect(subject.in_words).to eq 'one half'
21
+ end
22
+
23
+ context 'with plural' do
24
+ let(:numerator) { 2 }
25
+
26
+ it do
27
+ expect(subject.in_words).to eq 'two halves'
28
+ end
29
+ end
30
+ end
31
+
32
+ context 'quarters' do
33
+ let(:denominator) { 4 }
34
+ let(:attributes) do
35
+ {
36
+ number: 'four',
37
+ ordinal: 'fourth',
38
+ fraction: { singular: 'quarter' }
39
+ }
40
+ end
41
+
42
+ it do
43
+ expect(subject.in_words).to eq 'one quarter'
44
+ end
45
+ end
46
+
47
+ context 'fifths' do
48
+ let(:denominator) { 5 }
49
+ let(:attributes) do
50
+ { number: 'five', ordinal: 'fifth' }
51
+ end
52
+
53
+ it do
54
+ expect(subject.in_words).to eq 'one fifth'
55
+ end
56
+ end
57
+
58
+ context 'sixths' do
59
+ let(:denominator) { 6 }
60
+ let(:attributes) { {} }
61
+
62
+ it do
63
+ expect(subject.in_words).to eq 'one sixth'
64
+ end
65
+ end
66
+
67
+ context 'nineteenths' do
68
+ let(:denominator) { 19 }
69
+ let(:attributes) { {} }
70
+
71
+ it do
72
+ expect(subject.in_words).to eq 'one nineteenth'
73
+ expect(subject.fraction).to eq 'nineteenth'
74
+ end
75
+
76
+ context 'plural' do
77
+ let(:numerator) { 763 }
78
+
79
+ it do
80
+ expect(subject.in_words).to eq 'seven hundred and sixty-three nineteenths'
81
+ end
82
+ end
83
+ end
84
+
85
+ context 'one hundred and seconds' do
86
+ let(:denominator) { 102 }
87
+ let(:attributes) { {} }
88
+
89
+ it do
90
+ expect(subject.in_words).to eq 'one one hundred and second'
91
+ end
92
+ end
93
+
94
+ context 'one hundred and sixth' do
95
+ let(:denominator) { 106 }
96
+ let(:attributes) { {} }
97
+
98
+ it do
99
+ expect(subject.in_words).to eq 'one one hundred and sixth'
100
+ end
101
+ end
102
+
103
+ context 'one hundred and nineteenth' do
104
+ let(:denominator) { 119 }
105
+ let(:attributes) { {} }
106
+
107
+ it do
108
+ expect(subject.ordinal).to eq 'one hundred and nineteenth'
109
+ expect(subject.in_words).to eq 'one one hundred and nineteenth'
110
+ end
111
+ end
112
+
113
+ context 'one thousandth' do
114
+ let(:denominator) { 1000 }
115
+ let(:attributes) { {} }
116
+
117
+ it do
118
+ expect(subject.ordinal).to eq 'one thousandth'
119
+ expect(subject.in_words).to eq 'one one thousandth'
120
+ end
121
+
122
+ context 'plural' do
123
+ let(:numerator) { 2 }
124
+ let(:denominator) { 1000 }
125
+
126
+ it do
127
+ expect(subject.in_words).to eq 'two thousandths'
128
+ end
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe NumbersInWords do
4
+ FRACTIONS = {
5
+ 'half' => 0.5,
6
+ 'a half' => 0.5,
7
+ 'one half' => 0.5,
8
+ 'two halves' => 1.0,
9
+ 'three halves' => 1.5,
10
+ 'three quarters' => 0.75,
11
+ 'three fifths' => 3 / 5.0,
12
+ 'seven fifteenths' => 7 / 15.0
13
+ }.freeze
14
+
15
+ PENDING = {
16
+ 'twenty and three fifteenths' => 20 + 3 / 15.0,
17
+ 'three ninety-sevenths' => 3 / 97.0
18
+ }.freeze
19
+
20
+ FRACTIONS.each do |string, float|
21
+ it "#{string} == #{float}" do
22
+ expect(string.in_numbers).to eql(float)
23
+ end
24
+ end
25
+
26
+ PENDING.each do |string, float|
27
+ pending "#{string} == #{float}" do
28
+ expect(string.in_numbers).to eql(float)
29
+ end
30
+ end
31
+ end