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.
- checksums.yaml +5 -5
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -1148
- data/.travis.yml +4 -4
- data/Gemfile +3 -1
- data/README.md +5 -43
- data/Rakefile +3 -1
- data/lib/numbers_in_words.rb +43 -19
- data/lib/numbers_in_words/duck_punch.rb +12 -8
- data/lib/numbers_in_words/exceptional_numbers.rb +119 -0
- data/lib/numbers_in_words/fraction.rb +151 -0
- data/lib/numbers_in_words/number_group.rb +34 -21
- data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
- data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
- data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
- data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
- data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
- data/lib/numbers_in_words/parsing/to_number.rb +159 -0
- data/lib/numbers_in_words/powers_of_ten.rb +49 -0
- data/lib/numbers_in_words/to_word.rb +78 -13
- data/lib/numbers_in_words/version.rb +3 -1
- data/lib/numbers_in_words/writer.rb +69 -0
- data/numbers_in_words.gemspec +14 -13
- data/spec/exceptional_numbers_spec.rb +26 -0
- data/spec/fraction_spec.rb +132 -0
- data/spec/fractions_spec.rb +31 -0
- data/spec/non_monkey_patch_spec.rb +39 -20
- data/spec/number_group_spec.rb +12 -12
- data/spec/number_parser_spec.rb +63 -0
- data/spec/numbers_in_words_spec.rb +56 -69
- data/spec/numerical_strings_spec.rb +28 -12
- data/spec/spec_helper.rb +2 -4
- data/spec/to_word_spec.rb +18 -0
- data/spec/words_in_numbers_spec.rb +130 -125
- data/spec/writer_spec.rb +26 -0
- data/spec/years_spec.rb +23 -13
- metadata +40 -25
- data/lib/numbers_in_words/english/constants.rb +0 -124
- data/lib/numbers_in_words/english/language_writer_english.rb +0 -116
- data/lib/numbers_in_words/language_writer.rb +0 -30
- data/lib/numbers_in_words/number_parser.rb +0 -135
- data/lib/numbers_in_words/to_number.rb +0 -88
- 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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'writer'
|
4
|
+
require_relative 'number_group'
|
5
|
+
require_relative 'fraction'
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
module NumbersInWords
|
8
|
+
# Arbitrarily small number for rationalizing fractions
|
9
|
+
EPSILON = 0.0000000001
|
9
10
|
|
10
|
-
|
11
|
-
|
12
|
-
|
11
|
+
class ToWord
|
12
|
+
attr_reader :that
|
13
|
+
|
14
|
+
def initialize(that)
|
15
|
+
@that = that
|
13
16
|
end
|
14
|
-
end
|
15
17
|
|
16
|
-
|
17
|
-
|
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
|
@@ -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
|
data/numbers_in_words.gemspec
CHANGED
@@ -1,23 +1,24 @@
|
|
1
|
-
#
|
2
|
-
|
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 =
|
8
|
-
gem.description =
|
9
|
-
gem.summary =
|
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 = [
|
13
|
-
gem.email = [
|
14
|
-
gem.homepage =
|
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.
|
17
|
-
gem.add_development_dependency
|
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 = [
|
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
|