numbers_in_words 0.3.0 → 1.0.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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +2 -0
  3. data/.gitignore +1 -0
  4. data/.rspec +1 -0
  5. data/.rubocop.yml +35 -1148
  6. data/.travis.yml +14 -4
  7. data/Gemfile +4 -1
  8. data/README.md +6 -43
  9. data/Rakefile +3 -1
  10. data/bin/spec +0 -0
  11. data/lib/numbers_in_words.rb +44 -19
  12. data/lib/numbers_in_words/duck_punch.rb +12 -8
  13. data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
  14. data/lib/numbers_in_words/fraction.rb +136 -0
  15. data/lib/numbers_in_words/number_group.rb +34 -25
  16. data/lib/numbers_in_words/parsing/fraction_parsing.rb +34 -0
  17. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  18. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  19. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  20. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  21. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  22. data/lib/numbers_in_words/parsing/special.rb +67 -0
  23. data/lib/numbers_in_words/parsing/to_number.rb +77 -0
  24. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  25. data/lib/numbers_in_words/to_word.rb +78 -13
  26. data/lib/numbers_in_words/version.rb +3 -1
  27. data/lib/numbers_in_words/writer.rb +69 -0
  28. data/numbers_in_words.gemspec +14 -13
  29. data/spec/exceptional_numbers_spec.rb +26 -0
  30. data/spec/fraction_spec.rb +152 -0
  31. data/spec/fractions_spec.rb +31 -0
  32. data/spec/non_monkey_patch_spec.rb +40 -15
  33. data/spec/number_group_spec.rb +12 -12
  34. data/spec/number_parser_spec.rb +31 -0
  35. data/spec/numbers_in_words_spec.rb +63 -70
  36. data/spec/numerical_strings_spec.rb +35 -0
  37. data/spec/spec_helper.rb +24 -4
  38. data/spec/to_word_spec.rb +18 -0
  39. data/spec/words_in_numbers_spec.rb +133 -116
  40. data/spec/writer_spec.rb +26 -0
  41. data/spec/years_spec.rb +27 -0
  42. metadata +49 -27
  43. data/lib/numbers_in_words/english/constants.rb +0 -93
  44. data/lib/numbers_in_words/english/language_writer_english.rb +0 -109
  45. data/lib/numbers_in_words/language_writer.rb +0 -31
  46. data/lib/numbers_in_words/number_parser.rb +0 -81
  47. data/lib/numbers_in_words/to_number.rb +0 -82
  48. data/spec/language_writer_spec.rb +0 -23
@@ -1,7 +1,17 @@
1
+ env:
2
+ global:
3
+ - CC_TEST_REPORTER_ID=9a548b48c1c0804d8fb9083ae8e9693a5d5197eb86e8cf1b63a89d18f33e0464
4
+ sudo: false
1
5
  language: ruby
2
6
  rvm:
3
- - "1.9.3"
4
- - "2.1.7"
5
- - "2.2.3"
6
- - rbx
7
+ - "2.4.10"
8
+ - "2.5.8"
9
+ - "2.6.6"
10
+ - "2.7.1"
11
+ before_script:
12
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
13
+ - chmod +x ./cc-test-reporter
14
+ - ./cc-test-reporter before-build
7
15
  script: ./bin/spec
16
+ after_script:
17
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
data/Gemfile CHANGED
@@ -1,5 +1,8 @@
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'
6
+ gem 'simplecov', '< 0.18.0', require: false, group: :test
4
7
  # Specify your gem's dependencies in numbers_in_words.gemspec
5
8
  gemspec
data/README.md CHANGED
@@ -1,23 +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)
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)
4
4
  [![Gem Version](http://img.shields.io/gem/v/numbers_in_words.svg)](https://rubygems.org/gems/numbers_in_words)
5
5
  [![License](http://img.shields.io/:license-mit-blue.svg)](http://markburns.mit-license.org)
6
- [![Badges](http://img.shields.io/:badges-6/6-ff6799.svg)](https://github.com/badges/badgerbadgerbadger)
7
6
 
8
7
  Installation
9
8
  ============
10
9
 
11
10
  ```ruby
12
11
  gem 'numbers_in_words'
13
-
14
- require 'numbers_in_words'
15
- require 'numbers_in_words/duck_punch' #see why later
16
12
  ```
17
13
 
18
- This project was created for a test for a job interview. I haven't really used
19
- it myself, but I saw it mentioned somewhere so I thought I'd tidy it up a bit.
20
-
21
14
  Usage
22
15
  =========
23
16
 
@@ -27,11 +20,14 @@ require 'numbers_in_words'
27
20
  NumbersInWords.in_words(112)
28
21
  #=> one hundred and twelve
29
22
 
30
- NumbersInWords.in_numbers(112)
23
+ NumbersInWords.in_numbers("one googol")
31
24
  #=>10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
32
25
 
33
26
  NumbersInWords.in_numbers("Seventy million, five-hundred and fifty six thousand point eight nine three")
34
27
  #=> 70556000.893
28
+
29
+ NumbersInWords.in_numbers("nineteen sixty five")
30
+ #=> 1965
35
31
  ```
36
32
 
37
33
 
@@ -49,36 +45,3 @@ require 'numbers_in_words/duck_punch'
49
45
  "Seventy million, five-hundred and fifty six thousand point eight nine three".in_numbers
50
46
  #=> 70556000.893
51
47
  ```
52
-
53
-
54
- NoMethodError `in_words` or `in_numbers`
55
- ----------
56
- I'm going to hopefully preempt some support queries by predicting this will happen:
57
-
58
- You've got one of:
59
-
60
- ```
61
- NoMethodError: undefined method `in_words' for 123:Fixnum
62
- NoMethodError: undefined method `in_numbers' for "123":String
63
- ```
64
-
65
- Previous versions of this gem duckpunched Fixnum and String with a whole bunch
66
- of methods. This gem will now only add methods if you specifically tell it to
67
- with:
68
-
69
- ```ruby
70
- require 'numbers_in_words'
71
- require 'numbers_in_words/duck_punch'
72
- ```
73
-
74
- Plus it now only adds a single `#in_words` method to `Numeric` and an `#in_numbers`
75
- method to `String` instead of a whole bunch of them.
76
-
77
-
78
- Future plans
79
- ============
80
-
81
- * Handle complex numbers
82
- * Option for outputting punctuation
83
- * Reject invalid numbers
84
- * 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'
data/bin/spec CHANGED
File without changes
@@ -1,33 +1,58 @@
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'
5
+ require 'numbers_in_words/parsing/number_parser'
12
6
  require 'numbers_in_words/to_word'
7
+ require 'numbers_in_words/parsing/to_number'
13
8
 
14
9
  module NumbersInWords
15
- LENGTH_OF_GOOGOL = 101 #length of the string i.e. one with 100 zeros
10
+ LENGTH_OF_GOOGOL = 101 # length of the string i.e. one with 100 zeros
11
+ Error = ::Class.new(::StandardError)
12
+ AmbiguousParsingError = ::Class.new(Error)
13
+ DivideByZeroError = ::Class.new(Error)
14
+ InvalidNumber = ::Class.new(Error)
16
15
 
17
16
  class << self
18
- attr_writer :language
17
+ extend Forwardable
18
+ def_delegators :exceptional_numbers, :fraction
19
+
20
+ def in_words(num, fraction: false)
21
+ ToWord.new(num).in_words(fraction: fraction)
22
+ end
23
+
24
+ def in_numbers(words, only_compress: false)
25
+ ToNumber.new(words, only_compress).call
26
+ end
27
+
28
+ def exceptional_numbers
29
+ @exceptional_numbers ||= ExceptionalNumbers.new
30
+ end
19
31
 
20
- def language
21
- @language ||= "English"
32
+ def lookup(number)
33
+ exceptional_numbers.lookup(number)
22
34
  end
23
35
 
24
- def in_words(i, language=NumbersInWords.language)
25
- NumbersInWords::ToWord.new(i, language).in_words
36
+ def exceptional_number(text)
37
+ exceptional_numbers_to_i[text]
26
38
  end
27
39
 
28
- def in_numbers(s, language=NumbersInWords.language)
29
- NumbersInWords::ToNumber.new(s, language).in_numbers
40
+ def power_of_ten(text)
41
+ powers_of_ten_to_i[text]
42
+ end
43
+
44
+ private
45
+
46
+ def exceptional_numbers_to_i
47
+ @exceptional_numbers_to_i ||= swap_keys exceptional_numbers.to_h
48
+ end
49
+
50
+ def powers_of_ten_to_i
51
+ @powers_of_ten_to_i ||= swap_keys POWERS_OF_TEN
52
+ end
53
+
54
+ def swap_keys(hash)
55
+ hash.each_with_object({}) { |(k, v), h| h[v] = k }
30
56
  end
31
57
  end
32
58
  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 language=NumbersInWords.language
9
- NumbersInWords.in_numbers(self, language)
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,115 @@
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 fraction(denominator: nil, numerator: nil, word: nil)
60
+ raise unless denominator || word
61
+
62
+ numerator ||= 1
63
+
64
+ denominator ||= NumbersInWords.in_numbers(word)
65
+
66
+ Fraction.new(denominator: denominator, numerator: numerator, attributes: DEFINITIONS[denominator])
67
+ end
68
+
69
+ def to_h
70
+ @to_h ||= DEFINITIONS.transform_values do |h|
71
+ h[:number]
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def generate_fraction_lookup
78
+ named_fractions.each_with_object({}) do |f, result|
79
+ f.lookup_keys.each do |k|
80
+ key = k.split(' ').last
81
+ result[key] = 1.0 / f.denominator.to_f
82
+ end
83
+ end
84
+ end
85
+
86
+ def named_fractions
87
+ @named_fractions ||= numbers.flat_map do |n|
88
+ [
89
+ Fraction.new(denominator: n, numerator: 1),
90
+ Fraction.new(denominator: n, numerator: 2)
91
+ ]
92
+ end
93
+ end
94
+
95
+ def numbers
96
+ (2..100).to_a + powers_of_ten_skipping_googolplex.map { |p| 10**p }
97
+ end
98
+
99
+ def powers_of_ten_skipping_googolplex
100
+ POWERS_OF_TEN.keys[0..-2]
101
+ end
102
+
103
+ def determine_fraction_names
104
+ names = named_fractions.map(&:in_words)
105
+
106
+ words = names.map(&:split).map(&:last)
107
+ words += strip_punctuation(words)
108
+ words.uniq
109
+ end
110
+
111
+ def strip_punctuation(words)
112
+ words.map { |w| w.gsub(/^a-z/, ' ') }
113
+ end
114
+ end
115
+ end
@@ -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