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,136 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class Fraction
|
5
|
+
attr_reader :denominator, :numerator, :attributes
|
6
|
+
|
7
|
+
def self.in_words(that)
|
8
|
+
r = that.rationalize(EPSILON)
|
9
|
+
|
10
|
+
NumbersInWords
|
11
|
+
.fraction(denominator: r.denominator, numerator: r.numerator)
|
12
|
+
.in_words
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(denominator:, numerator: 1, attributes: nil)
|
16
|
+
@denominator = denominator
|
17
|
+
@numerator = numerator
|
18
|
+
@attributes = attributes || NumbersInWords::ExceptionalNumbers::DEFINITIONS[denominator] || {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def lookup_keys
|
22
|
+
key = in_words
|
23
|
+
key2 = strip_punctuation(key.split(' ')).join(' ')
|
24
|
+
|
25
|
+
key3 = "a #{key}"
|
26
|
+
key4 = "an #{key}"
|
27
|
+
key5 = "a #{key2}"
|
28
|
+
key6 = "an #{key2}"
|
29
|
+
[key, key2, key3, key4, key5, key6].uniq
|
30
|
+
end
|
31
|
+
|
32
|
+
def in_words
|
33
|
+
NumbersInWords.in_words(numerator) + ' ' + fraction
|
34
|
+
end
|
35
|
+
|
36
|
+
def ordinal
|
37
|
+
pluralize? ? pluralized_ordinal_in_words : singular_ordinal_in_words
|
38
|
+
end
|
39
|
+
|
40
|
+
def fraction
|
41
|
+
if denominator == Float::INFINITY
|
42
|
+
# We've reached the limits of ruby's number system
|
43
|
+
# by the time we get to a googolplex (10 ** (10 ** 100))
|
44
|
+
return pluralize? ? 'infinitieths' : 'infinitieth'
|
45
|
+
end
|
46
|
+
|
47
|
+
pluralize? ? pluralized_fraction : singular_fraction
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def strip_punctuation(words)
|
53
|
+
words.map { |w| w.gsub(/^a-z/, ' ') }
|
54
|
+
end
|
55
|
+
|
56
|
+
def pluralized_fraction
|
57
|
+
fraction_plural || pluralized_ordinal_in_words
|
58
|
+
end
|
59
|
+
|
60
|
+
def singular_fraction
|
61
|
+
fraction_singular || singular_ordinal_in_words
|
62
|
+
end
|
63
|
+
|
64
|
+
def pluralized_ordinal_in_words
|
65
|
+
pluralized_ordinal || denominator_ordinal_in_words
|
66
|
+
end
|
67
|
+
|
68
|
+
def singular_ordinal_in_words
|
69
|
+
singular_ordinal || denominator_ordinal_in_words
|
70
|
+
end
|
71
|
+
|
72
|
+
def singular_ordinal
|
73
|
+
attributes[:ordinal]
|
74
|
+
end
|
75
|
+
|
76
|
+
def pluralized_ordinal
|
77
|
+
singular_ordinal && singular_ordinal + 's'
|
78
|
+
end
|
79
|
+
|
80
|
+
def pluralize?
|
81
|
+
numerator > 1
|
82
|
+
end
|
83
|
+
|
84
|
+
def denominator_ordinal_in_words
|
85
|
+
if denominator > 100
|
86
|
+
# one hundred and second
|
87
|
+
with_remainder(100, ' and ')
|
88
|
+
elsif denominator > 19
|
89
|
+
# two thirty-fifths
|
90
|
+
with_remainder(10, '-')
|
91
|
+
else
|
92
|
+
# one seventh
|
93
|
+
singular = NumbersInWords.in_words(denominator) + 'th'
|
94
|
+
pluralize? ? singular + 's' : singular
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def with_remainder(mod, join_word)
|
99
|
+
rest = denominator % mod
|
100
|
+
main = denominator - rest
|
101
|
+
main = NumbersInWords.in_words(main)
|
102
|
+
|
103
|
+
main = main.gsub(/^one /, '') if pluralize?
|
104
|
+
|
105
|
+
rest_zero(rest, main) || joined(main, rest, join_word)
|
106
|
+
end
|
107
|
+
|
108
|
+
def joined(main, rest, join_word)
|
109
|
+
main +
|
110
|
+
join_word +
|
111
|
+
self.class.new(numerator: numerator, denominator: rest).ordinal
|
112
|
+
end
|
113
|
+
|
114
|
+
def rest_zero(rest, main)
|
115
|
+
return unless rest.zero?
|
116
|
+
|
117
|
+
if pluralize?
|
118
|
+
main + 'ths'
|
119
|
+
else
|
120
|
+
main + 'th'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def exception
|
125
|
+
attributes[:fraction]
|
126
|
+
end
|
127
|
+
|
128
|
+
def fraction_singular
|
129
|
+
exception && exception[:singular]
|
130
|
+
end
|
131
|
+
|
132
|
+
def fraction_plural
|
133
|
+
exception && exception[:plural]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class NumberGroup
|
5
|
+
include Enumerable
|
6
|
+
attr_accessor :number
|
7
|
+
|
8
|
+
def self.groups_of(number, size)
|
9
|
+
new(number).groups(size)
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(number)
|
13
|
+
@number = number
|
14
|
+
end
|
15
|
+
|
16
|
+
# split into groups this gives us 1234567 => 123 456 7
|
17
|
+
# so we need to reverse first
|
18
|
+
# in stages
|
19
|
+
def groups(size)
|
20
|
+
# 1234567 => %w(765 432 1)
|
21
|
+
@array = in_groups_of(@number.to_s.reverse.split(''), size)
|
22
|
+
# %w(765 432 1) => %w(1 432 765)
|
23
|
+
@array.reverse!
|
24
|
+
|
25
|
+
# %w(1 432 765) => [1, 234, 567]
|
26
|
+
@array.map! { |group| group.reverse.join('').to_i }
|
27
|
+
@array.reverse! # put in ascending order of power of ten
|
28
|
+
|
29
|
+
power = 0
|
30
|
+
|
31
|
+
# [1, 234, 567] => {6 => 1, 3 => 234, 0 => 567}
|
32
|
+
@array.each_with_object({}) do |digits, o|
|
33
|
+
o[power] = digits
|
34
|
+
power += size
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def split_decimals
|
39
|
+
return unless @number.is_a? Float
|
40
|
+
|
41
|
+
int, decimal = @number.to_s.split '.'
|
42
|
+
|
43
|
+
[int.to_i, decimal.split(//).map(&:to_i)]
|
44
|
+
end
|
45
|
+
|
46
|
+
def split_googols
|
47
|
+
googols = @number.to_s[0..-LENGTH_OF_GOOGOL].to_i
|
48
|
+
remainder = @number.to_s[(1 - LENGTH_OF_GOOGOL)..].to_i
|
49
|
+
[googols, remainder]
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def in_groups_of(array, number, fill_with = nil)
|
55
|
+
# size % number gives how many extra we have;
|
56
|
+
# subtracting from number gives how many to add;
|
57
|
+
# modulo number ensures we don't add group of just fill.
|
58
|
+
padding = (number - array.size % number) % number
|
59
|
+
collection = array.dup.concat(Array.new(padding, fill_with))
|
60
|
+
|
61
|
+
collection.each_slice(number).to_a
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
module FractionParsing
|
5
|
+
def fraction(text)
|
6
|
+
return unless possible_fraction?(text)
|
7
|
+
|
8
|
+
NumbersInWords.exceptional_numbers.lookup_fraction(text)
|
9
|
+
end
|
10
|
+
|
11
|
+
def strip_punctuation(text)
|
12
|
+
text = text.downcase.gsub(/[^a-z 0-9]/, ' ')
|
13
|
+
to_remove = true
|
14
|
+
|
15
|
+
to_remove = text.gsub! ' ', ' ' while to_remove
|
16
|
+
|
17
|
+
text
|
18
|
+
end
|
19
|
+
|
20
|
+
def possible_fraction?(text)
|
21
|
+
words = text.split(' ')
|
22
|
+
result = words & NumbersInWords.exceptional_numbers.fraction_names
|
23
|
+
result.length.positive?
|
24
|
+
end
|
25
|
+
|
26
|
+
def text_including_punctuation
|
27
|
+
to_s.strip
|
28
|
+
end
|
29
|
+
|
30
|
+
def text
|
31
|
+
strip_punctuation text_including_punctuation
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
require_relative 'parse_fractions'
|
5
|
+
require_relative 'parse_status'
|
6
|
+
require_relative 'parse_individual_number'
|
7
|
+
require_relative 'pair_parsing'
|
8
|
+
|
9
|
+
module NumbersInWords
|
10
|
+
class NumberParser
|
11
|
+
# Example: 364,895,457,898
|
12
|
+
# three hundred and sixty four billion eight hundred and ninety five million
|
13
|
+
# four hundred and fifty seven thousand eight hundred and ninety eight
|
14
|
+
#
|
15
|
+
# 3 100 60 4 10^9, 8 100 90 5 10^6, 4 100 50 7 1000, 8 100 90 8
|
16
|
+
# memory answer
|
17
|
+
# x1. 3 add to memory because answer and memory are zero 3 0
|
18
|
+
# x2. memory * 100 (because memory<100) 300 0
|
19
|
+
# x3. 60 add to memory because memory > 60 360 0
|
20
|
+
# x3. 4 add to memory because memory > 4 364 0
|
21
|
+
# x4. multiply memory by 10^9 because memory < power of ten 364*10^9 0
|
22
|
+
# x5. add memory to answer (and reset)memory > 8 (memory pow of ten > 2) 0 364*10^9
|
23
|
+
# x6. 8 add to memory because not finished 8 ''
|
24
|
+
# x7. multiply memory by 100 because memory < 100 800 ''
|
25
|
+
# x8. add 90 to memory because memory > 90 890 ''
|
26
|
+
# x9. add 5 to memory because memory > 5 895 ''
|
27
|
+
# x10. multiply memory by 10^6 because memory < power of ten 895*10^6 ''
|
28
|
+
# x11. add memory to answer (and reset) because memory power ten > 2 0 364895 * 10^6
|
29
|
+
# x12. 4 add to memory because not finished 4 ''
|
30
|
+
# x13. memory * 100 because memory < 100 400 ''
|
31
|
+
# x14. memory + 50 because memory > 50 450 ''
|
32
|
+
# x15. memory + 7 because memory > 7 457 ''
|
33
|
+
# x16. memory * 1000 because memory < 1000 457000 ''
|
34
|
+
# x17. add memory to answer (and reset)memory > 8 (memory pow of ten > 2) 0 364895457000
|
35
|
+
# x18. 8 add to memory because not finished 8 ''
|
36
|
+
# x19. memory * 100 because memory < 100 800 ''
|
37
|
+
# x14. memory + 90 because memory > 90 890 ''
|
38
|
+
# x15. memory + 8 because memory > 8 898 ''
|
39
|
+
# 16. finished so add memory to answer
|
40
|
+
|
41
|
+
# Example
|
42
|
+
# 2001
|
43
|
+
# two thousand and one
|
44
|
+
# 2 1000 1
|
45
|
+
# memory answer
|
46
|
+
# 1. add 2 to memory because first 2 0
|
47
|
+
# 2. multiply memory by 1000 because memory < 1000 2000 0
|
48
|
+
# 3. add memory to answer,reset, because power of ten>2 0 2000
|
49
|
+
# 4. add 1 to memory 1 2000
|
50
|
+
# 5. finish - add memory to answer 0 2001
|
51
|
+
|
52
|
+
SCALES_N = [10**2, 10**3, 10**6, 10**9, 10**12, 10**100].freeze
|
53
|
+
|
54
|
+
def parse(nums, only_compress: false)
|
55
|
+
fractions(nums) ||
|
56
|
+
small_numbers(nums, only_compress) ||
|
57
|
+
pair_parsing(nums, only_compress) ||
|
58
|
+
parse_each(nums)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def fractions(nums)
|
64
|
+
ParseFractions.new(nums).call
|
65
|
+
end
|
66
|
+
|
67
|
+
# 7 0.066666666666667 => 0.46666666666666
|
68
|
+
|
69
|
+
# 15 => 15
|
70
|
+
def small_numbers(nums, only_compress)
|
71
|
+
return unless nums.length < 2
|
72
|
+
return nums if only_compress
|
73
|
+
|
74
|
+
nums.empty? ? 0 : nums[0]
|
75
|
+
end
|
76
|
+
|
77
|
+
# 15 75 => 1,575
|
78
|
+
def pair_parsing(nums, only_compress)
|
79
|
+
return if (SCALES_N & nums).any?
|
80
|
+
|
81
|
+
pair_parse(nums, only_compress)
|
82
|
+
end
|
83
|
+
|
84
|
+
def parse_each(nums)
|
85
|
+
status = ParseStatus.new
|
86
|
+
|
87
|
+
nums.each do |num|
|
88
|
+
ParseIndividualNumber.new(status, num).call
|
89
|
+
end
|
90
|
+
|
91
|
+
status.calculate
|
92
|
+
end
|
93
|
+
|
94
|
+
def pair_parse(nums, only_compress)
|
95
|
+
PairParsing.new(nums, only_compress).pair_parse
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class PairParsing
|
5
|
+
attr_accessor :ints
|
6
|
+
attr_reader :only_compress
|
7
|
+
|
8
|
+
def initialize(ints, only_compress)
|
9
|
+
@ints = ints
|
10
|
+
@only_compress = only_compress
|
11
|
+
end
|
12
|
+
|
13
|
+
# 15,16
|
14
|
+
# 85,16
|
15
|
+
def pair_parse
|
16
|
+
ints = compressed
|
17
|
+
return ints if only_compress
|
18
|
+
|
19
|
+
return ints[0] if ints.length == 1
|
20
|
+
|
21
|
+
sum = 0
|
22
|
+
|
23
|
+
ints.each do |n|
|
24
|
+
sum *= n >= 10 ? 100 : 10
|
25
|
+
sum += n
|
26
|
+
end
|
27
|
+
|
28
|
+
sum
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# [40, 2] => [42]
|
34
|
+
def compressed
|
35
|
+
return [] if ints.empty?
|
36
|
+
|
37
|
+
result = []
|
38
|
+
index = 0
|
39
|
+
|
40
|
+
index, result = compress_numbers(result, index)
|
41
|
+
|
42
|
+
result << ints[-1] if index < ints.length
|
43
|
+
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def compress_numbers(result, index)
|
48
|
+
while index < ints.length - 1
|
49
|
+
int, jump = compress_int(ints[index], ints[index + 1])
|
50
|
+
result << int
|
51
|
+
index += jump
|
52
|
+
end
|
53
|
+
|
54
|
+
[index, result]
|
55
|
+
end
|
56
|
+
|
57
|
+
def compress_int(int, next_int)
|
58
|
+
tens = (int % 10).zero? && int > 10
|
59
|
+
return [int + next_int, 2] if tens && next_int < 10
|
60
|
+
|
61
|
+
[int, 1]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class ParseFractions
|
5
|
+
attr_reader :nums
|
6
|
+
|
7
|
+
def initialize(nums)
|
8
|
+
@nums = nums.map(&:to_f)
|
9
|
+
end
|
10
|
+
|
11
|
+
def call
|
12
|
+
return if no_fractions?
|
13
|
+
|
14
|
+
just_fraction || calculate
|
15
|
+
end
|
16
|
+
|
17
|
+
def calculate
|
18
|
+
(parse(numbers) * parse(fractions)).rationalize(EPSILON).to_f
|
19
|
+
end
|
20
|
+
|
21
|
+
def parse(numbers)
|
22
|
+
NumberParser.new.parse(numbers)
|
23
|
+
end
|
24
|
+
|
25
|
+
def numbers
|
26
|
+
nums[0..index_of_fraction - 1]
|
27
|
+
end
|
28
|
+
|
29
|
+
def fractions
|
30
|
+
nums[index_of_fraction..]
|
31
|
+
end
|
32
|
+
|
33
|
+
def just_fraction
|
34
|
+
return nums.first if index_of_fraction.zero?
|
35
|
+
end
|
36
|
+
|
37
|
+
def index_of_fraction
|
38
|
+
nums.index { |n| n < 1.0 }
|
39
|
+
end
|
40
|
+
|
41
|
+
def no_fractions?
|
42
|
+
nums.all? { |n| n.zero? || n >= 1.0 }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NumbersInWords
|
4
|
+
class ParseIndividualNumber
|
5
|
+
extend Forwardable
|
6
|
+
def_delegators :parse_status, :reset=, :memory=, :answer=, :reset, :memory, :answer
|
7
|
+
|
8
|
+
attr_reader :parse_status, :num
|
9
|
+
|
10
|
+
def initialize(parse_status, num)
|
11
|
+
@parse_status = parse_status
|
12
|
+
@num = num
|
13
|
+
end
|
14
|
+
|
15
|
+
def call
|
16
|
+
if reset
|
17
|
+
clear
|
18
|
+
else
|
19
|
+
handle_power_of_ten
|
20
|
+
|
21
|
+
update_memory
|
22
|
+
end
|
23
|
+
|
24
|
+
[reset, memory, answer]
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def clear
|
30
|
+
self.reset = false
|
31
|
+
self.memory += num
|
32
|
+
end
|
33
|
+
|
34
|
+
def handle_power_of_ten
|
35
|
+
# x4. multiply memory by 10^9 because memory < power of ten
|
36
|
+
return unless power_of_ten?(num)
|
37
|
+
return unless power_of_ten(num) > 2
|
38
|
+
|
39
|
+
self.memory *= num
|
40
|
+
# 17. add memory to answer (and reset) (memory pow of ten > 2)
|
41
|
+
self.answer += memory
|
42
|
+
self.memory = 0
|
43
|
+
self.reset = true
|
44
|
+
end
|
45
|
+
|
46
|
+
def update_memory
|
47
|
+
self.memory = new_memory
|
48
|
+
end
|
49
|
+
|
50
|
+
def new_memory
|
51
|
+
if memory < num
|
52
|
+
memory * num
|
53
|
+
else
|
54
|
+
memory + num
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def power_of_ten(integer)
|
59
|
+
Math.log10(integer)
|
60
|
+
end
|
61
|
+
|
62
|
+
def power_of_ten?(integer)
|
63
|
+
return true if integer.zero?
|
64
|
+
|
65
|
+
power_of_ten(integer) == power_of_ten(integer).to_i
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|