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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +2 -0
- data/.gitignore +1 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -1148
- data/.travis.yml +14 -4
- data/Gemfile +4 -1
- data/README.md +6 -43
- data/Rakefile +3 -1
- data/bin/spec +0 -0
- data/lib/numbers_in_words.rb +44 -19
- data/lib/numbers_in_words/duck_punch.rb +12 -8
- data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
- data/lib/numbers_in_words/fraction.rb +136 -0
- data/lib/numbers_in_words/number_group.rb +34 -25
- data/lib/numbers_in_words/parsing/fraction_parsing.rb +34 -0
- 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/special.rb +67 -0
- data/lib/numbers_in_words/parsing/to_number.rb +77 -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 +152 -0
- data/spec/fractions_spec.rb +31 -0
- data/spec/non_monkey_patch_spec.rb +40 -15
- data/spec/number_group_spec.rb +12 -12
- data/spec/number_parser_spec.rb +31 -0
- data/spec/numbers_in_words_spec.rb +63 -70
- data/spec/numerical_strings_spec.rb +35 -0
- data/spec/spec_helper.rb +24 -4
- data/spec/to_word_spec.rb +18 -0
- data/spec/words_in_numbers_spec.rb +133 -116
- data/spec/writer_spec.rb +26 -0
- data/spec/years_spec.rb +27 -0
- metadata +49 -27
- data/lib/numbers_in_words/english/constants.rb +0 -93
- data/lib/numbers_in_words/english/language_writer_english.rb +0 -109
- data/lib/numbers_in_words/language_writer.rb +0 -31
- data/lib/numbers_in_words/number_parser.rb +0 -81
- data/lib/numbers_in_words/to_number.rb +0 -82
- 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
|
-
|
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) # => ' \
|
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 = [
|
13
|
-
gem.email = [
|
14
|
-
gem.homepage =
|
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.
|
17
|
-
gem.add_development_dependency
|
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 = [
|
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
|