numbers_in_words 0.4.0 → 0.4.1

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