numbers_in_words 0.2.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +14 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +58 -0
  6. data/.travis.yml +15 -0
  7. data/Gemfile +4 -0
  8. data/Gemfile.lock +50 -26
  9. data/README.md +20 -70
  10. data/Rakefile +3 -1
  11. data/bin/spec +2 -0
  12. data/lib/numbers_in_words.rb +49 -15
  13. data/lib/numbers_in_words/duck_punch.rb +12 -8
  14. data/lib/numbers_in_words/exceptional_numbers.rb +115 -0
  15. data/lib/numbers_in_words/fraction.rb +136 -0
  16. data/lib/numbers_in_words/number_group.rb +34 -25
  17. data/lib/numbers_in_words/parsing/fraction_parsing.rb +34 -0
  18. data/lib/numbers_in_words/parsing/number_parser.rb +98 -0
  19. data/lib/numbers_in_words/parsing/pair_parsing.rb +64 -0
  20. data/lib/numbers_in_words/parsing/parse_fractions.rb +45 -0
  21. data/lib/numbers_in_words/parsing/parse_individual_number.rb +68 -0
  22. data/lib/numbers_in_words/parsing/parse_status.rb +17 -0
  23. data/lib/numbers_in_words/parsing/special.rb +67 -0
  24. data/lib/numbers_in_words/parsing/to_number.rb +77 -0
  25. data/lib/numbers_in_words/powers_of_ten.rb +49 -0
  26. data/lib/numbers_in_words/to_word.rb +78 -13
  27. data/lib/numbers_in_words/version.rb +3 -1
  28. data/lib/numbers_in_words/writer.rb +69 -0
  29. data/numbers_in_words.gemspec +15 -14
  30. data/spec/exceptional_numbers_spec.rb +26 -0
  31. data/spec/fraction_spec.rb +152 -0
  32. data/spec/fractions_spec.rb +31 -0
  33. data/spec/non_monkey_patch_spec.rb +51 -0
  34. data/spec/number_group_spec.rb +12 -12
  35. data/spec/number_parser_spec.rb +31 -0
  36. data/spec/numbers_in_words_spec.rb +74 -54
  37. data/spec/numerical_strings_spec.rb +35 -0
  38. data/spec/spec_helper.rb +24 -0
  39. data/spec/to_word_spec.rb +18 -0
  40. data/spec/words_in_numbers_spec.rb +135 -117
  41. data/spec/writer_spec.rb +26 -0
  42. data/spec/years_spec.rb +27 -0
  43. metadata +61 -59
  44. data/lib/numbers_in_words/english/constants.rb +0 -93
  45. data/lib/numbers_in_words/english/language_writer_english.rb +0 -107
  46. data/lib/numbers_in_words/language_writer.rb +0 -31
  47. data/lib/numbers_in_words/number_parser.rb +0 -81
  48. data/lib/numbers_in_words/to_number.rb +0 -82
  49. data/spec/language_writer_spec.rb +0 -23
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 71344514fae412865a50ec69a9b58335ac9249aadf29bd5145a8455f503473b0
4
+ data.tar.gz: 128abd6b5546902d819c11b9105d573a564e6e1679c84bbeb8910bb6f28c03f8
5
+ SHA512:
6
+ metadata.gz: 2cb8730bc67b4be3a3aba9a63e4198c1a2e11e9664879d9f859fc4cf11b26743671abb97d180d411cd6a2dfaa6309afadf9f2d61a584c6ef2d5b05ecbbbfab16
7
+ data.tar.gz: 1c50db96276a5a9a9211bdb460c1f9e686fb35436a180d0aaf8de34961061cfd400421680a227558642b124d9be88eaa9f5a67ff3dfbbf65e4ba8ebfed5293d1
@@ -0,0 +1,14 @@
1
+ ---
2
+ engines:
3
+ fixme:
4
+ enabled: true
5
+ rubocop:
6
+ enabled: true
7
+ ratings:
8
+ paths:
9
+ - Gemfile.lock
10
+ - "**.rb"
11
+ exclude_paths:
12
+ - spec/**/*
13
+ - .codeclimate.yml
14
+ - .rubocop.yml
data/.gitignore CHANGED
@@ -3,3 +3,5 @@
3
3
  *.html
4
4
  tmp
5
5
  pkg/
6
+ Gemfile.lock
7
+ .byebug_history
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --require spec_helper
@@ -0,0 +1,58 @@
1
+ # The behavior of RuboCop can be controlled via the .rubocop.yml
2
+ # configuration file. It makes it possible to enable/disable
3
+ # certain cops (checks) and to alter their behavior if they accept
4
+ # any parameters. The file can be placed either in your home
5
+ # directory or in some project directory.
6
+ #
7
+ # RuboCop will start looking for the configuration file in the directory
8
+ # where the inspected file is and continue its way up to the root directory.
9
+ #
10
+ # See https://docs.rubocop.org/rubocop/configuration
11
+ Layout/EmptyLinesAroundAttributeAccessor:
12
+ Enabled: true
13
+ Layout/SpaceAroundMethodCallOperator:
14
+ Enabled: true
15
+ Lint/DeprecatedOpenSSLConstant:
16
+ Enabled: true
17
+ Lint/MixedRegexpCaptureTypes:
18
+ Enabled: true
19
+ Lint/RaiseException:
20
+ Enabled: true
21
+ Lint/StructNewOverride:
22
+ Enabled: true
23
+ Style/ExponentialNotation:
24
+ Enabled: true
25
+ Style/HashEachMethods:
26
+ Enabled: true
27
+ Style/HashTransformKeys:
28
+ Enabled: true
29
+ Style/HashTransformValues:
30
+ Enabled: true
31
+ Style/RedundantFetchBlock:
32
+ Enabled: true
33
+ Style/RedundantRegexpCharacterClass:
34
+ Enabled: true
35
+ Style/RedundantRegexpEscape:
36
+ Enabled: true
37
+ Style/SlicingWithRange:
38
+ Enabled: true
39
+ Lint/DuplicateElsifCondition:
40
+ Enabled: true
41
+ Style/AccessorGrouping:
42
+ Enabled: true
43
+ Style/ArrayCoercion:
44
+ Enabled: true
45
+ Style/BisectedAttrAccessor:
46
+ Enabled: true
47
+ Style/CaseLikeIf:
48
+ Enabled: true
49
+ Style/Documentation:
50
+ Enabled: false
51
+ Style/HashAsLastArrayItem:
52
+ Enabled: true
53
+ Style/HashLikeCase:
54
+ Enabled: true
55
+ Style/RedundantAssignment:
56
+ Enabled: true
57
+ Style/RedundantFileExtensionInRequire:
58
+ Enabled: true
@@ -0,0 +1,15 @@
1
+ env:
2
+ global:
3
+ - CC_TEST_REPORTER_ID=9a548b48c1c0804d8fb9083ae8e9693a5d5197eb86e8cf1b63a89d18f33e0464
4
+ sudo: false
5
+ language: ruby
6
+ rvm:
7
+ - "2.6.6"
8
+ - "2.7.1"
9
+ before_script:
10
+ - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
11
+ - chmod +x ./cc-test-reporter
12
+ - ./cc-test-reporter before-build
13
+ script: ./bin/spec
14
+ after_script:
15
+ - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
data/Gemfile CHANGED
@@ -1,4 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
5
+ gem 'byebug'
6
+ gem 'simplecov', '< 0.18.0', require: false, group: :test
3
7
  # Specify your gem's dependencies in numbers_in_words.gemspec
4
8
  gemspec
@@ -1,39 +1,63 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- numbers_in_words (0.2.0)
5
- activesupport
4
+ numbers_in_words (0.5.1)
6
5
 
7
6
  GEM
8
7
  remote: https://rubygems.org/
9
8
  specs:
10
- activesupport (3.2.10)
11
- i18n (~> 0.6)
12
- multi_json (~> 1.0)
13
- columnize (0.3.6)
14
- debugger (1.2.3)
15
- columnize (>= 0.3.1)
16
- debugger-linecache (~> 1.1.1)
17
- debugger-ruby_core_source (~> 1.1.5)
18
- debugger-linecache (1.1.2)
19
- debugger-ruby_core_source (>= 1.1.1)
20
- debugger-ruby_core_source (1.1.6)
21
- diff-lcs (1.1.3)
22
- i18n (0.6.1)
23
- multi_json (1.5.0)
24
- rspec (2.12.0)
25
- rspec-core (~> 2.12.0)
26
- rspec-expectations (~> 2.12.0)
27
- rspec-mocks (~> 2.12.0)
28
- rspec-core (2.12.2)
29
- rspec-expectations (2.12.1)
30
- diff-lcs (~> 1.1.3)
31
- rspec-mocks (2.12.1)
9
+ ast (2.4.1)
10
+ byebug (11.1.3)
11
+ diff-lcs (1.4.4)
12
+ docile (1.3.2)
13
+ json (2.3.1)
14
+ parallel (1.19.2)
15
+ parser (2.7.1.4)
16
+ ast (~> 2.4.1)
17
+ rainbow (3.0.0)
18
+ regexp_parser (1.7.1)
19
+ rexml (3.2.4)
20
+ rspec (3.4.0)
21
+ rspec-core (~> 3.4.0)
22
+ rspec-expectations (~> 3.4.0)
23
+ rspec-mocks (~> 3.4.0)
24
+ rspec-core (3.4.4)
25
+ rspec-support (~> 3.4.0)
26
+ rspec-expectations (3.4.0)
27
+ diff-lcs (>= 1.2.0, < 2.0)
28
+ rspec-support (~> 3.4.0)
29
+ rspec-mocks (3.4.1)
30
+ diff-lcs (>= 1.2.0, < 2.0)
31
+ rspec-support (~> 3.4.0)
32
+ rspec-support (3.4.1)
33
+ rubocop (0.88.0)
34
+ parallel (~> 1.10)
35
+ parser (>= 2.7.1.1)
36
+ rainbow (>= 2.2.2, < 4.0)
37
+ regexp_parser (>= 1.7)
38
+ rexml
39
+ rubocop-ast (>= 0.1.0, < 1.0)
40
+ ruby-progressbar (~> 1.7)
41
+ unicode-display_width (>= 1.4.0, < 2.0)
42
+ rubocop-ast (0.1.0)
43
+ parser (>= 2.7.0.1)
44
+ ruby-progressbar (1.10.1)
45
+ simplecov (0.17.1)
46
+ docile (~> 1.1)
47
+ json (>= 1.8, < 3)
48
+ simplecov-html (~> 0.10.0)
49
+ simplecov-html (0.10.2)
50
+ unicode-display_width (1.7.0)
32
51
 
33
52
  PLATFORMS
34
53
  ruby
35
54
 
36
55
  DEPENDENCIES
37
- debugger
56
+ byebug
38
57
  numbers_in_words!
39
- rspec
58
+ rspec (~> 3.4.0)
59
+ rubocop
60
+ simplecov (< 0.18.0)
61
+
62
+ BUNDLED WITH
63
+ 2.1.4
data/README.md CHANGED
@@ -1,42 +1,37 @@
1
+ [![Build Status](http://img.shields.io/travis/markburns/numbers_in_words.svg)](https://travis-ci.org/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
+ [![Gem Version](http://img.shields.io/gem/v/numbers_in_words.svg)](https://rubygems.org/gems/numbers_in_words)
5
+ [![License](http://img.shields.io/:license-mit-blue.svg)](http://markburns.mit-license.org)
6
+
1
7
  Installation
2
8
  ============
3
9
 
4
- ```
10
+ ```ruby
5
11
  gem 'numbers_in_words'
6
-
7
- require 'numbers_in_words'
8
- require 'numbers_in_words/duck_punch' #see why later
9
12
  ```
10
13
 
11
- This project was created for a test for a job interview. I haven't really used
12
- it myself, but I saw it mentioned somewhere so I thought I'd tidy it up a bit.
13
-
14
- I'm going to hopefully preempt some support queries by predicting this will happen:
14
+ Usage
15
+ =========
15
16
 
16
- You've got one of:
17
+ ```ruby
18
+ require 'numbers_in_words'
17
19
 
18
- ```
19
- NoMethodError: undefined method `in_words' for 123:Fixnum
20
- NoMethodError: undefined method `in_numbers' for "123":String
21
- ```
20
+ NumbersInWords.in_words(112)
21
+ #=> one hundred and twelve
22
22
 
23
- Here's why
24
- ==========
23
+ NumbersInWords.in_numbers("one googol")
24
+ #=>10000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
25
25
 
26
- Previous versions of this gem duckpunched Fixnum and String with a whole bunch
27
- of methods. This gem will now only add methods if you specifically tell it to
28
- with:
26
+ NumbersInWords.in_numbers("Seventy million, five-hundred and fifty six thousand point eight nine three")
27
+ #=> 70556000.893
29
28
 
30
- ```
31
- require 'numbers_in_words'
32
- require 'numbers_in_words/duck_punch'
29
+ NumbersInWords.in_numbers("nineteen sixty five")
30
+ #=> 1965
33
31
  ```
34
32
 
35
- Plus it now only adds a single `#in_words` method to `Numeric` and an `#in_numbers`
36
- method to `String` instead of a whole bunch of them.
37
33
 
38
- Usage
39
- =========
34
+ Monkey patch version
40
35
 
41
36
  ```ruby
42
37
  require 'numbers_in_words'
@@ -50,48 +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
-
56
- Whilst creating this project I realized that in English:
57
-
58
- * Numbers are grouped in groups of threes
59
- * Numbers less than 1,000 are grouped by hundreds and then by tens
60
- * There are specific rules for when we put an "and" in between numbers
61
-
62
- It makes sense to manage the numbers by these groups, so
63
- I created a method groups_of which will split any integer into
64
- groups of a certain size. It returns a hash with the power of ten
65
- as the key and the multiplier as the value. E.g:
66
-
67
- ```ruby
68
- 31245.groups_of(3)
69
- #=> {0=>245,3=>31} #i.e. 31 thousands, and 245 ones
70
-
71
- 245.group_of(2)
72
- #=> {0=>45,2=>2} #i.e. 2 hundreds, and 45 ones
73
- ```
74
-
75
- (In Japanese numbers are grouped in groups of 4, so it makes sense to try and
76
- separate the language related stuff from the number grouping related stuff)
77
-
78
- Example of usage:
79
-
80
- ```ruby
81
- 245.group_words(2,"English") do |power, name, digits|
82
- puts "#{digits}*10^#{power} #{digits} #{name}s"
83
- end
84
-
85
-
86
- 2 * 10^2 = 2 hundreds
87
- 45 * 10^0 = 45 ones
88
-
89
- ```
90
-
91
- Future plans
92
- ============
93
-
94
- * Handle complex numbers
95
- * Option for outputting punctuation
96
- * Reject invalid numbers
97
- * 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'
@@ -0,0 +1,2 @@
1
+ bundle exec rspec spec/non_monkey_patch_spec.rb
2
+ bundle exec rspec spec
@@ -1,24 +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
19
- def language
20
- @language ||= "English"
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
31
+
32
+ def lookup(number)
33
+ exceptional_numbers.lookup(number)
34
+ end
35
+
36
+ def exceptional_number(text)
37
+ exceptional_numbers_to_i[text]
38
+ end
39
+
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 }
21
56
  end
22
57
  end
23
58
  end
24
-
@@ -1,19 +1,23 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module NumbersInWords
2
- def in_words language=NumbersInWords.language
3
- NumbersInWords::ToWord.new(self, language).in_words
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::ToNumber.new(self, language).in_numbers
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