strings-numeral 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0c3b788a344901a4da93755096f0c7657e6a28218ad6dc419eafcbc61be0aa98
4
+ data.tar.gz: 220f0401d2d1c6c4d1f39c01509a9265e142ab418f80a5e6339134c60d2e46ca
5
+ SHA512:
6
+ metadata.gz: 959a8ccf4432fafb017afe51c8b27fddacd1d4ec8cbe672067ffe74dc15f22ec0c51955403382e8640a2826923044fba78ab894f385f8bb134afda9869934fe2
7
+ data.tar.gz: 263c41b90e351ffbcb5acc7905fecfff6a63c42d8dbd1b8f8ceaad0854291b7bec76fef55423443b2d54a6f78f7fc3ccd2973965a8ac1993b2b3095d893b928a
@@ -0,0 +1,7 @@
1
+ # Change log
2
+
3
+ ## [v0.1.0] - 2019-12-18
4
+
5
+ * Initial implementation and release
6
+
7
+ [v0.1.0]: https://github.com/piotrmurach/strings-numeral/compare/v0.1.0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Piotr Murach
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,327 @@
1
+ <div align="center">
2
+ <img width="225" src="https://github.com/piotrmurach/strings/blob/master/assets/strings_logo.png" alt="strings logo" />
3
+ </div>
4
+
5
+ # Strings::Numeral
6
+
7
+ [![Gem Version](https://badge.fury.io/rb/strings-numeral.svg)][gem]
8
+ [![Build Status](https://secure.travis-ci.org/piotrmurach/strings-numeral.svg?branch=master)][travis]
9
+ [![Build status](https://ci.appveyor.com/api/projects/status/494htkcankqegwtg?svg=true)][appveyor]
10
+ [![Maintainability](https://api.codeclimate.com/v1/badges/de0c5ad1cba6715b7135/maintainability)][codeclimate]
11
+ [![Coverage Status](https://coveralls.io/repos/github/piotrmurach/strings-numeral/badge.svg?branch=master)][coverage]
12
+ [![Inline docs](http://inch-ci.org/github/piotrmurach/strings-numeral.svg?branch=master)][inchpages]
13
+
14
+ [gem]: http://badge.fury.io/rb/strings-numeral
15
+ [travis]: http://travis-ci.org/piotrmurach/strings-numeral
16
+ [appveyor]: https://ci.appveyor.com/project/piotrmurach/strings-numeral
17
+ [codeclimate]: https://codeclimate.com/github/piotrmurach/strings-numeral/maintainability
18
+ [coverage]: https://coveralls.io/github/piotrmurach/strings-numeral?branch=master
19
+ [inchpages]: http://inch-ci.org/github/piotrmurach/strings-numeral
20
+
21
+ > Express numbers as string numerals.
22
+
23
+ **Strings::Numeral** provides conversions of numbers to numerals component for [Strings](https://github.com/piotrmurach/strings).
24
+
25
+ ## Features
26
+
27
+ * No monkey-patching String class
28
+ * Functional API that can be easily wrapped by other objects
29
+ * Instance based configuration
30
+ * Highly performant
31
+
32
+ ## Installation
33
+
34
+ Add this line to your application's Gemfile:
35
+
36
+ ```ruby
37
+ gem 'strings-numeral'
38
+ ```
39
+
40
+ And then execute:
41
+
42
+ $ bundle
43
+
44
+ Or install it yourself as:
45
+
46
+ $ gem install strings-numeral
47
+
48
+ ## Contents
49
+
50
+ * [1. Usage](#1-usage)
51
+ * [2. API](#2-api)
52
+ * [2.1 numeralize](#21-numeralize)
53
+ * [2.2 cardinalize](#22-cardinalize)
54
+ * [2.3 ordinalize](#23-ordinalize)
55
+ * [2.4 monetize](#24-monetize)
56
+ * [2.5 romanize](#25-romanize)
57
+ * [2.6 configuration](#26-configuration)
58
+ * [3. Extending core classes](#3-extending-core-classes)
59
+
60
+ ## 1. Usage
61
+
62
+ **Strings::Numeral** helps to express any number as a numeral in words. It exposes few methods to achieve this. For example, you can express a number as a cardinal numeral using `cardinalize`:
63
+
64
+ ```ruby
65
+ Strings::Numeral.cardinalize(1234)
66
+ # => "one thousand, two hundred thirty four"
67
+ ```
68
+
69
+ But you're not limited to converting integers only. It can handle decimals as well:
70
+
71
+ ```ruby
72
+ Strings::Numeral.cardinalize(1234.567)
73
+ # => "one thousand, two hundred thirty four and five hundred sixty seven thousandths"
74
+ ```
75
+
76
+ For more options on how to customize formatting see [configuration](#25-configuration) section.
77
+
78
+ Similarly, you can convert a number to a ordinal numeral with `ordinalize`:
79
+
80
+ ```ruby
81
+ Strings::Numeral.ordinalize(1234)
82
+ # => "one thousand, two hundred thirty fourth"
83
+ ```
84
+
85
+ You can also convert a number to a short ordinal:
86
+
87
+ ```ruby
88
+ Strings::Numeral.ordinalize(1234, short: true)
89
+ # => "1234th"
90
+ ```
91
+
92
+ Using `monetize` you can convert any number into a monetary numeral:
93
+
94
+ ```ruby
95
+ Strings::Numeral.monetize(1234.567)
96
+ # => "one thousand, two hundred thirty four dollars and fifty seven cents",
97
+ ```
98
+
99
+ To turn a number into a roman numeral use `romanize`:
100
+
101
+ ```ruby
102
+ Strings::Numeral.romanize(2020)
103
+ # => "MMXX"
104
+ ```
105
+
106
+ ## 2. API
107
+
108
+ ### 2.1 numeralize
109
+
110
+ The `normalize` is a wrapping method for the [cardinalize](#22-cardinalize) and [ordinalize](#23-ordinalize) methods. By default it converts a number to cardinal numeral:
111
+
112
+ ```ruby
113
+ Strings::Numeral.numeralize(1234.567)
114
+ # => "one thousand, two hundred thirty four and five hundred sixty seven thousandths"
115
+ ```
116
+
117
+ You can also make it convert to ordinal numerals using `:term` option:
118
+
119
+ ```ruby
120
+ Strings::Numeral.numeralize(1234.567, term: :ord)
121
+ # => "one thousand, two hundred thirty fourth and five hundred sixty seven thousandths"
122
+ ```
123
+
124
+ ### 2.2 cardinalize
125
+
126
+ To express a number as a cardinal numeral use `cardinalize` or `cardinalise`.
127
+
128
+ ```ruby
129
+ Strings::Numeral.cardinalize(1234)
130
+ # => "one thousand, two hundred thirty four"
131
+ ```
132
+
133
+ You're not limited to integers only. You can also express decimal numbers as well:
134
+
135
+ ```ruby
136
+ Strings::Numeral.cardinalize(123.456)
137
+ # => "one hundred twenty three and four hundred fifty six thousandths"
138
+ ```
139
+
140
+ By default the fractional part of a decimal number is expressed as a fraction. If you wish to spell out fractional part digit by digit use `:decimal` option with `:digit` value:
141
+
142
+ ```ruby
143
+ Strings::Numeral.cardinalize(123.456, decimal: :digit)
144
+ # => "one hundred twenty three point four five six"
145
+ ```
146
+
147
+ You may prefer to use a different delimiter for thousand's. You can do use by passing the `:delimiter` option:
148
+
149
+ ```ruby
150
+ Strings::Numeral.cardinalize(1_234_567, delimiter: " and ")
151
+ # => "one million and two hundred thirty four thousand and five hundred sixty seven"
152
+ ```
153
+
154
+ To change word that splits integer from factional part use `:separator` option:
155
+
156
+ ```ruby
157
+ Strings::Numeral.cardinalize(1_234.567, separator: "dot")
158
+ # => "one thousand, two hundred thirty four dot five hundred sixty seven thousandths"
159
+ ```
160
+
161
+ ### 2.3 ordinalize
162
+
163
+ To express a number as a cardinal numeral use `ordinalize` or `ordinalise`.
164
+
165
+ ```ruby
166
+ Strings::Numeral.ordinalize(1234)
167
+ # => "one thousand, two hundred thirty fourth"
168
+ ```
169
+
170
+ You're not limited to integers only. You can also express decimal numbers as well:
171
+
172
+ ```ruby
173
+ Strings::Numeral.ordinalize(123.456)
174
+ # => "one hundred twenty third and four hundred fifty six thousandths"
175
+ ```
176
+
177
+ By default the fractional part of a decimal number is expressed as a fraction. If you wish to spell out fractional part digit by digit use `:decimal` option with `:digit` value:
178
+
179
+ ```ruby
180
+ Strings::Numeral.ordinalize(123.456, decimal: :digit)
181
+ # => "one hundred twenty third point four five six"
182
+ ```
183
+
184
+ You may prefer to use a different delimiter for thousand's. You can do use by passing the `:delimiter` option:
185
+
186
+ ```ruby
187
+ Strings::Numeral.ordinalize(1_234_567, delimiter: " and ")
188
+ # => "one million and two hundred thirty four thousand and five hundred sixty seventh"
189
+ ```
190
+
191
+ To change word that splits integer from factional part use `:separator` option:
192
+
193
+ ```ruby
194
+ Strings::Numeral.ordinalize(1_234.567, separator: "dot")
195
+ # => "one thousand, two hundred thirty fourth dot five hundred sixty seven thousandths"
196
+ ```
197
+
198
+ ### 2.4 monetize
199
+
200
+ To express a number as a monetary numeral use `monetize` or `monetise`.
201
+
202
+ ```ruby
203
+ Strings::Numeral.monetize(123.456)
204
+ # => "one hundred twenty three dollars and forty six cents",
205
+ ```
206
+
207
+ By default `monetize` displays money using `USD` currency. You can change this with the `:currency` option that as value accepts internationally recognised symbols. Currently support currencies are: `EUR`, `GBP`, `JPY`, `PLN` and `USD`.
208
+
209
+ ```ruby
210
+ Strings::Numeral.monetize(123.456, currency: :jpy)
211
+ # => "one hundred twenty three yen and forty six sen"
212
+ ```
213
+
214
+ ### 2.5 romanize
215
+
216
+ To convert a number into a Roman numeral use `romanize`:
217
+
218
+ ```ruby
219
+ Strings::Numeral.romanize(2020)
220
+ # => "MMXX"
221
+ ```
222
+
223
+ ### 2.6 configuration
224
+
225
+ All available configuration options are:
226
+
227
+ * `currency` - Adds currency words for integer and fractional parts. Supports `EUR`, `GBP`, `JPY`, `PLN` and `USD`. Defaults to `USD`.
228
+ * `decimal` - Formats fractional part of a number. The `:digit` value spells out every digit and the `:fraction` appends divider word. Defaults to `:fraction`.
229
+ * `delimiter` - Sets the thousands delimiter. Defaults to `", "`.
230
+ * `separator` - Sets the separator between the fractional and integer parts. Defaults to `"and"` for `:fraction` and `"point"` for `:digit` option.
231
+ * `trailing_zeros` - If `true` keeps trailing zeros at the end of the fractional part. Defaults to `false`.
232
+
233
+ The above options can be passed as keyword arguments:
234
+
235
+ ```ruby
236
+ Strings::Numeral.cardinalize("12.100", trailing_zeros: true, decimal: :digit)
237
+ # => "twelve point one zero zero"
238
+ ```
239
+
240
+ Or you can configure the options for an instance:
241
+
242
+ ```ruby
243
+ numeral = Strings::Numeral.new do |config|
244
+ config.delimiter "; "
245
+ config.separator "dot"
246
+ config.decimal :digit
247
+ config.trailing_zeros true
248
+ end
249
+ ```
250
+
251
+ Once configured, you can use the instance like so:
252
+
253
+ ```ruby
254
+ numeral.cardinalize("1234.56700")
255
+ # => "one thousand; two hundred thirty four dot five six seven zero zero"
256
+ ```
257
+
258
+ ## 3. Extending Core Classes
259
+
260
+ Though it is highly discouraged to pollute core Ruby classes, you can add the required methods to `String`, `Float` and `Integer` classes using refinements.
261
+
262
+ For example, if you wish to only extend `Float` class with `cardinalize` method do:
263
+
264
+ ```ruby
265
+ module MyFloatExt
266
+ refine Float do
267
+ def cardinalize(**options)
268
+ Strings::Numeral.cardinalize(self, **options)
269
+ end
270
+ end
271
+ end
272
+ ```
273
+
274
+ Then `cardinalize` method will be available for any float number where refinement is applied:
275
+
276
+ ```ruby
277
+ using MyFloatExt
278
+
279
+ 12.34.cardinalize
280
+ # => "twelve and thirty four"
281
+ ```
282
+
283
+ However, if you want to include all the **Strings::Numeral** methods in `Float`, `Integer` and `String` classes, you can use provided extensions file:
284
+
285
+
286
+ ```ruby
287
+ require "strings/numeral/extensions"
288
+
289
+ using Strings::Numeral::Extensions
290
+ ```
291
+
292
+ Alternatively, you can choose what class you wish to refine with all the methods:
293
+
294
+ ```ruby
295
+ require "bigdecimal"
296
+ require "strings/numeral/extensions"
297
+
298
+ module MyBigDecimalExt
299
+ refine BigDecimal do
300
+ include Strings::Numeral::Extensions::Methods
301
+ end
302
+ end
303
+
304
+ using MyBigDecimalExt
305
+ ```
306
+
307
+ ## Development
308
+
309
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
310
+
311
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
312
+
313
+ ## Contributing
314
+
315
+ Bug reports and pull requests are welcome on GitHub at https://github.com/piotrmurach/strings-numeral. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
316
+
317
+ ## License
318
+
319
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
320
+
321
+ ## Code of Conduct
322
+
323
+ Everyone interacting in the Strings::Numeral project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotrmurach/strings-numeral/blob/master/CODE_OF_CONDUCT.md).
324
+
325
+ ## Copyright
326
+
327
+ Copyright (c) 2019 Piotr Murach. See LICENSE for further details.
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ FileList["tasks/**/*.rake"].each(&method(:import))
4
+
5
+ desc "Run all specs"
6
+ task ci: %w[ spec ]
7
+
8
+ task default: :spec
@@ -0,0 +1 @@
1
+ require "strings/numeral"
@@ -0,0 +1,557 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "numeral/configuration"
4
+ require_relative "numeral/version"
5
+
6
+ module Strings
7
+ class Numeral
8
+ class Error < StandardError; end
9
+
10
+ NEGATIVE = "negative"
11
+ HUNDRED = "hundred"
12
+ ZERO = "zero"
13
+ AND = "and"
14
+ POINT = "point"
15
+ SPACE = " "
16
+
17
+ CARDINALS = {
18
+ 0 => "",
19
+ 1 => "one",
20
+ 2 => "two",
21
+ 3 => "three",
22
+ 4 => "four",
23
+ 5 => "five",
24
+ 6 => "six",
25
+ 7 => "seven",
26
+ 8 => "eight",
27
+ 9 => "nine",
28
+ 10 => "ten",
29
+ 11 => "eleven",
30
+ 12 => "twelve",
31
+ 13 => "thirteen",
32
+ 14 => "fourteen",
33
+ 15 => "fifteen",
34
+ 16 => "sixteen",
35
+ 17 => "seventeen",
36
+ 18 => "eighteen",
37
+ 19 => "nineteen",
38
+ 20 => "twenty",
39
+ 30 => "thirty",
40
+ 40 => "forty",
41
+ 50 => "fifty",
42
+ 60 => "sixty",
43
+ 70 => "seventy",
44
+ 80 => "eighty",
45
+ 90 => "ninety",
46
+ }.freeze
47
+
48
+ CARDINAL_TO_SHORT_ORDINAL = {
49
+ 0 => "th",
50
+ 1 => "st",
51
+ 11 => "th",
52
+ 2 => "nd",
53
+ 12 => "th",
54
+ 3 => "rd",
55
+ 13 => "th",
56
+ 4 => "th",
57
+ 5 => "th",
58
+ 6 => "th",
59
+ 7 => "th",
60
+ 8 => "th",
61
+ 9 => "th"
62
+ }.freeze
63
+
64
+ CARDINAL_TO_ORDINAL = {
65
+ "zero" => "zeroth",
66
+ "one" => "first",
67
+ "two" => "second",
68
+ "three" => "third",
69
+ "four" => "fourth",
70
+ "five" => "fifth",
71
+ "six" => "sixth",
72
+ "seven" => "seventh",
73
+ "eight" => "eighth",
74
+ "nine" => "ninth",
75
+ "ten" => "tenth",
76
+ "eleven" => "eleventh",
77
+ "twelve" => "twelfth",
78
+ "thirteen" => "thirteenth",
79
+ "fourteen" => "fourteenth",
80
+ "fifteen" => "fifteenth",
81
+ "sixteen" => "sixteenth",
82
+ "seventeen" => "seventeenth",
83
+ "eighteen" => "eighteenth",
84
+ "nineteen" => "nineteenth",
85
+ "twenty" => "twentieth",
86
+ "thirty" => "thirtieth",
87
+ "forty" => "fortieth",
88
+ "fifty" => "fiftieth",
89
+ "sixty" => "sixtieth",
90
+ "seventy" => "seventieth",
91
+ "eighty" => "eightieth",
92
+ "ninety" => "ninetieth"
93
+ }.freeze
94
+
95
+ CARDINAL_TO_ROMAN = {
96
+ 1 => "I",
97
+ 4 => "IV",
98
+ 5 => "V",
99
+ 9 => "IX",
100
+ 10 => "X",
101
+ 40 => "XL",
102
+ 50 => "L",
103
+ 90 => "XC",
104
+ 100 => "C",
105
+ 400 => "CD",
106
+ 500 => "D",
107
+ 900 => "CM",
108
+ 1000 => "M"
109
+ }.freeze
110
+
111
+ SCALES = [
112
+ "hundreds-tens-ones",
113
+ "thousand",
114
+ "million",
115
+ "billion",
116
+ "trillion",
117
+ "quadrillion",
118
+ "quintillion",
119
+ "sextillion",
120
+ "septillion",
121
+ "octillion",
122
+ "nonillion",
123
+ "decillion",
124
+ "undecillion",
125
+ "duodecillion",
126
+ "tredecillion",
127
+ "quattuordecillion",
128
+ "quindecillion",
129
+ "sexdecillion",
130
+ "septemdecillion",
131
+ "octodecillion",
132
+ "novemdecillion",
133
+ "vigintillion"
134
+ ].freeze
135
+
136
+ DECIMAL_SLOTS = [
137
+ "tenths",
138
+ "hundredths",
139
+ "thousandths",
140
+ "ten-thousandths",
141
+ "hundred-thousandths",
142
+ "millionths",
143
+ "ten-millionths",
144
+ "hundred-millionths",
145
+ "billionths",
146
+ "ten-billionths",
147
+ "hundred-billionths",
148
+ "trillionths",
149
+ "quadrillionths",
150
+ "quintillionths",
151
+ "sextillionths",
152
+ "septillionths",
153
+ "octillionths",
154
+ "nonillionths",
155
+ "decillionths",
156
+ "undecillionths",
157
+ "duodecillionths",
158
+ "tredecillionths",
159
+ "quattuordecillionths",
160
+ "quindecillionths",
161
+ "sexdecillionths",
162
+ "septemdecillionths",
163
+ "octodecillionths",
164
+ "novemdecillionths",
165
+ "vigintillionths"
166
+ ].freeze
167
+
168
+ CURRENCIES = {
169
+ eur: {
170
+ unit: "euro",
171
+ units: "euros",
172
+ decimal_unit: "cent",
173
+ decimal_units: "cents"
174
+ },
175
+ gbp: {
176
+ unit: "pound",
177
+ units: "pounds",
178
+ decimal_unit: "pence",
179
+ decimal_units: "pence",
180
+ },
181
+ jpy: {
182
+ unit: "yen",
183
+ units: "yen",
184
+ decimal_unit: "sen",
185
+ decimal_units: "sen",
186
+ },
187
+ pln: {
188
+ unit: "zloty",
189
+ units: "zlotys",
190
+ decimal_unit: "grosz",
191
+ decimal_units: "groszy"
192
+ },
193
+ usd: {
194
+ unit: "dollar",
195
+ units: "dollars",
196
+ decimal_unit: "cent",
197
+ decimal_units: "cents"
198
+ }
199
+ }.freeze
200
+
201
+ def self.instance
202
+ @instance ||= Numeral.new
203
+ end
204
+
205
+ class << self
206
+ def numeralize(num, **options)
207
+ instance.numeralize(num, **options)
208
+ end
209
+ alias :numeralise :numeralize
210
+
211
+ def cardinalize(num, **options)
212
+ instance.cardinalize(num, **options)
213
+ end
214
+ alias :cardinalise :cardinalize
215
+
216
+ def ordinalize(num, **options)
217
+ instance.ordinalize(num, **options)
218
+ end
219
+ alias :ordinalise :ordinalize
220
+
221
+ def ordinalize_short(num)
222
+ instance.ordinalize_short(num)
223
+ end
224
+
225
+ def monetize(num, **options)
226
+ instance.monetize(num, **options)
227
+ end
228
+ alias :monetise :monetize
229
+
230
+ def romanize(num)
231
+ instance.romanize(num)
232
+ end
233
+ alias :romanise :romanize
234
+ end
235
+
236
+ # Create numeral with custom configuration
237
+ #
238
+ # @yieldparam [Configuration]
239
+ #
240
+ # @return [Numeral]
241
+ #
242
+ # @api public
243
+ def initialize
244
+ @configuration = Configuration.new
245
+ if block_given?
246
+ yield @configuration
247
+ end
248
+ end
249
+
250
+ # Convert a number to a numeral
251
+ #
252
+ # @param [Numeric,String] num
253
+ # the number to convert
254
+ #
255
+ # @api public
256
+ def numeralize(num, **options)
257
+ case options.delete(:term)
258
+ when /ord/
259
+ ordinalize(num, **options)
260
+ else
261
+ cardinalize(num, **options)
262
+ end
263
+ end
264
+
265
+ # Convert a number to a cardinal numeral
266
+ #
267
+ # @example
268
+ # cardinalize(1234)
269
+ # # => one thousand, two hundred thirty four
270
+ #
271
+ # @param [Numeric,String] num
272
+ #
273
+ # @return [String]
274
+ #
275
+ # @api public
276
+ def cardinalize(num, **options)
277
+ convert_numeral(num, **options)
278
+ end
279
+ alias :cardinalise :cardinalize
280
+
281
+ # Convert a number to an ordinal numeral
282
+ #
283
+ # @example
284
+ # ordinalize(1234)
285
+ # # => one thousand, two hundred thirty fourth
286
+ #
287
+ # ordinalize(12, short: true) # => 12th
288
+ #
289
+ # @param [Numeric,String] num
290
+ # the number to convert
291
+ #
292
+ # @return [String]
293
+ #
294
+ # @api public
295
+ def ordinalize(num, **options)
296
+ if options[:short]
297
+ ordinalize_short(num)
298
+ else
299
+ decimals = (num.to_i.abs != num.to_f.abs)
300
+ sentence = convert_numeral(num, **options)
301
+ separators = [AND, POINT,
302
+ options.fetch(:separator, @configuration.separator)].compact
303
+
304
+ if decimals && sentence =~ /(\w+) (#{Regexp.union(separators)})/
305
+ last_digits = $1
306
+ separator = $2
307
+ replacement = CARDINAL_TO_ORDINAL[last_digits]
308
+ pattern = /#{last_digits} #{separator}/
309
+ suffix = "#{replacement} #{separator}"
310
+ elsif sentence =~ /(\w+)$/
311
+ last_digits = $1
312
+ replacement = CARDINAL_TO_ORDINAL[last_digits]
313
+ pattern = /#{last_digits}$/
314
+ suffix = replacement
315
+ end
316
+
317
+ if replacement
318
+ sentence.sub(pattern, suffix)
319
+ else
320
+ sentence
321
+ end
322
+ end
323
+ end
324
+ alias :ordinalise :ordinalize
325
+
326
+ # Convert a number to a short ordinal form
327
+ #
328
+ # @example
329
+ # ordinalize_short(123) # => 123rd
330
+ #
331
+ # @param [Numeric, String] num
332
+ # the number to convert
333
+ #
334
+ # @return [String]
335
+ #
336
+ # @api private
337
+ def ordinalize_short(num)
338
+ num_abs = num.to_i.abs
339
+
340
+ num.to_i.to_s + (CARDINAL_TO_SHORT_ORDINAL[num_abs % 100] ||
341
+ CARDINAL_TO_SHORT_ORDINAL[num_abs % 10])
342
+ end
343
+
344
+ # Convert a number into a monetary numeral
345
+ #
346
+ # @example
347
+ # monetize(123.45)
348
+ # # => "one hundred twenty three dollars and forty five cents"
349
+ #
350
+ # @param [Numeric,String] num
351
+ # the number to convert
352
+ #
353
+ # @return [String]
354
+ #
355
+ # @api public
356
+ def monetize(num, **options)
357
+ sep = options.fetch(:separator, @configuration.separator)
358
+ curr_name = options.fetch(:currency, @configuration.currency)
359
+ n = "%0.2f" % num.to_s
360
+ decimals = (num.to_i.abs != num.to_f.abs)
361
+ sentence = convert_numeral(n, **options.merge(trailing_zeros: true))
362
+ dec_num = n.split(".")[1]
363
+ curr = CURRENCIES[curr_name.to_s.downcase.to_sym]
364
+ separators = [AND, POINT, sep].compact
365
+
366
+ if decimals
367
+ regex = /(\w+) (#{Regexp.union(separators)})/
368
+ sentence.sub!(regex, "\\1 #{curr[:units]} \\2")
369
+ else
370
+ sentence += SPACE + (num.to_i == 1 ? curr[:unit] : curr[:units])
371
+ end
372
+
373
+ if decimals
374
+ slots = Regexp.union(DECIMAL_SLOTS.map { |slot| slot.chomp('s') })
375
+ regex = /(#{slots})s?/i
376
+ suffix = dec_num.to_i == 1 ? curr[:decimal_unit] : curr[:decimal_units]
377
+ if sentence.sub!(regex, suffix).nil?
378
+ sentence += SPACE + suffix
379
+ end
380
+ end
381
+
382
+ sentence
383
+ end
384
+ alias :monetise :monetize
385
+
386
+ # Convert a number to a roman numeral
387
+ #
388
+ # @example
389
+ # romanize(2020) # => "MMXX"
390
+ #
391
+ # @param [Integer] num
392
+ # the number to convert
393
+ #
394
+ # @return [String]
395
+ #
396
+ # @api public
397
+ def romanize(num)
398
+ n = num.to_i
399
+
400
+ if n < 1 || n > 4999
401
+ raise Error, "'#{n}' is out of range"
402
+ end
403
+
404
+ CARDINAL_TO_ROMAN.keys.reverse_each.reduce([]) do |word, card|
405
+ while n >= card
406
+ n -= card
407
+ word << CARDINAL_TO_ROMAN[card]
408
+ end
409
+ word
410
+ end.join
411
+ end
412
+
413
+ private
414
+
415
+ # Convert a number into a numeral
416
+ #
417
+ # @param [Numeric] num
418
+ # the number to convert to numeral
419
+ # @param [String] delimiter
420
+ # sets the thousand's delimiter, defaults to `, `
421
+ # @param [String] decimal
422
+ # the decimal word conversion, defaults to `:fraction`
423
+ # @param [String] separator
424
+ # sets the separator between the fractional and integer numerals,
425
+ # defaults to `and` for fractions and `point` for digits
426
+ #
427
+ # @return [String]
428
+ # the number as numeral
429
+ #
430
+ # @api private
431
+ def convert_numeral(num, **options)
432
+ delimiter = options.fetch(:delimiter, @configuration.delimiter)
433
+ decimal = options.fetch(:decimal, @configuration.decimal)
434
+ separator = options.fetch(:separator, @configuration.separator)
435
+
436
+ negative = num.to_i < 0
437
+ n = num.to_i.abs
438
+ decimals = (n != num.to_f.abs)
439
+
440
+ sentence = convert_to_words(n).join(delimiter)
441
+
442
+ if sentence.empty?
443
+ sentence = ZERO
444
+ end
445
+
446
+ if negative
447
+ sentence = NEGATIVE + SPACE + sentence
448
+ end
449
+
450
+ if decimals
451
+ sentence = sentence + SPACE +
452
+ (separator.nil? ? (decimal == :fraction ? AND : POINT) : separator) +
453
+ SPACE + convert_decimals(num, **options)
454
+ end
455
+
456
+ sentence
457
+ end
458
+
459
+ # Convert decimal part to words
460
+ #
461
+ # @param [String] trailing_zeros
462
+ # whether or not to keep trailing zeros, defaults to `false`
463
+ #
464
+ # @return [String]
465
+ #
466
+ # @api private
467
+ def convert_decimals(num, **options)
468
+ delimiter = options.fetch(:delimiter, @configuration.delimiter)
469
+ decimal = options.fetch(:decimal, @configuration.decimal)
470
+ trailing_zeros = options.fetch(:trailing_zeros, @configuration.trailing_zeros)
471
+
472
+ dec_num = num.to_s.split(".")[1]
473
+ dec_num.gsub!(/0+$/, "") unless trailing_zeros
474
+
475
+ case decimal
476
+ when :fraction
477
+ unit = DECIMAL_SLOTS[dec_num.to_s.length - 1]
478
+ unit = unit[0...-1] if dec_num.to_i == 1 # strip off 's'
479
+ convert_to_words(dec_num.to_i).join(delimiter) + SPACE + unit
480
+ when :digit
481
+ dec_num.chars.map do |n|
482
+ (v = convert_tens(n.to_i)).empty? ? ZERO : v
483
+ end.join(SPACE)
484
+ else
485
+ raise Error, "Unknown decimal option '#{decimal.inspect}'"
486
+ end
487
+ end
488
+
489
+ # Convert an integer to number words
490
+ #
491
+ # @param [Integer] n
492
+ #
493
+ # @return [Array[String]]
494
+ #
495
+ # @api public
496
+ def convert_to_words(n)
497
+ words = []
498
+
499
+ SCALES.each_with_index do |scale, i|
500
+ mod = n % 1000
501
+
502
+ word = []
503
+ word << convert_hundreds(mod)
504
+ word << scale unless i.zero?
505
+
506
+ words.insert(0, word.join(SPACE))
507
+
508
+ n /= 1000
509
+
510
+ break if n.zero?
511
+ end
512
+
513
+ words
514
+ end
515
+
516
+ # Convert 3 digit number to equivalent word
517
+ #
518
+ # @return [String]
519
+ #
520
+ # @api private
521
+ def convert_hundreds(num)
522
+ word = []
523
+ hundreds = (num % 1000) / 100
524
+ tens = num % 100
525
+
526
+ if !hundreds.zero?
527
+ word << convert_tens(hundreds)
528
+ word << HUNDRED
529
+ end
530
+
531
+ if !tens.zero?
532
+ word << convert_tens(tens)
533
+ end
534
+
535
+ word.join(SPACE)
536
+ end
537
+
538
+ # Convert number in 0..99 range to equivalent word
539
+ #
540
+ # @return [String]
541
+ #
542
+ # @api private
543
+ def convert_tens(num)
544
+ word = []
545
+ tens = num % 100
546
+
547
+ if tens.to_s.size < 2 || tens <= 20
548
+ word << CARDINALS[tens]
549
+ else
550
+ word << CARDINALS[(tens / 10) * 10]
551
+ word << CARDINALS[tens % 10] unless (tens % 10).zero?
552
+ end
553
+
554
+ word.join(SPACE)
555
+ end
556
+ end # Numeral
557
+ end # Strings
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strings
4
+ class Numeral
5
+ class Configuration
6
+ # Initialize a configuration
7
+ #
8
+ # @api private
9
+ def initialize
10
+ @currency = :usd
11
+ @delimiter = ", "
12
+ @decimal = :fraction
13
+ @separator = nil
14
+ @trailing_zeros = false
15
+ end
16
+
17
+ def currency(value = (not_set = true))
18
+ if not_set
19
+ @currency
20
+ else
21
+ @currency = value
22
+ end
23
+ end
24
+
25
+ def delimiter(value = (not_set = true))
26
+ if not_set
27
+ @delimiter
28
+ else
29
+ @delimiter = value
30
+ end
31
+ end
32
+
33
+ def separator(value = (not_set = true))
34
+ if not_set
35
+ @separator
36
+ else
37
+ @separator = value
38
+ end
39
+ end
40
+
41
+ def decimal(value = (not_set = true))
42
+ if not_set
43
+ @decimal
44
+ else
45
+ @decimal = value
46
+ end
47
+ end
48
+
49
+ def trailing_zeros(value = (not_set = true))
50
+ if not_set
51
+ @trailing_zeros
52
+ else
53
+ @trailing_zeros = value
54
+ end
55
+ end
56
+ end # Configuration
57
+ end # Numeral
58
+ end # Strings
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strings
4
+ class Numeral
5
+ module Extensions
6
+ Methods = Module.new do
7
+ def numeralize(**options)
8
+ Strings::Numeral.numeralize(self, **options)
9
+ end
10
+
11
+ def cardinalize(**options)
12
+ Strings::Numeral.cardinalize(self, **options)
13
+ end
14
+
15
+ def ordinalize(**options)
16
+ Strings::Numeral.ordinalize(self, **options)
17
+ end
18
+
19
+ def ordinalize_short
20
+ Strings::Numeral.ordinalize_short(self)
21
+ end
22
+
23
+ def monetize(**options)
24
+ Strings::Numeral.monetize(self, **options)
25
+ end
26
+
27
+ def romanize
28
+ Strings::Numeral.romanize(self)
29
+ end
30
+ end
31
+
32
+ refine String do
33
+ include Methods
34
+ end
35
+
36
+ refine Float do
37
+ include Methods
38
+ end
39
+
40
+ refine Integer do
41
+ include Methods
42
+ end
43
+ end # Extensions
44
+ end # Numeral
45
+ end # Strings
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Strings
4
+ class Numeral
5
+ VERSION = "0.1.0"
6
+ end # Numeral
7
+ end # Strings
@@ -0,0 +1,34 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "strings/numeral/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "strings-numeral"
7
+ spec.version = Strings::Numeral::VERSION
8
+ spec.authors = ["Piotr Murach"]
9
+ spec.email = ["me@piotrmurach.com"]
10
+ spec.summary = %q{Express numbers as word numerals}
11
+ spec.description = %q{Express numbers as word numerals like cardinal, ordinal, roman and monetary}
12
+ spec.homepage = "https://github.com/piotrmurach/strings-numeral"
13
+ spec.license = "MIT"
14
+
15
+ if spec.respond_to?(:metadata)
16
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
17
+ spec.metadata["changelog_uri"] = "https://github.com/piotrmurach/strings-numeral/blob/master/CHANGELOG.md"
18
+ spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/strings-numeral"
19
+ spec.metadata["homepage_uri"] = spec.homepage
20
+ spec.metadata["source_code_uri"] = "https://github.com/piotrmurach/strings-numeral"
21
+ end
22
+
23
+ spec.files = Dir["lib/**/*.rb"]
24
+ spec.files += Dir["tasks/*", "strings-numeral.gemspec"]
25
+ spec.files += Dir["README.md", "CHANGELOG.md", "LICENSE.txt", "Rakefile"]
26
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
27
+ spec.require_paths = ["lib"]
28
+
29
+ spec.required_ruby_version = ">= 2.0.0"
30
+
31
+ spec.add_development_dependency "bundler", ">= 1.5"
32
+ spec.add_development_dependency "rake"
33
+ spec.add_development_dependency "rspec", "~> 3.0"
34
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Load gem inside irb console"
4
+ task :console do
5
+ require "irb"
6
+ require "irb/completion"
7
+ require File.join(__FILE__, "../../lib/strings-numeral")
8
+ ARGV.clear
9
+ IRB.start
10
+ end
11
+ task c: %w[ console ]
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Measure code coverage"
4
+ task :coverage do
5
+ begin
6
+ original, ENV["COVERAGE"] = ENV["COVERAGE"], "true"
7
+ Rake::Task["spec"].invoke
8
+ ensure
9
+ ENV["COVERAGE"] = original
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "rspec/core/rake_task"
5
+
6
+ desc "Run all specs"
7
+ RSpec::Core::RakeTask.new(:spec) do |task|
8
+ task.pattern = "spec/{unit,integration}{,/*/**}/*_spec.rb"
9
+ end
10
+
11
+ namespace :spec do
12
+ desc "Run unit specs"
13
+ RSpec::Core::RakeTask.new(:unit) do |task|
14
+ task.pattern = "spec/unit{,/*/**}/*_spec.rb"
15
+ end
16
+
17
+ desc "Run integration specs"
18
+ RSpec::Core::RakeTask.new(:integration) do |task|
19
+ task.pattern = "spec/integration{,/*/**}/*_spec.rb"
20
+ end
21
+
22
+ desc "Run performance specs"
23
+ RSpec::Core::RakeTask.new(:perf) do |task|
24
+ task.pattern = "spec/perf{,/*/**}/*_spec.rb"
25
+ end
26
+ end
27
+
28
+ rescue LoadError
29
+ %w[spec spec:unit spec:integration].each do |name|
30
+ task name do
31
+ $stderr.puts "In order to run #{name}, do `gem install rspec`"
32
+ end
33
+ end
34
+ end
metadata ADDED
@@ -0,0 +1,103 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: strings-numeral
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Murach
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2019-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '1.5'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '1.5'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ description: Express numbers as word numerals like cardinal, ordinal, roman and monetary
56
+ email:
57
+ - me@piotrmurach.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - CHANGELOG.md
63
+ - LICENSE.txt
64
+ - README.md
65
+ - Rakefile
66
+ - lib/strings-numeral.rb
67
+ - lib/strings/numeral.rb
68
+ - lib/strings/numeral/configuration.rb
69
+ - lib/strings/numeral/extensions.rb
70
+ - lib/strings/numeral/version.rb
71
+ - strings-numeral.gemspec
72
+ - tasks/console.rake
73
+ - tasks/coverage.rake
74
+ - tasks/spec.rake
75
+ homepage: https://github.com/piotrmurach/strings-numeral
76
+ licenses:
77
+ - MIT
78
+ metadata:
79
+ allowed_push_host: https://rubygems.org
80
+ changelog_uri: https://github.com/piotrmurach/strings-numeral/blob/master/CHANGELOG.md
81
+ documentation_uri: https://www.rubydoc.info/gems/strings-numeral
82
+ homepage_uri: https://github.com/piotrmurach/strings-numeral
83
+ source_code_uri: https://github.com/piotrmurach/strings-numeral
84
+ post_install_message:
85
+ rdoc_options: []
86
+ require_paths:
87
+ - lib
88
+ required_ruby_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: 2.0.0
93
+ required_rubygems_version: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ version: '0'
98
+ requirements: []
99
+ rubygems_version: 3.1.0.pre3
100
+ signing_key:
101
+ specification_version: 4
102
+ summary: Express numbers as word numerals
103
+ test_files: []