numbers_in_words 0.2.0 → 0.5.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 (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