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.
Files changed (44) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +1 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +35 -1148
  5. data/.travis.yml +4 -4
  6. data/Gemfile +3 -1
  7. data/README.md +5 -43
  8. data/Rakefile +3 -1
  9. data/lib/numbers_in_words.rb +43 -19
  10. data/lib/numbers_in_words/duck_punch.rb +12 -8
  11. data/lib/numbers_in_words/exceptional_numbers.rb +119 -0
  12. data/lib/numbers_in_words/fraction.rb +151 -0
  13. data/lib/numbers_in_words/number_group.rb +34 -21
  14. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  15. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  16. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  17. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  18. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  19. data/lib/numbers_in_words/parsing/to_number.rb +159 -0
  20. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  21. data/lib/numbers_in_words/to_word.rb +78 -13
  22. data/lib/numbers_in_words/version.rb +3 -1
  23. data/lib/numbers_in_words/writer.rb +69 -0
  24. data/numbers_in_words.gemspec +14 -13
  25. data/spec/exceptional_numbers_spec.rb +26 -0
  26. data/spec/fraction_spec.rb +132 -0
  27. data/spec/fractions_spec.rb +31 -0
  28. data/spec/non_monkey_patch_spec.rb +39 -20
  29. data/spec/number_group_spec.rb +12 -12
  30. data/spec/number_parser_spec.rb +63 -0
  31. data/spec/numbers_in_words_spec.rb +56 -69
  32. data/spec/numerical_strings_spec.rb +28 -12
  33. data/spec/spec_helper.rb +2 -4
  34. data/spec/to_word_spec.rb +18 -0
  35. data/spec/words_in_numbers_spec.rb +130 -125
  36. data/spec/writer_spec.rb +26 -0
  37. data/spec/years_spec.rb +23 -13
  38. metadata +40 -25
  39. data/lib/numbers_in_words/english/constants.rb +0 -124
  40. data/lib/numbers_in_words/english/language_writer_english.rb +0 -116
  41. data/lib/numbers_in_words/language_writer.rb +0 -30
  42. data/lib/numbers_in_words/number_parser.rb +0 -135
  43. data/lib/numbers_in_words/to_number.rb +0 -88
  44. data/spec/language_writer_spec.rb +0 -23
@@ -1,7 +1,7 @@
1
+ sudo: false
1
2
  language: ruby
2
3
  rvm:
3
- - "1.9.3"
4
- - "2.1.7"
5
- - "2.2.3"
6
- - rbx
4
+ - "2.4.6"
5
+ - "2.5.5"
6
+ - "2.6.3"
7
7
  script: ./bin/spec
data/Gemfile CHANGED
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
- gem "codeclimate-test-reporter", group: :test, require: nil
5
+ gem 'byebug'
4
6
  # Specify your gem's dependencies in numbers_in_words.gemspec
5
7
  gemspec
data/README.md CHANGED
@@ -1,24 +1,16 @@
1
1
  [![Build Status](http://img.shields.io/travis/markburns/numbers_in_words.svg)](https://travis-ci.org/markburns/numbers_in_words)
2
- [![Dependency Status](http://img.shields.io/gemnasium/markburns/numbers_in_words.svg)](https://gemnasium.com/markburns/numbers_in_words)
3
- [![Code Climate](http://img.shields.io/codeclimate/github/markburns/numbers_in_words.svg)](https://codeclimate.com/github/markburns/numbers_in_words)
4
- [![Test Coverage](https://codeclimate.com/github/markburns/numbers_in_words/badges/coverage.svg)](https://codeclimate.com/github/markburns/numbers_in_words/coverage)
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/a51210488896b798af20/maintainability)](https://codeclimate.com/github/markburns/numbers_in_words/maintainability)
3
+ [![Test Coverage](https://api.codeclimate.com/v1/badges/a51210488896b798af20/test_coverage)](https://codeclimate.com/github/markburns/numbers_in_words/test_coverage)
5
4
  [![Gem Version](http://img.shields.io/gem/v/numbers_in_words.svg)](https://rubygems.org/gems/numbers_in_words)
6
5
  [![License](http://img.shields.io/:license-mit-blue.svg)](http://markburns.mit-license.org)
7
- [![Badges](http://img.shields.io/:badges-7/7-ff6799.svg)](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
@@ -1 +1,3 @@
1
- require "bundler/gem_tasks"
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
@@ -1,33 +1,57 @@
1
- require 'active_support/core_ext/array'
1
+ # frozen_string_literal: true
2
2
 
3
- require "numbers_in_words/version"
4
- require 'numbers_in_words/language_writer'
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 = 101 #length of the string i.e. one with 100 zeros
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
- attr_writer :language
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 language
21
- @language ||= "English"
31
+ def lookup(number)
32
+ exceptional_numbers.lookup(number)
22
33
  end
23
34
 
24
- def in_words(i, language=NumbersInWords.language, only_compress=false)
25
- NumbersInWords::ToWord.new(i, language).in_words(only_compress)
35
+ def exceptional_number(text)
36
+ exceptional_numbers_to_i[text]
26
37
  end
27
38
 
28
- def in_numbers(s, language=NumbersInWords.language, only_compress=false)
29
- NumbersInWords::ToNumber.new(s, language).in_numbers(only_compress)
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
- def in_words language=NumbersInWords.language
3
- NumbersInWords.in_words(self, language)
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 WordsInNumbers
8
- def in_numbers(only_compress = false, language=NumbersInWords.language)
9
- NumbersInWords::ToNumber.new(self, language).in_numbers(only_compress)
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 WordsInNumbers
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