numbers_in_words 0.1.1 → 0.5.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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +14 -0
  3. data/.gitignore +7 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +58 -0
  6. data/.travis.yml +15 -0
  7. data/Gemfile +8 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +47 -0
  10. data/Rakefile +2 -43
  11. data/bin/spec +2 -0
  12. data/lib/numbers_in_words.rb +55 -4
  13. data/lib/numbers_in_words/duck_punch.rb +23 -0
  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 +64 -0
  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 +84 -0
  27. data/lib/numbers_in_words/version.rb +5 -0
  28. data/lib/numbers_in_words/writer.rb +69 -0
  29. data/numbers_in_words.gemspec +20 -27
  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 +17 -0
  35. data/spec/number_parser_spec.rb +31 -0
  36. data/spec/numbers_in_words_spec.rb +69 -83
  37. data/spec/numerical_strings_spec.rb +35 -0
  38. data/spec/spec_helper.rb +26 -0
  39. data/spec/to_word_spec.rb +18 -0
  40. data/spec/words_in_numbers_spec.rb +137 -119
  41. data/spec/writer_spec.rb +26 -0
  42. data/spec/years_spec.rb +27 -0
  43. metadata +95 -45
  44. data/CHANGELOG +0 -1
  45. data/Manifest +0 -11
  46. data/README +0 -84
  47. data/examples/display_numbers_in_words.rb +0 -22
  48. data/init.rb +0 -8
  49. data/lib/numbers.rb +0 -260
  50. data/lib/words.rb +0 -221
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ class ParseStatus
5
+ attr_accessor :reset, :memory, :answer
6
+
7
+ def initialize
8
+ @reset = true
9
+ @memory = 0
10
+ @answer = 0
11
+ end
12
+
13
+ def calculate
14
+ answer + memory
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './fraction_parsing'
4
+
5
+ module NumbersInWords
6
+ class Special
7
+ extend Forwardable
8
+ def_delegator :that, :to_s
9
+
10
+ include FractionParsing
11
+
12
+ attr_reader :that, :only_compress
13
+
14
+ def initialize(that, only_compress)
15
+ @that = that
16
+ @only_compress = only_compress
17
+ end
18
+
19
+ def call
20
+ float ||
21
+ negative ||
22
+ fraction(that) ||
23
+ mixed_words_and_digits ||
24
+ one
25
+ end
26
+
27
+ def float
28
+ text_including_punctuation.to_f if text =~ /^-?\d+(.\d+)?$/
29
+ end
30
+
31
+ def negative
32
+ stripped = strip_minus text
33
+ return unless stripped
34
+
35
+ stripped_n = NumbersInWords.in_numbers(stripped, only_compress: only_compress)
36
+ only_compress ? stripped_n.map { |k| k * -1 } : -1 * stripped_n
37
+ end
38
+
39
+ def mixed_words_and_digits
40
+ return unless numeric?(that)
41
+
42
+ in_words = that.split(' ').map { |word| numeric?(word) ? NumbersInWords.in_words(word) : word }.join(' ')
43
+ ToNumber.new(in_words, only_compress).call
44
+ end
45
+
46
+ def numeric?(word)
47
+ word.match(/\d+/)
48
+ end
49
+
50
+ def strip_minus(txt)
51
+ txt.gsub(/^minus/, '') if txt =~ /^minus/
52
+ end
53
+
54
+ def one
55
+ one = check_one text
56
+
57
+ return unless one
58
+
59
+ res = NumbersInWords.in_numbers(one[1])
60
+ only_compress ? [res] : res
61
+ end
62
+
63
+ def check_one(txt)
64
+ txt.match(/^one (#{POWERS_RX})$/)
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './special'
4
+ require_relative './fraction_parsing'
5
+
6
+ module NumbersInWords
7
+ class ToNumber
8
+ include FractionParsing
9
+ extend Forwardable
10
+ def_delegator :that, :to_s
11
+
12
+ attr_reader :that, :only_compress
13
+
14
+ def initialize(that, only_compress)
15
+ @that = that
16
+ @only_compress = only_compress
17
+ end
18
+
19
+ def call
20
+ special || decimal || as_numbers
21
+ end
22
+
23
+ private
24
+
25
+ def special
26
+ Special.new(that, only_compress).call
27
+ end
28
+
29
+ def decimal
30
+ match = check_decimal text
31
+ return unless match
32
+
33
+ integer = NumbersInWords.in_numbers(match.pre_match)
34
+ decimal = NumbersInWords.in_numbers(match.post_match)
35
+ integer + "0.#{decimal}".to_f
36
+ end
37
+
38
+ def as_numbers
39
+ numbers = word_array_to_nums text.split(' ')
40
+
41
+ NumbersInWords::NumberParser.new.parse numbers, only_compress: only_compress
42
+ end
43
+
44
+ def word_array_to_nums(words)
45
+ words.map { |i| word_to_num(i) }.compact
46
+ end
47
+
48
+ # handles simple single word numbers
49
+ # e.g. one, seven, twenty, eight, thousand etc
50
+ def word_to_num(word)
51
+ text = canonize(word.to_s.chomp.strip)
52
+
53
+ NumbersInWords.exceptional_number(text) || fraction(text) || power(text)
54
+ end
55
+
56
+ def power(text)
57
+ power = NumbersInWords.power_of_ten(text)
58
+
59
+ 10**power if power
60
+ end
61
+
62
+ def canonize(word)
63
+ aliases[word] || word
64
+ end
65
+
66
+ def aliases
67
+ {
68
+ 'a' => 'one',
69
+ 'oh' => 'zero'
70
+ }
71
+ end
72
+
73
+ def check_decimal(txt)
74
+ txt.match(/\spoint\s/)
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ GOOGOL = 10**100
5
+
6
+ POWERS_OF_TEN = {
7
+ 0 => 'one',
8
+ 1 => 'ten',
9
+ 2 => 'hundred',
10
+ 1 * 3 => 'thousand',
11
+ 2 * 3 => 'million',
12
+ 3 * 3 => 'billion',
13
+ 4 * 3 => 'trillion',
14
+ 5 * 3 => 'quadrillion',
15
+ 6 * 3 => 'quintillion',
16
+ 7 * 3 => 'sextillion',
17
+ 8 * 3 => 'septillion',
18
+ 9 * 3 => 'octillion',
19
+ 10 * 3 => 'nonillion',
20
+ 11 * 3 => 'decillion',
21
+ 12 * 3 => 'undecillion',
22
+ 13 * 3 => 'duodecillion',
23
+ 14 * 3 => 'tredecillion',
24
+ 15 * 3 => 'quattuordecillion',
25
+ 16 * 3 => 'quindecillion',
26
+ 17 * 3 => 'sexdecillion',
27
+ 18 * 3 => 'septendecillion',
28
+ 19 * 3 => 'octodecillion',
29
+ 20 * 3 => 'novemdecillion',
30
+ 21 * 3 => 'vigintillion',
31
+ 22 * 3 => 'unvigintillion',
32
+ 23 * 3 => 'duovigintillion',
33
+ 24 * 3 => 'trevigintillion',
34
+ 25 * 3 => 'quattuorvigintillion',
35
+ 26 * 3 => 'quinvigintillion',
36
+ 27 * 3 => 'sexvigintillion',
37
+ 28 * 3 => 'septenvigintillion',
38
+ 29 * 3 => 'octovigintillion',
39
+ 30 * 3 => 'novemvigintillion',
40
+ 31 * 3 => 'trigintillion',
41
+ 32 * 3 => 'untrigintillion',
42
+ 33 * 3 => 'duotrigintillion',
43
+ 100 => 'googol',
44
+ 101 * 3 => 'centillion',
45
+ GOOGOL => 'googolplex'
46
+ }.freeze
47
+
48
+ POWERS_RX = Regexp.union(POWERS_OF_TEN.values[1..]).freeze
49
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'writer'
4
+ require_relative 'number_group'
5
+ require_relative 'fraction'
6
+
7
+ module NumbersInWords
8
+ # Arbitrarily small number for rationalizing fractions
9
+ EPSILON = 0.0000000001
10
+
11
+ class ToWord
12
+ attr_reader :that
13
+
14
+ def initialize(that)
15
+ @that = that
16
+ end
17
+
18
+ def to_i
19
+ that.to_i
20
+ end
21
+
22
+ def negative
23
+ return unless to_i.negative?
24
+
25
+ 'minus ' + NumbersInWords.in_words(-@that)
26
+ end
27
+
28
+ def in_words(fraction: false)
29
+ as_fraction(fraction) ||
30
+ handle_exceptional_numbers ||
31
+ decimals ||
32
+ negative ||
33
+ output
34
+ end
35
+
36
+ def as_fraction(fraction)
37
+ return Fraction.in_words(that) if fraction
38
+ end
39
+
40
+ def decimals
41
+ int, decimals = NumberGroup.new(@that).split_decimals
42
+ return unless int
43
+
44
+ out = NumbersInWords.in_words(int) + ' point '
45
+ decimals.each do |decimal|
46
+ out << NumbersInWords.in_words(decimal.to_i) + ' '
47
+ end
48
+ out.strip
49
+ end
50
+
51
+ def output
52
+ output = if to_i.to_s.length == 2 # 20-99
53
+ handle_tens(to_i)
54
+ else
55
+ Writer.new(that).call # longer numbers
56
+ end
57
+
58
+ output.strip
59
+ end
60
+
61
+ def handle_tens(number)
62
+ output = ''
63
+
64
+ tens = (number / 10).round * 10 # write the tens
65
+
66
+ output += NumbersInWords.lookup(tens) # e.g. eighty
67
+
68
+ digit = number - tens # write the digits
69
+
70
+ unless digit.zero?
71
+ join = number < 100 ? '-' : ' '
72
+ output << join + NumbersInWords.in_words(digit)
73
+ end
74
+
75
+ output
76
+ end
77
+
78
+ def handle_exceptional_numbers
79
+ return unless @that.is_a?(Integer)
80
+
81
+ NumbersInWords.exceptional_numbers.lookup(@that)
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ VERSION = '0.5.0'
5
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NumbersInWords
4
+ class Writer
5
+ def initialize(that)
6
+ @that = that
7
+ end
8
+
9
+ def call
10
+ length = @that.to_s.length
11
+ output =
12
+ if length == 3
13
+ # e.g. 113 splits into "one hundred" and "thirteen"
14
+ write_groups(2)
15
+
16
+ # more than one hundred less than one googol
17
+ elsif length < LENGTH_OF_GOOGOL
18
+ write_groups(3)
19
+
20
+ elsif length >= LENGTH_OF_GOOGOL
21
+ write_googols
22
+ end
23
+ output.strip
24
+ end
25
+
26
+ def group_words(size)
27
+ # 1000 and over Numbers are split into groups of three
28
+ groups = NumberGroup.groups_of @that, size
29
+ powers = groups.keys.sort.reverse # put in descending order
30
+
31
+ powers.each do |power|
32
+ name = NumbersInWords::POWERS_OF_TEN[power]
33
+ digits = groups[power]
34
+ yield power, name, digits
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def write_googols
41
+ googols, remainder = NumberGroup.new(@that).split_googols
42
+ output = ''
43
+
44
+ output = output + ' ' + NumbersInWords.in_words(googols) + ' googol'
45
+ if remainder.positive?
46
+ prefix = ' '
47
+ prefix += 'and ' if remainder < 100
48
+ output = output + prefix + NumbersInWords.in_words(remainder)
49
+ end
50
+
51
+ output
52
+ end
53
+
54
+ def write_groups(group)
55
+ # e.g. 113 splits into "one hundred" and "thirteen"
56
+ output = ''
57
+ group_words(group) do |power, name, digits|
58
+ if digits.positive?
59
+ prefix = ' '
60
+ # no and between thousands and hundreds
61
+ prefix += 'and ' if power.zero? && (digits < 100)
62
+ output = output + prefix + NumbersInWords.in_words(digits)
63
+ output = output + prefix + name unless power.zero?
64
+ end
65
+ end
66
+ output
67
+ end
68
+ end
69
+ end
@@ -1,32 +1,25 @@
1
- # -*- encoding: utf-8 -*-
1
+ # frozen_string_literal: true
2
2
 
3
- Gem::Specification.new do |s|
4
- s.name = "numbers_in_words"
5
- s.version = "0.1.1"
3
+ lib = File.expand_path('lib', __dir__)
4
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
+ require 'numbers_in_words/version'
6
6
 
7
- s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
- s.authors = ["Mark Burns"]
9
- s.date = "2012-04-10"
10
- s.description = "#in_words method for integers and #in_numbers for strings"
11
- s.email = "markthedeveloper@gmail.com"
12
- s.extra_rdoc_files = ["CHANGELOG", "README", "lib/numbers.rb", "lib/numbers_in_words.rb", "lib/words.rb"]
13
- s.files = ["CHANGELOG", "Manifest", "README", "Rakefile", "examples/display_numbers_in_words.rb", "init.rb", "lib/numbers.rb", "lib/numbers_in_words.rb", "lib/words.rb", "spec/numbers_in_words_spec.rb", "spec/words_in_numbers_spec.rb", "numbers_in_words.gemspec"]
14
- s.homepage = "http://rubygems.org/gems/numbers_in_words"
15
- s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Numbers_in_words", "--main", "README"]
16
- s.require_paths = ["lib"]
17
- s.rubyforge_project = "numbers_in_words"
18
- s.rubygems_version = "1.8.17"
19
- s.summary = "Example: 123.in_words #=> \"one hundred and twenty three\", \"seventy-five point eight\".in_numbers #=> 75.8"
7
+ Gem::Specification.new do |gem|
8
+ gem.name = 'numbers_in_words'
9
+ gem.description = 'convert written numbers into Integers and vice-versa'
10
+ gem.summary = 'Example: NumbersInWords.in_words(123) # => ' \
11
+ '"one hundred and twenty three", NumbersInWords.in_numbers("seventy-five point eight") # = > 75.8'
20
12
 
21
- if s.respond_to? :specification_version then
22
- s.specification_version = 3
13
+ gem.version = NumbersInWords::VERSION
14
+ gem.authors = ['Mark Burns', 'Dimid Duchovny']
15
+ gem.email = ['markthedeveloper@gmail.com', 'dimidd@gmail.com']
16
+ gem.homepage = 'http://github.com/markburns/numbers_in_words'
23
17
 
24
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
25
- s.add_runtime_dependency(%q<active_support>, [">= 0"])
26
- else
27
- s.add_dependency(%q<active_support>, [">= 0"])
28
- end
29
- else
30
- s.add_dependency(%q<active_support>, [">= 0"])
31
- end
18
+ gem.add_development_dependency 'rspec', '~> 3.4.0'
19
+ gem.add_development_dependency 'rubocop'
20
+
21
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
22
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
23
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
24
+ gem.require_paths = ['lib']
32
25
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './spec/spec_helper'
4
+
5
+ describe NumbersInWords::ExceptionalNumbers do
6
+ describe '#fraction' do
7
+ FRACTIONS = {
8
+ [1, 2] => 'one half',
9
+ [2, 2] => 'two halves',
10
+ [3, 2] => 'three halves',
11
+ [1, 3] => 'one third',
12
+ [1, 4] => 'one quarter',
13
+ [2, 17] => 'two seventeenths',
14
+ [1, 1_000] => 'one one thousandth',
15
+ [74, 101] => 'seventy-four hundred and firsts',
16
+ [13, 97] => 'thirteen ninety-sevenths',
17
+ [131, 1_000] => 'one hundred and thirty-one thousandths'
18
+ }.freeze
19
+
20
+ FRACTIONS.each do |(numerator, denominator), string|
21
+ it "#{numerator}/#{denominator} == #{string}" do
22
+ expect(subject.fraction(denominator: denominator, numerator: numerator).in_words).to eql(string)
23
+ end
24
+ end
25
+ end
26
+ end