numbers_in_words 0.1.1 → 0.5.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.
- checksums.yaml +7 -0
- data/.codeclimate.yml +14 -0
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +58 -0
- data/.travis.yml +15 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +2 -43
- data/bin/spec +2 -0
- data/lib/numbers_in_words.rb +55 -4
- data/lib/numbers_in_words/duck_punch.rb +23 -0
- 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 +64 -0
- 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 +84 -0
- data/lib/numbers_in_words/version.rb +5 -0
- data/lib/numbers_in_words/writer.rb +69 -0
- data/numbers_in_words.gemspec +20 -27
- 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 +51 -0
- data/spec/number_group_spec.rb +17 -0
- data/spec/number_parser_spec.rb +31 -0
- data/spec/numbers_in_words_spec.rb +69 -83
- data/spec/numerical_strings_spec.rb +35 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/to_word_spec.rb +18 -0
- data/spec/words_in_numbers_spec.rb +137 -119
- data/spec/writer_spec.rb +26 -0
- data/spec/years_spec.rb +27 -0
- metadata +95 -45
- data/CHANGELOG +0 -1
- data/Manifest +0 -11
- data/README +0 -84
- data/examples/display_numbers_in_words.rb +0 -22
- data/init.rb +0 -8
- data/lib/numbers.rb +0 -260
- data/lib/words.rb +0 -221
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class ParseStatus
|
5
|
+
attr_accessor :reset, :memory, :answer
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@reset = true
|
9
|
+
@memory = 0
|
10
|
+
@answer = 0
|
11
|
+
end
|
12
|
+
|
13
|
+
def calculate
|
14
|
+
answer + memory
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './fraction_parsing'
|
4
|
+
|
5
|
+
module NumbersInWords
|
6
|
+
class Special
|
7
|
+
extend Forwardable
|
8
|
+
def_delegator :that, :to_s
|
9
|
+
|
10
|
+
include FractionParsing
|
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
|
+
float ||
|
21
|
+
negative ||
|
22
|
+
fraction(that) ||
|
23
|
+
mixed_words_and_digits ||
|
24
|
+
one
|
25
|
+
end
|
26
|
+
|
27
|
+
def float
|
28
|
+
text_including_punctuation.to_f if text =~ /^-?\d+(.\d+)?$/
|
29
|
+
end
|
30
|
+
|
31
|
+
def negative
|
32
|
+
stripped = strip_minus text
|
33
|
+
return unless stripped
|
34
|
+
|
35
|
+
stripped_n = NumbersInWords.in_numbers(stripped, only_compress: only_compress)
|
36
|
+
only_compress ? stripped_n.map { |k| k * -1 } : -1 * stripped_n
|
37
|
+
end
|
38
|
+
|
39
|
+
def mixed_words_and_digits
|
40
|
+
return unless numeric?(that)
|
41
|
+
|
42
|
+
in_words = that.split(' ').map { |word| numeric?(word) ? NumbersInWords.in_words(word) : word }.join(' ')
|
43
|
+
ToNumber.new(in_words, only_compress).call
|
44
|
+
end
|
45
|
+
|
46
|
+
def numeric?(word)
|
47
|
+
word.match(/\d+/)
|
48
|
+
end
|
49
|
+
|
50
|
+
def strip_minus(txt)
|
51
|
+
txt.gsub(/^minus/, '') if txt =~ /^minus/
|
52
|
+
end
|
53
|
+
|
54
|
+
def one
|
55
|
+
one = check_one text
|
56
|
+
|
57
|
+
return unless one
|
58
|
+
|
59
|
+
res = NumbersInWords.in_numbers(one[1])
|
60
|
+
only_compress ? [res] : res
|
61
|
+
end
|
62
|
+
|
63
|
+
def check_one(txt)
|
64
|
+
txt.match(/^one (#{POWERS_RX})$/)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -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..]).freeze
|
49
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'writer'
|
4
|
+
require_relative 'number_group'
|
5
|
+
require_relative 'fraction'
|
6
|
+
|
7
|
+
module NumbersInWords
|
8
|
+
# Arbitrarily small number for rationalizing fractions
|
9
|
+
EPSILON = 0.0000000001
|
10
|
+
|
11
|
+
class ToWord
|
12
|
+
attr_reader :that
|
13
|
+
|
14
|
+
def initialize(that)
|
15
|
+
@that = that
|
16
|
+
end
|
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
|
83
|
+
end
|
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,32 +1,25 @@
|
|
1
|
-
#
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'numbers_in_words/version'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
s.extra_rdoc_files = ["CHANGELOG", "README", "lib/numbers.rb", "lib/numbers_in_words.rb", "lib/words.rb"]
|
13
|
-
s.files = ["CHANGELOG", "Manifest", "README", "Rakefile", "examples/display_numbers_in_words.rb", "init.rb", "lib/numbers.rb", "lib/numbers_in_words.rb", "lib/words.rb", "spec/numbers_in_words_spec.rb", "spec/words_in_numbers_spec.rb", "numbers_in_words.gemspec"]
|
14
|
-
s.homepage = "http://rubygems.org/gems/numbers_in_words"
|
15
|
-
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Numbers_in_words", "--main", "README"]
|
16
|
-
s.require_paths = ["lib"]
|
17
|
-
s.rubyforge_project = "numbers_in_words"
|
18
|
-
s.rubygems_version = "1.8.17"
|
19
|
-
s.summary = "Example: 123.in_words #=> \"one hundred and twenty three\", \"seventy-five point eight\".in_numbers #=> 75.8"
|
7
|
+
Gem::Specification.new do |gem|
|
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'
|
20
12
|
|
21
|
-
|
22
|
-
|
13
|
+
gem.version = NumbersInWords::VERSION
|
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'
|
23
17
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
end
|
18
|
+
gem.add_development_dependency 'rspec', '~> 3.4.0'
|
19
|
+
gem.add_development_dependency 'rubocop'
|
20
|
+
|
21
|
+
gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
|
22
|
+
gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
|
23
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
24
|
+
gem.require_paths = ['lib']
|
32
25
|
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
|