strings-numeral 0.1.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.
@@ -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: []