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
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,24 +1,16 @@
|
|
1
1
|
[](https://travis-ci.org/markburns/numbers_in_words)
|
2
|
-
[](https://codeclimate.com/github/markburns/numbers_in_words/coverage)
|
2
|
+
[](https://codeclimate.com/github/markburns/numbers_in_words/maintainability)
|
3
|
+
[](https://codeclimate.com/github/markburns/numbers_in_words/test_coverage)
|
5
4
|
[](https://rubygems.org/gems/numbers_in_words)
|
6
5
|
[](http://markburns.mit-license.org)
|
7
|
-
[](https://github.com/badges/badgerbadgerbadger)
|
8
6
|
|
9
7
|
Installation
|
10
8
|
============
|
11
9
|
|
12
10
|
```ruby
|
13
11
|
gem 'numbers_in_words'
|
14
|
-
|
15
|
-
require 'numbers_in_words'
|
16
|
-
require 'numbers_in_words/duck_punch' #optional see why later
|
17
12
|
```
|
18
13
|
|
19
|
-
This project was created for a test for a job interview. I haven't really used
|
20
|
-
it myself, but I saw it mentioned somewhere so I thought I'd tidy it up a bit.
|
21
|
-
|
22
14
|
Usage
|
23
15
|
=========
|
24
16
|
|
@@ -33,6 +25,9 @@ NumbersInWords.in_numbers("one googol")
|
|
33
25
|
|
34
26
|
NumbersInWords.in_numbers("Seventy million, five-hundred and fifty six thousand point eight nine three")
|
35
27
|
#=> 70556000.893
|
28
|
+
|
29
|
+
NumbersInWords.in_numbers("nineteen sixty five")
|
30
|
+
#=> 1965
|
36
31
|
```
|
37
32
|
|
38
33
|
|
@@ -50,36 +45,3 @@ require 'numbers_in_words/duck_punch'
|
|
50
45
|
"Seventy million, five-hundred and fifty six thousand point eight nine three".in_numbers
|
51
46
|
#=> 70556000.893
|
52
47
|
```
|
53
|
-
|
54
|
-
|
55
|
-
NoMethodError `in_words` or `in_numbers`
|
56
|
-
----------
|
57
|
-
I'm going to hopefully preempt some support queries by predicting this will happen:
|
58
|
-
|
59
|
-
You've got one of:
|
60
|
-
|
61
|
-
```
|
62
|
-
NoMethodError: undefined method `in_words' for 123:Fixnum
|
63
|
-
NoMethodError: undefined method `in_numbers' for "123":String
|
64
|
-
```
|
65
|
-
|
66
|
-
Previous versions of this gem duckpunched Fixnum and String with a whole bunch
|
67
|
-
of methods. This gem will now only add methods if you specifically tell it to
|
68
|
-
with:
|
69
|
-
|
70
|
-
```ruby
|
71
|
-
require 'numbers_in_words'
|
72
|
-
require 'numbers_in_words/duck_punch'
|
73
|
-
```
|
74
|
-
|
75
|
-
Plus it now only adds a single `#in_words` method to `Numeric` and an `#in_numbers`
|
76
|
-
method to `String` instead of a whole bunch of them.
|
77
|
-
|
78
|
-
|
79
|
-
Future plans
|
80
|
-
============
|
81
|
-
|
82
|
-
* Handle complex numbers
|
83
|
-
* Option for outputting punctuation
|
84
|
-
* Reject invalid numbers
|
85
|
-
* Support for other languages
|
data/Rakefile
CHANGED
data/lib/numbers_in_words.rb
CHANGED
@@ -1,33 +1,57 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require 'numbers_in_words/
|
5
|
-
|
6
|
-
require 'numbers_in_words/english/constants'
|
7
|
-
require 'numbers_in_words/english/language_writer_english'
|
8
|
-
|
9
|
-
require 'numbers_in_words/number_group'
|
10
|
-
require 'numbers_in_words/number_parser'
|
11
|
-
require 'numbers_in_words/to_number'
|
3
|
+
require 'numbers_in_words/version'
|
4
|
+
require 'numbers_in_words/exceptional_numbers'
|
12
5
|
require 'numbers_in_words/to_word'
|
6
|
+
require 'numbers_in_words/parsing/to_number'
|
13
7
|
|
14
8
|
module NumbersInWords
|
15
|
-
LENGTH_OF_GOOGOL
|
9
|
+
LENGTH_OF_GOOGOL = 101 # length of the string i.e. one with 100 zeros
|
10
|
+
Error = ::Class.new(::StandardError)
|
11
|
+
AmbiguousParsingError = ::Class.new(Error)
|
12
|
+
DivideByZeroError = ::Class.new(Error)
|
13
|
+
InvalidNumber = ::Class.new(Error)
|
16
14
|
|
17
15
|
class << self
|
18
|
-
|
16
|
+
extend Forwardable
|
17
|
+
def_delegators :exceptional_numbers, :fraction
|
18
|
+
|
19
|
+
def in_words(num, fraction: false)
|
20
|
+
ToWord.new(num).in_words(fraction: fraction)
|
21
|
+
end
|
22
|
+
|
23
|
+
def in_numbers(words, only_compress: false)
|
24
|
+
ToNumber.new(words, only_compress).call
|
25
|
+
end
|
26
|
+
|
27
|
+
def exceptional_numbers
|
28
|
+
@exceptional_numbers ||= ExceptionalNumbers.new
|
29
|
+
end
|
19
30
|
|
20
|
-
def
|
21
|
-
|
31
|
+
def lookup(number)
|
32
|
+
exceptional_numbers.lookup(number)
|
22
33
|
end
|
23
34
|
|
24
|
-
def
|
25
|
-
|
35
|
+
def exceptional_number(text)
|
36
|
+
exceptional_numbers_to_i[text]
|
26
37
|
end
|
27
38
|
|
28
|
-
def
|
29
|
-
|
39
|
+
def power_of_ten(text)
|
40
|
+
powers_of_ten_to_i[text]
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def exceptional_numbers_to_i
|
46
|
+
@exceptional_numbers_to_i ||= swap_keys exceptional_numbers.to_h
|
47
|
+
end
|
48
|
+
|
49
|
+
def powers_of_ten_to_i
|
50
|
+
@powers_of_ten_to_i ||= swap_keys POWERS_OF_TEN
|
51
|
+
end
|
52
|
+
|
53
|
+
def swap_keys(hash)
|
54
|
+
hash.each_with_object({}) { |(k, v), h| h[v] = k }
|
30
55
|
end
|
31
56
|
end
|
32
57
|
end
|
33
|
-
|
@@ -1,19 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module NumbersInWords
|
2
|
-
|
3
|
-
|
4
|
+
module NumericExtension
|
5
|
+
def in_words(fraction: false)
|
6
|
+
NumbersInWords::ToWord.new(self).in_words(fraction: fraction)
|
7
|
+
end
|
4
8
|
end
|
5
|
-
end
|
6
9
|
|
7
|
-
module
|
8
|
-
|
9
|
-
|
10
|
+
module StringExtension
|
11
|
+
def in_numbers(only_compress: false)
|
12
|
+
NumbersInWords::ToNumber.new(self, only_compress).call
|
13
|
+
end
|
10
14
|
end
|
11
15
|
end
|
12
16
|
|
13
17
|
class String
|
14
|
-
include
|
18
|
+
include NumbersInWords::StringExtension
|
15
19
|
end
|
16
20
|
|
17
21
|
class Numeric
|
18
|
-
include NumbersInWords
|
22
|
+
include NumbersInWords::NumericExtension
|
19
23
|
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require_relative 'fraction'
|
6
|
+
require_relative 'powers_of_ten'
|
7
|
+
|
8
|
+
module NumbersInWords
|
9
|
+
class ExceptionalNumbers
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
DEFINITIONS = {
|
13
|
+
0 => { number: 'zero', ordinal: 'zeroth' },
|
14
|
+
1 => { number: 'one', ordinal: 'first' },
|
15
|
+
2 => { number: 'two', ordinal: 'second', fraction: { singular: 'half', plural: 'halves' } },
|
16
|
+
3 => { number: 'three', ordinal: 'third' },
|
17
|
+
4 => { number: 'four', ordinal: 'fourth', fraction: { singular: 'quarter', plural: 'quarters' } },
|
18
|
+
5 => { number: 'five', ordinal: 'fifth' },
|
19
|
+
6 => { number: 'six' },
|
20
|
+
7 => { number: 'seven' },
|
21
|
+
8 => { number: 'eight', ordinal: 'eighth' },
|
22
|
+
9 => { number: 'nine', ordinal: 'ninth' },
|
23
|
+
10 => { number: 'ten' },
|
24
|
+
11 => { number: 'eleven' },
|
25
|
+
12 => { number: 'twelve', ordinal: 'twelfth' },
|
26
|
+
13 => { number: 'thirteen' },
|
27
|
+
14 => { number: 'fourteen' },
|
28
|
+
15 => { number: 'fifteen' },
|
29
|
+
16 => { number: 'sixteen' },
|
30
|
+
17 => { number: 'seventeen' },
|
31
|
+
18 => { number: 'eighteen' },
|
32
|
+
19 => { number: 'nineteen' },
|
33
|
+
20 => { number: 'twenty', ordinal: 'twentieth' },
|
34
|
+
30 => { number: 'thirty', ordinal: 'thirtieth' },
|
35
|
+
40 => { number: 'forty', ordinal: 'fortieth' },
|
36
|
+
50 => { number: 'fifty', ordinal: 'fiftieth' },
|
37
|
+
60 => { number: 'sixty', ordinal: 'sixtieth' },
|
38
|
+
70 => { number: 'seventy', ordinal: 'seventieth' },
|
39
|
+
80 => { number: 'eighty', ordinal: 'eightieth' },
|
40
|
+
90 => { number: 'ninety', ordinal: 'ninetieth' }
|
41
|
+
}.freeze
|
42
|
+
|
43
|
+
def fraction_names
|
44
|
+
@fraction_names ||= determine_fraction_names
|
45
|
+
end
|
46
|
+
|
47
|
+
def lookup_fraction(words)
|
48
|
+
fraction_lookup[words]
|
49
|
+
end
|
50
|
+
|
51
|
+
def fraction_lookup
|
52
|
+
@fraction_lookup ||= generate_fraction_lookup
|
53
|
+
end
|
54
|
+
|
55
|
+
def lookup(number)
|
56
|
+
to_h[number]
|
57
|
+
end
|
58
|
+
|
59
|
+
def fractions
|
60
|
+
DEFINITIONS
|
61
|
+
end
|
62
|
+
|
63
|
+
def fraction(denominator: nil, numerator: nil, word: nil)
|
64
|
+
raise unless denominator || word
|
65
|
+
|
66
|
+
numerator ||= 1
|
67
|
+
|
68
|
+
denominator ||= NumbersInWords.in_numbers(word)
|
69
|
+
|
70
|
+
Fraction.new(denominator: denominator, numerator: numerator, attributes: DEFINITIONS[denominator])
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_h
|
74
|
+
@to_h ||= DEFINITIONS.transform_values do |h|
|
75
|
+
h[:number]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def generate_fraction_lookup
|
82
|
+
named_fractions.each_with_object({}) do |f, result|
|
83
|
+
f.lookup_keys.each do |k|
|
84
|
+
key = k.split(' ').last
|
85
|
+
result[key] = 1.0 / f.denominator.to_f
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def named_fractions
|
91
|
+
@named_fractions ||= numbers.flat_map do |n|
|
92
|
+
[
|
93
|
+
Fraction.new(denominator: n, numerator: 1),
|
94
|
+
Fraction.new(denominator: n, numerator: 2)
|
95
|
+
]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def numbers
|
100
|
+
(2..100).to_a + powers_of_ten_skipping_googolplex.map { |p| 10**p }
|
101
|
+
end
|
102
|
+
|
103
|
+
def powers_of_ten_skipping_googolplex
|
104
|
+
POWERS_OF_TEN.keys[0..-2]
|
105
|
+
end
|
106
|
+
|
107
|
+
def determine_fraction_names
|
108
|
+
names = named_fractions.map(&:in_words)
|
109
|
+
|
110
|
+
words = names.map(&:split).map(&:last)
|
111
|
+
words += strip_punctuation(words)
|
112
|
+
words.uniq
|
113
|
+
end
|
114
|
+
|
115
|
+
def strip_punctuation(words)
|
116
|
+
words.map { |w| w.gsub(/^a-z/, ' ') }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,151 @@
|
|
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 to_r
|
22
|
+
return 0.0 if denominator == Float::INFINITY
|
23
|
+
|
24
|
+
(numerator / denominator.to_f).rationalize(EPSILON)
|
25
|
+
end
|
26
|
+
|
27
|
+
def lookup_keys
|
28
|
+
key = in_words
|
29
|
+
key2 = strip_punctuation(key.split(' ')).join(' ')
|
30
|
+
|
31
|
+
key3 = "a #{key}"
|
32
|
+
key4 = "an #{key}"
|
33
|
+
key5 = "a #{key2}"
|
34
|
+
key6 = "an #{key2}"
|
35
|
+
[key, key2, key3, key4, key5, key6].uniq
|
36
|
+
end
|
37
|
+
|
38
|
+
def in_words
|
39
|
+
if denominator == Float::INFINITY
|
40
|
+
# We've reached the limits of ruby's number system
|
41
|
+
# by the time we get to a googolplex (10 ** (10 ** 100))
|
42
|
+
# I suppose we could also call this an 'infinitieth'
|
43
|
+
return pluralize? ? 'googolplexths' : 'googolplexth'
|
44
|
+
end
|
45
|
+
|
46
|
+
NumbersInWords.in_words(numerator) + ' ' + fraction
|
47
|
+
end
|
48
|
+
|
49
|
+
def ordinal
|
50
|
+
pluralize? ? pluralized_ordinal_in_words : singular_ordinal_in_words
|
51
|
+
end
|
52
|
+
|
53
|
+
def fraction
|
54
|
+
pluralize? ? pluralized_fraction : singular_fraction
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def strip_punctuation(words)
|
60
|
+
words.map { |w| w.gsub(/^a-z/, ' ') }
|
61
|
+
end
|
62
|
+
|
63
|
+
def pluralized_fraction
|
64
|
+
fraction_plural || pluralized_ordinal_in_words
|
65
|
+
end
|
66
|
+
|
67
|
+
def singular_fraction
|
68
|
+
fraction_singular || singular_ordinal_in_words
|
69
|
+
end
|
70
|
+
|
71
|
+
def pluralized_ordinal_in_words
|
72
|
+
pluralized_ordinal || denominator_ordinal_in_words
|
73
|
+
end
|
74
|
+
|
75
|
+
def singular_ordinal_in_words
|
76
|
+
singular_ordinal || denominator_ordinal_in_words
|
77
|
+
end
|
78
|
+
|
79
|
+
def singular_ordinal
|
80
|
+
attributes[:ordinal]
|
81
|
+
end
|
82
|
+
|
83
|
+
def pluralized_ordinal
|
84
|
+
singular_ordinal && singular_ordinal + 's'
|
85
|
+
end
|
86
|
+
|
87
|
+
def pluralize?
|
88
|
+
numerator > 1
|
89
|
+
end
|
90
|
+
|
91
|
+
def denominator_ordinal_in_words
|
92
|
+
if denominator > 100
|
93
|
+
# one hundred and second
|
94
|
+
with_remainder(100, ' and ')
|
95
|
+
elsif denominator > 19
|
96
|
+
# two thirty-fifths
|
97
|
+
with_remainder(10, '-')
|
98
|
+
else
|
99
|
+
# one seventh
|
100
|
+
singular = NumbersInWords.in_words(denominator) + 'th'
|
101
|
+
pluralize? ? singular + 's' : singular
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def plural
|
106
|
+
exception && (fraction_plural || singular + 's') || ordinal_plural
|
107
|
+
end
|
108
|
+
|
109
|
+
def singular
|
110
|
+
(exception && exception[:singular]) || ordinal
|
111
|
+
end
|
112
|
+
|
113
|
+
def with_remainder(mod, join_word)
|
114
|
+
rest = denominator % mod
|
115
|
+
main = denominator - rest
|
116
|
+
main = NumbersInWords.in_words(main)
|
117
|
+
|
118
|
+
main = main.gsub(/^one /, '') if pluralize?
|
119
|
+
|
120
|
+
rest_zero(rest, main) || joined(main, rest, join_word)
|
121
|
+
end
|
122
|
+
|
123
|
+
def joined(main, rest, join_word)
|
124
|
+
main +
|
125
|
+
join_word +
|
126
|
+
self.class.new(numerator: numerator, denominator: rest).ordinal
|
127
|
+
end
|
128
|
+
|
129
|
+
def rest_zero(rest, main)
|
130
|
+
return unless rest.zero?
|
131
|
+
|
132
|
+
if pluralize?
|
133
|
+
main + 'ths'
|
134
|
+
else
|
135
|
+
main + 'th'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def exception
|
140
|
+
attributes[:fraction]
|
141
|
+
end
|
142
|
+
|
143
|
+
def fraction_singular
|
144
|
+
exception && exception[:singular]
|
145
|
+
end
|
146
|
+
|
147
|
+
def fraction_plural
|
148
|
+
exception && exception[:plural]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|