├── .editorconfig
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
└── workflows
│ └── ci.yml
├── .gitignore
├── .rspec
├── .rubocop.yml
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── appveyor.yml
├── bin
├── console
└── setup
├── lib
├── strings-numeral.rb
└── strings
│ ├── numeral.rb
│ └── numeral
│ ├── configuration.rb
│ ├── extensions.rb
│ └── version.rb
├── spec
├── perf
│ └── ordinalize_spec.rb
├── spec_helper.rb
└── unit
│ ├── cardinalize_spec.rb
│ ├── config_spec.rb
│ ├── extensions_spec.rb
│ ├── monetize_spec.rb
│ ├── numeralize_spec.rb
│ ├── ordinalize_spec.rb
│ └── romanize_spec.rb
├── strings-numeral.gemspec
└── tasks
├── console.rake
├── coverage.rake
└── spec.rake
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*.rb]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: piotrmurach
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Are you in the right place?
2 | * For issues or feature requests file a GitHub issue in this repository
3 | * For general questions or discussion post on StackOverflow
4 |
5 | ### Describe the problem
6 | A brief description of the issue/feature.
7 |
8 | ### Steps to reproduce the problem
9 | ```
10 | Your code here to reproduce the issue
11 | ```
12 |
13 | ### Actual behaviour
14 | What happened? This could be a description, log output, error raised etc...
15 |
16 | ### Expected behaviour
17 | What did you expect to happen?
18 |
19 | ### Describe your environment
20 |
21 | * OS version:
22 | * Ruby version:
23 | * Strings::Numeral version:
24 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ### Describe the change
2 | What does this Pull Request do?
3 |
4 | ### Why are we doing this?
5 | Any related context as to why is this is a desirable change.
6 |
7 | ### Benefits
8 | How will the library improve?
9 |
10 | ### Drawbacks
11 | Possible drawbacks applying this change.
12 |
13 | ### Requirements
14 | Put an X between brackets on each line if you have done the item:
15 | [] Tests written & passing locally?
16 | [] Code style checked?
17 | [] Rebased with `master` branch?
18 | [] Documentation updated?
19 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: CI
3 | on:
4 | push:
5 | branches:
6 | - master
7 | paths-ignore:
8 | - "bin/**"
9 | - "*.md"
10 | pull_request:
11 | branches:
12 | - master
13 | paths-ignore:
14 | - "bin/**"
15 | - "*.md"
16 | jobs:
17 | tests:
18 | name: Ruby ${{ matrix.ruby }}
19 | runs-on: ${{ matrix.os }}
20 | strategy:
21 | fail-fast: false
22 | matrix:
23 | os:
24 | - ubuntu-latest
25 | ruby:
26 | - 2.3
27 | - 2.4
28 | - 2.5
29 | - 2.6
30 | - 3.0
31 | - ruby-head
32 | - jruby-9.2.13.0
33 | - jruby-head
34 | - truffleruby-head
35 | include:
36 | - ruby: 2.0
37 | os: ubuntu-latest
38 | coverage: false
39 | bundler: 1
40 | - ruby: 2.1
41 | os: ubuntu-latest
42 | coverage: false
43 | bundler: 1
44 | - ruby: 2.2
45 | os: ubuntu-latest
46 | coverage: false
47 | bundler: 1
48 | - ruby: 2.7
49 | os: ubuntu-latest
50 | coverage: true
51 | bundler: latest
52 | env:
53 | COVERAGE: ${{ matrix.coverage }}
54 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
55 | continue-on-error: ${{ endsWith(matrix.ruby, 'head') }}
56 | steps:
57 | - uses: actions/checkout@v2
58 | - name: Set up Ruby
59 | uses: ruby/setup-ruby@v1
60 | with:
61 | ruby-version: ${{ matrix.ruby }}
62 | bundler: ${{ matrix.bundler }}
63 | - name: Install dependencies
64 | run: bundle install --jobs 4 --retry 3
65 | - name: Run tests
66 | run: bundle exec rake ci
67 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /_yardoc/
4 | /coverage/
5 | /doc/
6 | /pkg/
7 | /spec/reports/
8 | /tmp/
9 | /Gemfile.lock
10 |
11 | # rspec failure tracking
12 | .rspec_status
13 |
--------------------------------------------------------------------------------
/.rspec:
--------------------------------------------------------------------------------
1 | --format documentation
2 | --color
3 | --require spec_helper
4 |
--------------------------------------------------------------------------------
/.rubocop.yml:
--------------------------------------------------------------------------------
1 | AllCops:
2 | NewCops: enable
3 |
4 | Layout/FirstArrayElementIndentation:
5 | Enabled: false
6 |
7 | Layout/LineLength:
8 | Max: 82
9 | Exclude:
10 | - "spec/**/*"
11 |
12 | Layout/SpaceInsideHashLiteralBraces:
13 | EnforcedStyle: no_space
14 |
15 | Metrics/AbcSize:
16 | Max: 35
17 |
18 | Metrics/BlockLength:
19 | CountComments: true
20 | Max: 25
21 | IgnoredMethods: []
22 | Exclude:
23 | - "spec/**/*"
24 |
25 | Metrics/ClassLength:
26 | Max: 1500
27 |
28 | Metrics/MethodLength:
29 | Max: 20
30 |
31 | Naming/FileName:
32 | Exclude:
33 | - "lib/strings-numeral.rb"
34 |
35 | Naming/MethodParameterName:
36 | AllowedNames:
37 | - n
38 |
39 | Style/AccessorGrouping:
40 | Enabled: false
41 |
42 | Style/AsciiComments:
43 | Enabled: false
44 |
45 | Style/BlockDelimiters:
46 | Enabled: false
47 |
48 | Style/CommentedKeyword:
49 | Enabled: false
50 |
51 | Style/LambdaCall:
52 | EnforcedStyle: braces
53 |
54 | Style/NumericLiterals:
55 | Enabled: false
56 |
57 | Style/StringLiterals:
58 | EnforcedStyle: double_quotes
59 |
60 | Style/StringLiteralsInInterpolation:
61 | EnforcedStyle: double_quotes
62 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change log
2 |
3 | ## [v0.2.0] - unreleased
4 |
5 | ### Added
6 | * Add the ability to configure settings after initialisation
7 | * Add strict mode setting for enabling number validation of input parameter
8 |
9 | ### Changed
10 | * Change to use forwardable to delegate methods
11 | * Change extensions to require core library when loaded
12 | * Change conversion methods to accept only numbers in strict mode
13 |
14 | ## [v0.1.0] - 2019-12-18
15 |
16 | * Initial implementation and release
17 |
18 | [v0.2.0]: https://github.com/piotrmurach/strings-numeral/compare/v0.1.0...v0.2.0
19 | [v0.1.0]: https://github.com/piotrmurach/strings-numeral/compare/5546406...v0.1.0
20 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
6 |
7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8 |
9 | ## Our Standards
10 |
11 | Examples of behavior that contributes to a positive environment for our community include:
12 |
13 | * Demonstrating empathy and kindness toward other people
14 | * Being respectful of differing opinions, viewpoints, and experiences
15 | * Giving and gracefully accepting constructive feedback
16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17 | * Focusing on what is best not just for us as individuals, but for the overall community
18 |
19 | Examples of unacceptable behavior include:
20 |
21 | * The use of sexualized language or imagery, and sexual attention or
22 | advances of any kind
23 | * Trolling, insulting or derogatory comments, and personal or political attacks
24 | * Public or private harassment
25 | * Publishing others' private information, such as a physical or email
26 | address, without their explicit permission
27 | * Other conduct which could reasonably be considered inappropriate in a
28 | professional setting
29 |
30 | ## Enforcement Responsibilities
31 |
32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
33 |
34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
35 |
36 | ## Scope
37 |
38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
39 |
40 | ## Enforcement
41 |
42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at piotr@piotrmurach.com. All complaints will be reviewed and investigated promptly and fairly.
43 |
44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident.
45 |
46 | ## Enforcement Guidelines
47 |
48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
49 |
50 | ### 1. Correction
51 |
52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
53 |
54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
55 |
56 | ### 2. Warning
57 |
58 | **Community Impact**: A violation through a single incident or series of actions.
59 |
60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
61 |
62 | ### 3. Temporary Ban
63 |
64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
65 |
66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
67 |
68 | ### 4. Permanent Ban
69 |
70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
71 |
72 | **Consequence**: A permanent ban from any sort of public interaction within the community.
73 |
74 | ## Attribution
75 |
76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
78 |
79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity).
80 |
81 | [homepage]: https://www.contributor-covenant.org
82 |
83 | For answers to common questions about this code of conduct, see the FAQ at
84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations.
85 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source "https://rubygems.org"
2 |
3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4 |
5 | gemspec
6 |
7 | gem "json", "2.4.1" if RUBY_VERSION == "2.0.0"
8 |
9 | group :test do
10 | gem "activesupport"
11 | gem "benchmark-ips", "~> 2.7.2"
12 | gem "simplecov", "~> 0.16.1"
13 | gem "coveralls", "~> 0.8.22"
14 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.1.0")
15 | gem "rspec-benchmark", "~> 0.6"
16 | end
17 | end
18 |
19 | group :metrics do
20 | gem "yardstick", "~> 0.9.9"
21 | end
22 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019 Piotr Murach (piotrmurach.com)
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.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |

3 |
4 |
5 | # Strings::Numeral
6 |
7 | [][gem]
8 | [][gh_actions_ci]
9 | [][appveyor]
10 | [][codeclimate]
11 | [][coverage]
12 | [][inchpages]
13 |
14 | [gem]: https://badge.fury.io/rb/strings-numeral
15 | [gh_actions_ci]: https://github.com/piotrmurach/strings-numeral/actions?query=workflow%3ACI
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]: https://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 settings 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 | * `strict` - Enables number validation for the input parameter. Defaults to `false`.
232 | * `trailing_zeros` - If `true` keeps trailing zeros at the end of the fractional part. Defaults to `false`.
233 |
234 | The above settings can be passed as keyword arguments:
235 |
236 | ```ruby
237 | Strings::Numeral.cardinalize("12.100", trailing_zeros: true, decimal: :digit)
238 | # => "twelve point one zero zero"
239 | ```
240 |
241 | Or you can configure the settings for an instance during initialisation:
242 |
243 | ```ruby
244 | numeral = Strings::Numeral.new(delimiter: "; ", separator: "dot")
245 | ```
246 |
247 | After initialisation, you can use `configure` to change settings inside a block:
248 |
249 | ```ruby
250 | numeral.configure do |config|
251 | config.delimiter "; "
252 | config.separator "dot"
253 | config.decimal :digit
254 | config.trailing_zeros true
255 | end
256 | ```
257 |
258 | Once configured, you can use the instance like so:
259 |
260 | ```ruby
261 | numeral.cardinalize("1234.56700")
262 | # => "one thousand; two hundred thirty four dot five six seven zero zero"
263 | ```
264 |
265 | ## 3. Extending Core Classes
266 |
267 | Though it is highly discouraged to pollute core Ruby classes, you can add the required methods to `String`, `Float` and `Integer` classes using refinements.
268 |
269 | For example, if you wish to only extend `Float` class with `cardinalize` method do:
270 |
271 | ```ruby
272 | module MyFloatExt
273 | refine Float do
274 | def cardinalize(**options)
275 | Strings::Numeral.cardinalize(self, **options)
276 | end
277 | end
278 | end
279 | ```
280 |
281 | Then `cardinalize` method will be available for any float number where refinement is applied:
282 |
283 | ```ruby
284 | using MyFloatExt
285 |
286 | 12.34.cardinalize
287 | # => "twelve and thirty four"
288 | ```
289 |
290 | However, if you want to include all the **Strings::Numeral** methods in `Float`, `Integer` and `String` classes, you can use provided extensions file:
291 |
292 |
293 | ```ruby
294 | require "strings/numeral/extensions"
295 |
296 | using Strings::Numeral::Extensions
297 | ```
298 |
299 | Alternatively, you can choose what class you wish to refine with all the methods:
300 |
301 | ```ruby
302 | require "bigdecimal"
303 | require "strings/numeral/extensions"
304 |
305 | module MyBigDecimalExt
306 | refine BigDecimal do
307 | include Strings::Numeral::Extensions::Methods
308 | end
309 | end
310 |
311 | using MyBigDecimalExt
312 | ```
313 |
314 | ## Development
315 |
316 | 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.
317 |
318 | 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).
319 |
320 | ## Contributing
321 |
322 | 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 [code of conduct](https://github.com/piotrmurach/strings-numeral/blob/master/CODE_OF_CONDUCT.md).
323 |
324 | ## License
325 |
326 | The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
327 |
328 | ## Code of Conduct
329 |
330 | 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).
331 |
332 | ## Copyright
333 |
334 | Copyright (c) 2019 Piotr Murach. See LICENSE for further details.
335 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
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
9 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | ---
2 | skip_commits:
3 | files:
4 | - "bin/**"
5 | - "*.md"
6 | install:
7 | - SET PATH=C:\Ruby%ruby_version%\bin;%PATH%
8 | - gem install bundler -v '< 2.0'
9 | - bundle install
10 | before_test:
11 | - ruby -v
12 | - gem -v
13 | - bundle -v
14 | build: off
15 | test_script:
16 | - bundle exec rake ci
17 | environment:
18 | matrix:
19 | - ruby_version: "200"
20 | - ruby_version: "200-x64"
21 | - ruby_version: "21"
22 | - ruby_version: "21-x64"
23 | - ruby_version: "22"
24 | - ruby_version: "22-x64"
25 | - ruby_version: "23"
26 | - ruby_version: "23-x64"
27 | - ruby_version: "24"
28 | - ruby_version: "24-x64"
29 | - ruby_version: "25"
30 | - ruby_version: "25-x64"
31 | - ruby_version: "26"
32 | - ruby_version: "26-x64"
33 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "strings/numeral"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start(__FILE__)
15 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/lib/strings-numeral.rb:
--------------------------------------------------------------------------------
1 | require "strings/numeral"
2 |
--------------------------------------------------------------------------------
/lib/strings/numeral.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "forwardable"
4 |
5 | require_relative "numeral/configuration"
6 | require_relative "numeral/version"
7 |
8 | module Strings
9 | class Numeral
10 | class Error < StandardError; end
11 |
12 | NEGATIVE = "negative"
13 | HUNDRED = "hundred"
14 | ZERO = "zero"
15 | AND = "and"
16 | POINT = "point"
17 | SPACE = " "
18 |
19 | CARDINALS = {
20 | 0 => "",
21 | 1 => "one",
22 | 2 => "two",
23 | 3 => "three",
24 | 4 => "four",
25 | 5 => "five",
26 | 6 => "six",
27 | 7 => "seven",
28 | 8 => "eight",
29 | 9 => "nine",
30 | 10 => "ten",
31 | 11 => "eleven",
32 | 12 => "twelve",
33 | 13 => "thirteen",
34 | 14 => "fourteen",
35 | 15 => "fifteen",
36 | 16 => "sixteen",
37 | 17 => "seventeen",
38 | 18 => "eighteen",
39 | 19 => "nineteen",
40 | 20 => "twenty",
41 | 30 => "thirty",
42 | 40 => "forty",
43 | 50 => "fifty",
44 | 60 => "sixty",
45 | 70 => "seventy",
46 | 80 => "eighty",
47 | 90 => "ninety"
48 | }.freeze
49 |
50 | CARDINAL_TO_SHORT_ORDINAL = {
51 | 0 => "th",
52 | 1 => "st",
53 | 11 => "th",
54 | 2 => "nd",
55 | 12 => "th",
56 | 3 => "rd",
57 | 13 => "th",
58 | 4 => "th",
59 | 5 => "th",
60 | 6 => "th",
61 | 7 => "th",
62 | 8 => "th",
63 | 9 => "th"
64 | }.freeze
65 |
66 | CARDINAL_TO_ORDINAL = {
67 | "zero" => "zeroth",
68 | "one" => "first",
69 | "two" => "second",
70 | "three" => "third",
71 | "four" => "fourth",
72 | "five" => "fifth",
73 | "six" => "sixth",
74 | "seven" => "seventh",
75 | "eight" => "eighth",
76 | "nine" => "ninth",
77 | "ten" => "tenth",
78 | "eleven" => "eleventh",
79 | "twelve" => "twelfth",
80 | "thirteen" => "thirteenth",
81 | "fourteen" => "fourteenth",
82 | "fifteen" => "fifteenth",
83 | "sixteen" => "sixteenth",
84 | "seventeen" => "seventeenth",
85 | "eighteen" => "eighteenth",
86 | "nineteen" => "nineteenth",
87 | "twenty" => "twentieth",
88 | "thirty" => "thirtieth",
89 | "forty" => "fortieth",
90 | "fifty" => "fiftieth",
91 | "sixty" => "sixtieth",
92 | "seventy" => "seventieth",
93 | "eighty" => "eightieth",
94 | "ninety" => "ninetieth"
95 | }.freeze
96 |
97 | CARDINAL_TO_ROMAN = {
98 | 1 => "I",
99 | 4 => "IV",
100 | 5 => "V",
101 | 9 => "IX",
102 | 10 => "X",
103 | 40 => "XL",
104 | 50 => "L",
105 | 90 => "XC",
106 | 100 => "C",
107 | 400 => "CD",
108 | 500 => "D",
109 | 900 => "CM",
110 | 1000 => "M"
111 | }.freeze
112 |
113 | SCALES = [
114 | "hundreds-tens-ones",
115 | "thousand",
116 | "million",
117 | "billion",
118 | "trillion",
119 | "quadrillion",
120 | "quintillion",
121 | "sextillion",
122 | "septillion",
123 | "octillion",
124 | "nonillion",
125 | "decillion",
126 | "undecillion",
127 | "duodecillion",
128 | "tredecillion",
129 | "quattuordecillion",
130 | "quindecillion",
131 | "sexdecillion",
132 | "septemdecillion",
133 | "octodecillion",
134 | "novemdecillion",
135 | "vigintillion"
136 | ].freeze
137 |
138 | DECIMAL_SLOTS = [
139 | "tenths",
140 | "hundredths",
141 | "thousandths",
142 | "ten-thousandths",
143 | "hundred-thousandths",
144 | "millionths",
145 | "ten-millionths",
146 | "hundred-millionths",
147 | "billionths",
148 | "ten-billionths",
149 | "hundred-billionths",
150 | "trillionths",
151 | "quadrillionths",
152 | "quintillionths",
153 | "sextillionths",
154 | "septillionths",
155 | "octillionths",
156 | "nonillionths",
157 | "decillionths",
158 | "undecillionths",
159 | "duodecillionths",
160 | "tredecillionths",
161 | "quattuordecillionths",
162 | "quindecillionths",
163 | "sexdecillionths",
164 | "septemdecillionths",
165 | "octodecillionths",
166 | "novemdecillionths",
167 | "vigintillionths"
168 | ].freeze
169 |
170 | CURRENCIES = {
171 | eur: {
172 | unit: "euro",
173 | units: "euros",
174 | decimal_unit: "cent",
175 | decimal_units: "cents"
176 | },
177 | gbp: {
178 | unit: "pound",
179 | units: "pounds",
180 | decimal_unit: "pence",
181 | decimal_units: "pence"
182 | },
183 | jpy: {
184 | unit: "yen",
185 | units: "yen",
186 | decimal_unit: "sen",
187 | decimal_units: "sen"
188 | },
189 | pln: {
190 | unit: "zloty",
191 | units: "zlotys",
192 | decimal_unit: "grosz",
193 | decimal_units: "groszy"
194 | },
195 | usd: {
196 | unit: "dollar",
197 | units: "dollars",
198 | decimal_unit: "cent",
199 | decimal_units: "cents"
200 | }
201 | }.freeze
202 |
203 | # Global instance
204 | #
205 | # @api private
206 | def self.instance
207 | @instance ||= Numeral.new
208 | end
209 |
210 | class << self
211 | extend Forwardable
212 |
213 | delegate %i[numeralize cardinalize cardinalise ordinalize
214 | ordinalise ordinalize_short monetize monetise
215 | romanize romanise] => :instance
216 | end
217 |
218 | # Create numeral with custom configuration
219 | #
220 | # @return [Numeral]
221 | #
222 | # @api public
223 | def initialize(**options)
224 | configuration.update(**options)
225 | end
226 |
227 | # Access configuration
228 | #
229 | # @api public
230 | def configuration
231 | @configuration ||= Configuration.new
232 | end
233 |
234 | # Configure numerals settings
235 | #
236 | # @example
237 | # numeral = Strings::Numeral.new
238 | # numeral.configure do |config|
239 | # config.decimal :digit
240 | # config.delimiter "; "
241 | # config.separator "dot"
242 | # config.trailing_zeros true
243 | # end
244 | #
245 | # @example
246 | # numeral = Strings::Numeral.new
247 | # numeral.configure decimal: :digit, separator: "dot"
248 | #
249 | # @yieldparam [Configuration]
250 | #
251 | # @api public
252 | def configure(**options)
253 | if block_given?
254 | yield configuration
255 | else
256 | configuration.update(**options)
257 | end
258 | end
259 |
260 | # Convert a number to a numeral
261 | #
262 | # @param [Numeric,String] num
263 | # the number to convert
264 | #
265 | # @api public
266 | def numeralize(num, **options)
267 | case options.delete(:term)
268 | when /ord/
269 | ordinalize(num, **options)
270 | else
271 | cardinalize(num, **options)
272 | end
273 | end
274 |
275 | # Convert a number to a cardinal numeral
276 | #
277 | # @example
278 | # cardinalize(1234)
279 | # # => one thousand, two hundred thirty four
280 | #
281 | # @param [Numeric,String] num
282 | #
283 | # @return [String]
284 | #
285 | # @api public
286 | def cardinalize(num, **options)
287 | check_number(num, **options)
288 | convert_numeral(num, **options)
289 | end
290 | alias cardinalise cardinalize
291 |
292 | # Convert a number to an ordinal numeral
293 | #
294 | # @example
295 | # ordinalize(1234)
296 | # # => one thousand, two hundred thirty fourth
297 | #
298 | # ordinalize(12, short: true) # => 12th
299 | #
300 | # @param [Numeric,String] num
301 | # the number to convert
302 | #
303 | # @return [String]
304 | #
305 | # @api public
306 | def ordinalize(num, **options)
307 | check_number(num, **options)
308 | if options[:short]
309 | ordinalize_short(num)
310 | else
311 | decimals = (num.to_i.abs != num.to_f.abs)
312 | sentence = convert_numeral(num, **options)
313 | separators = [AND, POINT,
314 | options.fetch(:separator, @configuration.separator)].compact
315 |
316 | if decimals && sentence =~ /(\w+) (#{Regexp.union(separators)})/
317 | last_digits = $1
318 | separator = $2
319 | replacement = CARDINAL_TO_ORDINAL[last_digits]
320 | pattern = /#{last_digits} #{separator}/
321 | suffix = "#{replacement} #{separator}"
322 | elsif sentence =~ /(\w+)$/
323 | last_digits = $1
324 | replacement = CARDINAL_TO_ORDINAL[last_digits]
325 | pattern = /#{last_digits}$/
326 | suffix = replacement
327 | end
328 |
329 | if replacement
330 | sentence.sub(pattern, suffix)
331 | else
332 | sentence
333 | end
334 | end
335 | end
336 | alias ordinalise ordinalize
337 |
338 | # Convert a number to a short ordinal form
339 | #
340 | # @example
341 | # ordinalize_short(123) # => 123rd
342 | #
343 | # @param [Numeric, String] num
344 | # the number to convert
345 | #
346 | # @return [String]
347 | #
348 | # @api private
349 | def ordinalize_short(num)
350 | num_abs = num.to_i.abs
351 |
352 | num.to_i.to_s + (CARDINAL_TO_SHORT_ORDINAL[num_abs % 100] ||
353 | CARDINAL_TO_SHORT_ORDINAL[num_abs % 10])
354 | end
355 |
356 | # Convert a number into a monetary numeral
357 | #
358 | # @example
359 | # monetize(123.45)
360 | # # => "one hundred twenty three dollars and forty five cents"
361 | #
362 | # @param [Numeric,String] num
363 | # the number to convert
364 | #
365 | # @return [String]
366 | #
367 | # @api public
368 | def monetize(num, **options)
369 | check_number(num, **options)
370 | sep = options.fetch(:separator, @configuration.separator)
371 | curr_name = options.fetch(:currency, @configuration.currency)
372 | n = format("%0.2f", num.to_s)
373 | decimals = (num.to_i.abs != num.to_f.abs)
374 | sentence = convert_numeral(n, **options.merge(trailing_zeros: true))
375 | dec_num = n.split(".")[1]
376 | curr = CURRENCIES[curr_name.to_s.downcase.to_sym]
377 | separators = [AND, POINT, sep].compact
378 |
379 | if decimals
380 | regex = /(\w+) (#{Regexp.union(separators)})/
381 | sentence.sub!(regex, "\\1 #{curr[:units]} \\2")
382 | else
383 | sentence += SPACE + (num.to_i == 1 ? curr[:unit] : curr[:units])
384 | end
385 |
386 | if decimals
387 | slots = Regexp.union(DECIMAL_SLOTS.map { |slot| slot.chomp("s") })
388 | regex = /(#{slots})s?/i
389 | suffix = dec_num.to_i == 1 ? curr[:decimal_unit] : curr[:decimal_units]
390 | if sentence.sub!(regex, suffix).nil?
391 | sentence += SPACE + suffix
392 | end
393 | end
394 |
395 | sentence
396 | end
397 | alias monetise monetize
398 |
399 | # Convert a number to a roman numeral
400 | #
401 | # @example
402 | # romanize(2020) # => "MMXX"
403 | #
404 | # @param [Integer] num
405 | # the number to convert
406 | # @param [Boolean] strict
407 | # whether or not to validate input is a number
408 | #
409 | # @return [String]
410 | #
411 | # @api public
412 | def romanize(num, strict: configuration.strict)
413 | check_number(num, strict: strict)
414 | n = num.to_i
415 |
416 | if n < 1 || n > 4999
417 | raise Error, "'#{n}' is out of range"
418 | end
419 |
420 | CARDINAL_TO_ROMAN.keys.reverse_each.reduce([]) do |word, card|
421 | while n >= card
422 | n -= card
423 | word << CARDINAL_TO_ROMAN[card]
424 | end
425 | word
426 | end.join
427 | end
428 |
429 | private
430 |
431 | # Check whether the value is a number when in strict mode
432 | #
433 | # @api private
434 | def check_number(value, **options)
435 | strict = options.fetch(:strict, @configuration.strict)
436 | strict && !number?(value) && raise_not_number(value)
437 | end
438 |
439 | # Check whether or not value is a number
440 | #
441 | # @param [Object] value
442 | # the value to check
443 | #
444 | # @return [Boolean]
445 | #
446 | # @api private
447 | def number?(value)
448 | !Float(value).nil?
449 | rescue TypeError, ArgumentError
450 | false
451 | end
452 |
453 | # Raise not a number error
454 | #
455 | # @raise [Error]
456 | #
457 | # @api private
458 | def raise_not_number(value)
459 | raise Error, "not a number: #{value.inspect}"
460 | end
461 |
462 | # Convert a number into a numeral
463 | #
464 | # @param [Numeric] num
465 | # the number to convert to numeral
466 | # @param [String] delimiter
467 | # sets the thousand's delimiter, defaults to `, `
468 | # @param [String] decimal
469 | # the decimal word conversion, defaults to `:fraction`
470 | # @param [String] separator
471 | # sets the separator between the fractional and integer numerals,
472 | # defaults to `and` for fractions and `point` for digits
473 | #
474 | # @return [String]
475 | # the number as numeral
476 | #
477 | # @api private
478 | def convert_numeral(num, **options)
479 | delimiter = options.fetch(:delimiter, @configuration.delimiter)
480 | decimal = options.fetch(:decimal, @configuration.decimal)
481 | separator = options.fetch(:separator, @configuration.separator)
482 |
483 | negative = num.to_i < 0
484 | n = num.to_i.abs
485 | decimals = (n != num.to_f.abs)
486 |
487 | sentence = convert_to_words(n).join(delimiter)
488 |
489 | if sentence.empty?
490 | sentence = ZERO
491 | end
492 |
493 | if negative
494 | sentence = NEGATIVE + SPACE + sentence
495 | end
496 |
497 | if decimals
498 | sep = separator.nil? ? (decimal == :fraction ? AND : POINT) : separator
499 | sentence += SPACE + sep + SPACE + convert_decimals(num, **options)
500 | end
501 |
502 | sentence
503 | end
504 |
505 | # Convert decimal part to words
506 | #
507 | # @param [String] trailing_zeros
508 | # whether or not to keep trailing zeros, defaults to `false`
509 | #
510 | # @return [String]
511 | #
512 | # @api private
513 | def convert_decimals(num, **options)
514 | delimiter = options.fetch(:delimiter, @configuration.delimiter)
515 | decimal = options.fetch(:decimal, @configuration.decimal)
516 | trailing_zeros = options.fetch(:trailing_zeros, @configuration.trailing_zeros)
517 |
518 | dec_num = num.to_s.split(".")[1]
519 | dec_num.gsub!(/0+$/, "") unless trailing_zeros
520 |
521 | case decimal
522 | when :fraction
523 | unit = DECIMAL_SLOTS[dec_num.to_s.length - 1]
524 | unit = unit[0...-1] if dec_num.to_i == 1 # strip off 's'
525 | convert_to_words(dec_num.to_i).join(delimiter) + SPACE + unit
526 | when :digit
527 | dec_num.chars.map do |n|
528 | (v = convert_tens(n.to_i)).empty? ? ZERO : v
529 | end.join(SPACE)
530 | else
531 | raise Error, "Unknown decimal option '#{decimal.inspect}'"
532 | end
533 | end
534 |
535 | # Convert an integer to number words
536 | #
537 | # @param [Integer] n
538 | #
539 | # @return [Array[String]]
540 | #
541 | # @api public
542 | def convert_to_words(n)
543 | words = []
544 |
545 | SCALES.each_with_index do |scale, i|
546 | mod = n % 1000
547 |
548 | word = []
549 | word << convert_hundreds(mod)
550 | word << scale unless i.zero?
551 |
552 | words.insert(0, word.join(SPACE))
553 |
554 | n /= 1000
555 |
556 | break if n.zero?
557 | end
558 |
559 | words
560 | end
561 |
562 | # Convert 3 digit number to equivalent word
563 | #
564 | # @return [String]
565 | #
566 | # @api private
567 | def convert_hundreds(num)
568 | word = []
569 | hundreds = (num % 1000) / 100
570 | tens = num % 100
571 |
572 | if !hundreds.zero?
573 | word << convert_tens(hundreds)
574 | word << HUNDRED
575 | end
576 |
577 | if !tens.zero?
578 | word << convert_tens(tens)
579 | end
580 |
581 | word.join(SPACE)
582 | end
583 |
584 | # Convert number in 0..99 range to equivalent word
585 | #
586 | # @return [String]
587 | #
588 | # @api private
589 | def convert_tens(num)
590 | word = []
591 | tens = num % 100
592 |
593 | if tens.to_s.size < 2 || tens <= 20
594 | word << CARDINALS[tens]
595 | else
596 | word << CARDINALS[(tens / 10) * 10]
597 | word << CARDINALS[tens % 10] unless (tens % 10).zero?
598 | end
599 |
600 | word.join(SPACE)
601 | end
602 | end # Numeral
603 | end # Strings
604 |
--------------------------------------------------------------------------------
/lib/strings/numeral/configuration.rb:
--------------------------------------------------------------------------------
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 | @strict = false
15 | @trailing_zeros = false
16 | end
17 |
18 | # Update current configuration
19 | #
20 | # @api public
21 | def update(currency: nil, delimiter: nil, separator: nil, decimal: nil,
22 | trailing_zeros: nil, strict: nil)
23 | @currency = currency if currency
24 | @delimiter = delimiter if delimiter
25 | @separator = separator if separator
26 | @decimal = decimal if decimal
27 | @trailing_zeros = trailing_zeros if trailing_zeros
28 | @strict = strict if strict
29 | end
30 |
31 | # The currency words to use when converting
32 | #
33 | # @example
34 | # numeral = Strings::Numeral.new
35 | # numeral.configure do |config|
36 | # config.currency "PLN"
37 | # end
38 | #
39 | # @param [String] value
40 | #
41 | # @api public
42 | def currency(value = (not_set = true))
43 | if not_set
44 | @currency
45 | else
46 | @currency = value
47 | end
48 | end
49 |
50 | # The thousands delimiter
51 | #
52 | # @example
53 | # numeral = Strings::Numeral.new
54 | # numeral.configure do |config|
55 | # config.delimiter "; "
56 | # end
57 | #
58 | # @param [String] value
59 | #
60 | # @api public
61 | def delimiter(value = (not_set = true))
62 | if not_set
63 | @delimiter
64 | else
65 | @delimiter = value
66 | end
67 | end
68 |
69 | # The integer and fractional parts separator
70 | #
71 | # @example
72 | # numeral = Strings::Numeral.new
73 | # numeral.configure do |config|
74 | # config.separator "dot"
75 | # end
76 | #
77 | # @param [String] value
78 | #
79 | # @api public
80 | def separator(value = (not_set = true))
81 | if not_set
82 | @separator
83 | else
84 | @separator = value
85 | end
86 | end
87 |
88 | # The format for fractional part of a number
89 | #
90 | # @example
91 | # numeral = Strings::Numeral.new
92 | # numeral.configure do |config|
93 | # config.decimal :digit
94 | # end
95 | #
96 | # @param [Symbol] value
97 | #
98 | # @api public
99 | def decimal(value = (not_set = true))
100 | if not_set
101 | @decimal
102 | else
103 | @decimal = value
104 | end
105 | end
106 |
107 | # Whether or not to keep trailing zeros
108 | #
109 | # @example
110 | # numeral = Strings::Numeral.new
111 | # numeral.configure do |config|
112 | # config.trailing_zeros true
113 | # end
114 | #
115 | # @param [Boolean] value
116 | #
117 | # @api public
118 | def trailing_zeros(value = (not_set = true))
119 | if not_set
120 | @trailing_zeros
121 | else
122 | @trailing_zeros = value
123 | end
124 | end
125 |
126 | # Whether or not to check input parameter is a number
127 | #
128 | # @example
129 | # numeral = Strings::Numeral.new
130 | # numeral.configure do |config|
131 | # config.strict true
132 | # end
133 | #
134 | # @param [Boolean] value
135 | #
136 | # @api public
137 | def strict(value = (not_set = true))
138 | if not_set
139 | @strict
140 | else
141 | @strict = value
142 | end
143 | end
144 | end # Configuration
145 | end # Numeral
146 | end # Strings
147 |
--------------------------------------------------------------------------------
/lib/strings/numeral/extensions.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "../numeral" unless defined?(Strings::Numeral)
4 |
5 | module Strings
6 | class Numeral
7 | module Extensions
8 | Methods = Module.new do
9 | def numeralize(**options)
10 | Strings::Numeral.numeralize(self, **options)
11 | end
12 |
13 | def cardinalize(**options)
14 | Strings::Numeral.cardinalize(self, **options)
15 | end
16 |
17 | def ordinalize(**options)
18 | Strings::Numeral.ordinalize(self, **options)
19 | end
20 |
21 | def ordinalize_short
22 | Strings::Numeral.ordinalize_short(self)
23 | end
24 |
25 | def monetize(**options)
26 | Strings::Numeral.monetize(self, **options)
27 | end
28 |
29 | def romanize
30 | Strings::Numeral.romanize(self)
31 | end
32 | end
33 |
34 | refine String do
35 | include Methods
36 | end
37 |
38 | refine Float do
39 | include Methods
40 | end
41 |
42 | refine Integer do
43 | include Methods
44 | end
45 | end # Extensions
46 | end # Numeral
47 | end # Strings
48 |
--------------------------------------------------------------------------------
/lib/strings/numeral/version.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | module Strings
4 | class Numeral
5 | VERSION = "0.1.0"
6 | end # Numeral
7 | end # Strings
8 |
--------------------------------------------------------------------------------
/spec/perf/ordinalize_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "rspec-benchmark"
4 | require "active_support"
5 |
6 | RSpec.describe Strings::Numeral do
7 | include RSpec::Benchmark::Matchers
8 |
9 | it "converts number to short ordinal faster than ActiveSupport" do
10 | expect {
11 | Strings::Numeral.ordinalize_short(12345)
12 | }.to perform_faster_than {
13 | ActiveSupport::Inflector.ordinalize(12345)
14 | }.at_least(350).times
15 | end
16 |
17 | it "converts number to ordinal 10 times faster than ActiveSupport" do
18 | expect {
19 | Strings::Numeral.ordinalize(12345)
20 | }.to perform_faster_than {
21 | ActiveSupport::Inflector.ordinalize(12345)
22 | }.at_least(10).times
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | if ENV["COVERAGE"] == "true"
4 | require "simplecov"
5 | require "coveralls"
6 |
7 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([
8 | SimpleCov::Formatter::HTMLFormatter,
9 | Coveralls::SimpleCov::Formatter
10 | ])
11 |
12 | SimpleCov.start do
13 | command_name "spec"
14 | add_filter "spec"
15 | end
16 | end
17 |
18 | require "bundler/setup"
19 | require "strings/numeral"
20 |
21 | RSpec.configure do |config|
22 | # Enable flags like --only-failures and --next-failure
23 | config.example_status_persistence_file_path = ".rspec_status"
24 |
25 | # Disable RSpec exposing methods globally on `Module` and `main`
26 | config.disable_monkey_patching!
27 |
28 | config.expect_with :rspec do |c|
29 | c.syntax = :expect
30 | end
31 | end
32 |
--------------------------------------------------------------------------------
/spec/unit/cardinalize_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Strings::Numeral, "#cardinalize" do
4 | {
5 | 0 => "zero",
6 | 1 => "one",
7 | 20 => "twenty",
8 | 21 => "twenty one",
9 | 100 => "one hundred",
10 | 101 => "one hundred one",
11 | 111 => "one hundred eleven",
12 | 113 => "one hundred thirteen",
13 | 123 => "one hundred twenty three",
14 | 1234 => "one thousand, two hundred thirty four",
15 | 12345 => "twelve thousand, three hundred forty five",
16 | 123456 => "one hundred twenty three thousand, four hundred fifty six",
17 | 1234567 => "one million, two hundred thirty four thousand, five hundred sixty seven",
18 | -125 => "negative one hundred twenty five",
19 | 0.1 => "zero and one tenth",
20 | 0.01 => "zero and one hundredth",
21 | 0.21 => "zero and twenty one hundredths",
22 | 1.23 => "one and twenty three hundredths",
23 | 12.003 => "twelve and three thousandths",
24 | 0.001 => "zero and one thousandth",
25 | 12.100 => "twelve and one tenth",
26 | 12.000 => "twelve",
27 | 123.456 => "one hundred twenty three and four hundred fifty six thousandths",
28 | -114.5678 => "negative one hundred fourteen and five thousand, six hundred seventy eight ten-thousandths",
29 | 1234.567 => "one thousand, two hundred thirty four and five hundred sixty seven thousandths",
30 | -3456.07 => "negative three thousand, four hundred fifty six and seven hundredths"
31 | }.each do |num, word|
32 | it "cardinalizes #{num.inspect} to #{word.inspect}" do
33 | expect(Strings::Numeral.cardinalize(num)).to eq(word)
34 | end
35 | end
36 |
37 | {
38 | 0.1 => "zero point one",
39 | 0.21 => "zero point two one",
40 | 1.23 => "one point two three",
41 | 12.003 => "twelve point zero zero three",
42 | 12.100 => "twelve point one",
43 | 12.000 => "twelve",
44 | 123.456 => "one hundred twenty three point four five six",
45 | -114.5678 => "negative one hundred fourteen point five six seven eight",
46 | 1234.567 => "one thousand, two hundred thirty four point five six seven",
47 | -3456.07 => "negative three thousand, four hundred fifty six point zero seven"
48 | }.each do |num, word|
49 | it "cardinalizes #{num.inspect} to #{word.inspect}" do
50 | expect(Strings::Numeral.cardinalize(num, decimal: :digit)).to eq(word)
51 | end
52 | end
53 |
54 | it "doesn't recognise :decimal option" do
55 | expect {
56 | Strings::Numeral.cardinalize(123.45, decimal: :unknown)
57 | }.to raise_error(Strings::Numeral::Error, "Unknown decimal option ':unknown'")
58 | end
59 |
60 | it "allows to change a thousand's delimiter" do
61 | expect(Strings::Numeral.cardinalise(1_234_567, delimiter: " and "))
62 | .to eq("one million and two hundred thirty four thousand and five hundred sixty seven")
63 | end
64 |
65 | it "changes a separator between fractional and integer numerals" do
66 | expect(Strings::Numeral.cardinalize(1_234.567, separator: "dot"))
67 | .to eq("one thousand, two hundred thirty four dot five hundred sixty seven thousandths")
68 | end
69 |
70 | it "removes trailing zeros for strings to match number behaviour" do
71 | expect(Strings::Numeral.cardinalize("12.100")).to eq("twelve and one tenth")
72 | end
73 |
74 | it "keeps trailing zeros for strings when :trailing_zeros is set to true" do
75 | expect(Strings::Numeral.cardinalize("12.100", trailing_zeros: true))
76 | .to eq("twelve and one hundred thousandths")
77 | end
78 |
79 | it "keeps trailing zeros for strings with only one zero" do
80 | expect(Strings::Numeral.cardinalize("12.70", trailing_zeros: true))
81 | .to eq("twelve and seventy hundredths")
82 | end
83 |
84 | it "keeps trailing zeros when decimal format is digit" do
85 | expect(Strings::Numeral.cardinalize("12.100", trailing_zeros: true, decimal: :digit))
86 | .to eq("twelve point one zero zero")
87 | end
88 |
89 | it "checks value in strict mode as not a number" do
90 | expect {
91 | Strings::Numeral.cardinalize("unknown", strict: true)
92 | }.to raise_error(Strings::Numeral::Error, "not a number: \"unknown\"")
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/spec/unit/config_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Strings::Numeral, "configuration" do
4 | it "defaults configuration values" do
5 | numeral = Strings::Numeral.new
6 |
7 | expect(numeral.configuration.currency).to eq(:usd)
8 | expect(numeral.configuration.decimal).to eq(:fraction)
9 | expect(numeral.configuration.delimiter).to eq(", ")
10 | expect(numeral.configuration.separator).to eq(nil)
11 | expect(numeral.configuration.strict).to eq(false)
12 | expect(numeral.configuration.trailing_zeros).to eq(false)
13 | end
14 |
15 | it "configures settings at initialisation" do
16 | numeral = Strings::Numeral.new(
17 | currency: "PLN",
18 | decimal: :digit,
19 | delimiter: "; ",
20 | separator: "dot",
21 | strict: true,
22 | trailing_zeros: true
23 | )
24 |
25 | expect(numeral.configuration.currency).to eq("PLN")
26 | expect(numeral.configuration.decimal).to eq(:digit)
27 | expect(numeral.configuration.delimiter).to eq("; ")
28 | expect(numeral.configuration.separator).to eq("dot")
29 | expect(numeral.configuration.strict).to eq(true)
30 | expect(numeral.configuration.trailing_zeros).to eq(true)
31 |
32 | expect(numeral.monetize("1234.56700"))
33 | .to eq("one thousand; two hundred thirty four zlotys dot five seven groszy")
34 | end
35 |
36 | it "configures strict mode and validates value as not a number" do
37 | numeral = Strings::Numeral.new(strict: true)
38 |
39 | expect {
40 | numeral.monetize("strict")
41 | }.to raise_error(Strings::Numeral::Error, "not a number: \"strict\"")
42 | end
43 |
44 | it "configures settings at runtime using keyword arguments" do
45 | numeral = Strings::Numeral.new
46 |
47 | numeral.configure(currency: "PLN", decimal: :digit, delimiter: "; ",
48 | separator: "dot", strict: true, trailing_zeros: true)
49 |
50 | expect(numeral.configuration.currency).to eq("PLN")
51 | expect(numeral.configuration.decimal).to eq(:digit)
52 | expect(numeral.configuration.delimiter).to eq("; ")
53 | expect(numeral.configuration.separator).to eq("dot")
54 | expect(numeral.configuration.strict).to eq(true)
55 | expect(numeral.configuration.trailing_zeros).to eq(true)
56 |
57 | expect(numeral.monetize("1234.56700"))
58 | .to eq("one thousand; two hundred thirty four zlotys dot five seven groszy")
59 | end
60 |
61 | it "configures settings at runtime for a cardinal instance" do
62 | numeral = Strings::Numeral.new
63 |
64 | numeral.configure do |config|
65 | config.decimal :digit
66 | config.delimiter "; "
67 | config.separator "dot"
68 | config.strict true
69 | config.trailing_zeros true
70 | end
71 |
72 | expect(numeral.configuration.decimal).to eq(:digit)
73 | expect(numeral.configuration.delimiter).to eq("; ")
74 | expect(numeral.configuration.separator).to eq("dot")
75 | expect(numeral.configuration.strict).to eq(true)
76 | expect(numeral.configuration.trailing_zeros).to eq(true)
77 |
78 | expect(numeral.cardinalize("1234.56700"))
79 | .to eq("one thousand; two hundred thirty four dot five six seven zero zero")
80 | end
81 |
82 | it "configures settings at runtime for an ordinal instance" do
83 | numeral = Strings::Numeral.new
84 |
85 | numeral.configure do |config|
86 | config.decimal :digit
87 | config.delimiter "; "
88 | config.separator "dot"
89 | config.trailing_zeros true
90 | end
91 |
92 | expect(numeral.configuration.decimal).to eq(:digit)
93 | expect(numeral.configuration.delimiter).to eq("; ")
94 | expect(numeral.configuration.separator).to eq("dot")
95 | expect(numeral.configuration.trailing_zeros).to eq(true)
96 |
97 | expect(numeral.ordinalize("1234.56700"))
98 | .to eq("one thousand; two hundred thirty fourth dot five six seven zero zero")
99 | end
100 |
101 | it "configures settings at runtime for monetization" do
102 | numeral = Strings::Numeral.new
103 |
104 | numeral.configure do |config|
105 | config.currency :pln
106 | config.decimal :digit
107 | config.delimiter "; "
108 | config.separator "dot"
109 | end
110 |
111 | expect(numeral.configuration.currency).to eq(:pln)
112 | expect(numeral.configuration.decimal).to eq(:digit)
113 | expect(numeral.configuration.delimiter).to eq("; ")
114 | expect(numeral.configuration.separator).to eq("dot")
115 |
116 | expect(numeral.monetize("1234.56700"))
117 | .to eq("one thousand; two hundred thirty four zlotys dot five seven groszy")
118 | end
119 | end
120 |
--------------------------------------------------------------------------------
/spec/unit/extensions_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require "strings/numeral/extensions"
4 |
5 | using Strings::Numeral::Extensions
6 |
7 | RSpec.describe Strings::Numeral::Extensions, "extensions" do
8 | context "when String" do
9 | it "cardinalizes a number" do
10 | expect("12.34".numeralize).to eq("twelve and thirty four hundredths")
11 | end
12 |
13 | it "cardinalizes a number" do
14 | expect("12.34".cardinalize).to eq("twelve and thirty four hundredths")
15 | end
16 |
17 | it "ordinalizes a number" do
18 | expect("12.34".ordinalize).to eq("twelfth and thirty four hundredths")
19 | end
20 |
21 | it "ordinalizes a number" do
22 | expect("12.34".ordinalize_short).to eq("12th")
23 | end
24 |
25 | it "monetizes a number" do
26 | expect("12.34".monetize).to eq("twelve dollars and thirty four cents")
27 | end
28 |
29 | it "romanizes a number" do
30 | expect("12".romanize).to eq("XII")
31 | end
32 | end
33 |
34 | context "when Float" do
35 | it "cardinalizes a number" do
36 | expect(12.34.cardinalize).to eq("twelve and thirty four hundredths")
37 | end
38 |
39 | it "ordinalizes a number" do
40 | expect(12.34.ordinalize).to eq("twelfth and thirty four hundredths")
41 | end
42 |
43 | it "monetizes a number" do
44 | expect(12.34.monetize).to eq("twelve dollars and thirty four cents")
45 | end
46 |
47 | it "romanizes a number" do
48 | expect(12.1.romanize).to eq("XII")
49 | end
50 | end
51 |
52 | context "when Integer" do
53 | it "cardinalizes a number" do
54 | expect(12.cardinalize).to eq("twelve")
55 | end
56 |
57 | it "ordinalizes a number" do
58 | expect(12.ordinalize).to eq("twelfth")
59 | end
60 |
61 | it "monetizes a number" do
62 | expect(12.monetize).to eq("twelve dollars")
63 | end
64 |
65 | it "romanizes a number" do
66 | expect(12.romanize).to eq("XII")
67 | end
68 | end
69 | end
70 |
--------------------------------------------------------------------------------
/spec/unit/monetize_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Strings::Numeral, "#monetize" do
4 | {
5 | 1 => "one dollar",
6 | 20 => "twenty dollars",
7 | 21 => "twenty one dollars",
8 | 100 => "one hundred dollars",
9 | 1234 => "one thousand, two hundred thirty four dollars",
10 | 12345 => "twelve thousand, three hundred forty five dollars",
11 | 123456 => "one hundred twenty three thousand, four hundred fifty six dollars",
12 | 1234567 => "one million, two hundred thirty four thousand, five hundred sixty seven dollars",
13 | -125 => "negative one hundred twenty five dollars",
14 | 0.1 => "zero dollars and ten cents",
15 | 0.7 => "zero dollars and seventy cents",
16 | 0.01 => "zero dollars and one cent",
17 | 0.21 => "zero dollars and twenty one cents",
18 | 0.1234 => "zero dollars and twelve cents",
19 | 12.100 => "twelve dollars and ten cents",
20 | 12.000 => "twelve dollars",
21 | 123.456 => "one hundred twenty three dollars and forty six cents",
22 | -114.5678 => "negative one hundred fourteen dollars and fifty seven cents",
23 | 1234.567 => "one thousand, two hundred thirty four dollars and fifty seven cents",
24 | -3456.07 => "negative three thousand, four hundred fifty six dollars and seven cents"
25 | }.each do |num, word|
26 | it "monetizes #{num.inspect} to #{word.inspect}" do
27 | expect(Strings::Numeral.monetize(num)).to eq(word)
28 | end
29 | end
30 |
31 | it "monetizes 123.45 in EUR " do
32 | expect(Strings::Numeral.monetize(123.456, currency: :eur))
33 | .to eq("one hundred twenty three euros and forty six cents")
34 | end
35 |
36 | it "monetizes 123.45 in GBP " do
37 | expect(Strings::Numeral.monetize(123.456, currency: :gbp))
38 | .to eq("one hundred twenty three pounds and forty six pence")
39 | end
40 |
41 | it "monetizes 123.45 in JPY " do
42 | expect(Strings::Numeral.monetize(123.456, currency: :jpy))
43 | .to eq("one hundred twenty three yen and forty six sen")
44 | end
45 |
46 | it "monetizes 123.45 in PLN " do
47 | expect(Strings::Numeral.monetize(123.456, currency: :pln))
48 | .to eq("one hundred twenty three zlotys and forty six groszy")
49 | end
50 |
51 | it "checks value in strict mode as not a number" do
52 | expect {
53 | Strings::Numeral.monetize("unknown", strict: true)
54 | }.to raise_error(Strings::Numeral::Error, "not a number: \"unknown\"")
55 | end
56 | end
57 |
--------------------------------------------------------------------------------
/spec/unit/numeralize_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Strings::Numeral, "#numeralize" do
4 | it "converts a number to cardinal numeral by default" do
5 | expect(Strings::Numeral.numeralize(1234.567))
6 | .to eq("one thousand, two hundred thirty four and " \
7 | "five hundred sixty seven thousandths")
8 | end
9 |
10 | it "converts a number to ordinal numeral with a term" do
11 | expect(Strings::Numeral.numeralize(1234.567, term: :ord))
12 | .to eq("one thousand, two hundred thirty fourth and " \
13 | "five hundred sixty seven thousandths")
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/spec/unit/ordinalize_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Strings::Numeral, "#ordinalize" do
4 | {
5 | 0 => "0th",
6 | 11 => "11th",
7 | 21 => "21st",
8 | 113 => "113th",
9 | 123 => "123rd",
10 | 167 => "167th",
11 | 457 => "457th",
12 | -23 => "-23rd",
13 | "125" => "125th",
14 | "12.34" => "12th"
15 | }.each do |num, word|
16 | it "ordinalizes #{num.inspect} to short #{word.inspect}" do
17 | expect(Strings::Numeral.ordinalize(num, short: true)).to eq(word)
18 | end
19 | end
20 |
21 | {
22 | 0 => "zeroth",
23 | 1 => "first",
24 | 20 => "twentieth",
25 | 21 => "twenty first",
26 | 100 => "one hundred",
27 | 101 => "one hundred first",
28 | 111 => "one hundred eleventh",
29 | 113 => "one hundred thirteenth",
30 | 123 => "one hundred twenty third",
31 | 1234 => "one thousand, two hundred thirty fourth",
32 | 12345 => "twelve thousand, three hundred forty fifth",
33 | 123456 => "one hundred twenty three thousand, four hundred fifty sixth",
34 | 1234567 => "one million, two hundred thirty four thousand, five hundred sixty seventh",
35 | -125 => "negative one hundred twenty fifth",
36 | 0.1 => "zeroth and one tenth",
37 | 0.01 => "zeroth and one hundredth",
38 | 0.21 => "zeroth and twenty one hundredths",
39 | 1.23 => "first and twenty three hundredths",
40 | 12.003 => "twelfth and three thousandths",
41 | 0.001 => "zeroth and one thousandth",
42 | 12.100 => "twelfth and one tenth",
43 | 12.000 => "twelfth",
44 | 123.456 => "one hundred twenty third and four hundred fifty six thousandths",
45 | -114.5678 => "negative one hundred fourteenth and five thousand, six hundred seventy eight ten-thousandths",
46 | 1234.567 => "one thousand, two hundred thirty fourth and five hundred sixty seven thousandths",
47 | -3456.07 => "negative three thousand, four hundred fifty sixth and seven hundredths"
48 | }.each do |num, word|
49 | it "ordinalizes #{num.inspect} to #{word.inspect}" do
50 | expect(Strings::Numeral.ordinalize(num)).to eq(word)
51 | end
52 | end
53 |
54 | {
55 | 0.1 => "zeroth point one",
56 | 0.21 => "zeroth point two one",
57 | 1.23 => "first point two three",
58 | 12.003 => "twelfth point zero zero three",
59 | 12.100 => "twelfth point one",
60 | 12.000 => "twelfth",
61 | 123.456 => "one hundred twenty third point four five six",
62 | -114.5678 => "negative one hundred fourteenth point five six seven eight",
63 | 1234.567 => "one thousand, two hundred thirty fourth point five six seven",
64 | -3456.07 => "negative three thousand, four hundred fifty sixth point zero seven"
65 | }.each do |num, word|
66 | it "ordinalizes #{num.inspect} to #{word.inspect}" do
67 | expect(Strings::Numeral.ordinalize(num, decimal: :digit)).to eq(word)
68 | end
69 | end
70 |
71 | it "allows to change a thousand's delimiter" do
72 | expect(Strings::Numeral.ordinalise(1_234_567, delimiter: " and "))
73 | .to eq("one million and two hundred thirty four thousand and five hundred sixty seventh")
74 | end
75 |
76 | it "changes a separator between fractional and integer numerals" do
77 | expect(Strings::Numeral.ordinalize(1_234.567, separator: "dot"))
78 | .to eq("one thousand, two hundred thirty fourth dot five hundred sixty seven thousandths")
79 | end
80 |
81 | it "removes trailing zeros for strings to match number behaviour" do
82 | expect(Strings::Numeral.ordinalize("12.100")).to eq("twelfth and one tenth")
83 | end
84 |
85 | it "keeps trailing zeros for strings when :trailing_zeros is set to true" do
86 | expect(Strings::Numeral.ordinalize("12.100", trailing_zeros: true))
87 | .to eq("twelfth and one hundred thousandths")
88 | end
89 |
90 | it "checks value in strict mode as not a number" do
91 | expect {
92 | Strings::Numeral.ordinalize("unknown", strict: true)
93 | }.to raise_error(Strings::Numeral::Error, "not a number: \"unknown\"")
94 | end
95 | end
96 |
--------------------------------------------------------------------------------
/spec/unit/romanize_spec.rb:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | RSpec.describe Strings::Numeral, "#romanize" do
4 | {
5 | 1 => "I",
6 | 5 => "V",
7 | 9 => "IX",
8 | 15 => "XV",
9 | 155 => "CLV",
10 | 555 => "DLV",
11 | 1111 => "MCXI",
12 | 2020 => "MMXX",
13 | "3456" => "MMMCDLVI",
14 | 4999 => "MMMMCMXCIX"
15 | }.each do |num, word|
16 | it "romanizes #{num.inspect} to #{word.inspect}" do
17 | expect(Strings::Numeral.romanize(num)).to eq(word)
18 | end
19 | end
20 |
21 | it "raises when integer is above the range" do
22 | expect {
23 | Strings::Numeral.romanize(5000)
24 | }.to raise_error(Strings::Numeral::Error, "'5000' is out of range")
25 | end
26 |
27 | it "raises when integer below the range" do
28 | expect {
29 | Strings::Numeral.romanize(0)
30 | }.to raise_error(Strings::Numeral::Error, "'0' is out of range")
31 | end
32 |
33 | it "checks value in strict mode as not a number" do
34 | expect {
35 | Strings::Numeral.romanize("unknown", strict: true)
36 | }.to raise_error(Strings::Numeral::Error, "not a number: \"unknown\"")
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/strings-numeral.gemspec:
--------------------------------------------------------------------------------
1 | # frozen_string_literal: true
2 |
3 | require_relative "lib/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 = ["piotr@piotrmurach.com"]
10 | spec.summary = "Express numbers as word numerals."
11 | spec.description = "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["bug_tracker_uri"] = "https://github.com/piotrmurach/strings-numeral/issues"
18 | spec.metadata["changelog_uri"] = "https://github.com/piotrmurach/strings-numeral/blob/master/CHANGELOG.md"
19 | spec.metadata["documentation_uri"] = "https://www.rubydoc.info/gems/strings-numeral"
20 | spec.metadata["homepage_uri"] = spec.homepage
21 | spec.metadata["source_code_uri"] = "https://github.com/piotrmurach/strings-numeral"
22 | end
23 |
24 | spec.files = Dir["lib/**/*"]
25 | spec.extra_rdoc_files = ["README.md", "CHANGELOG.md", "LICENSE.txt"]
26 | spec.require_paths = ["lib"]
27 | spec.required_ruby_version = ">= 2.0.0"
28 |
29 | spec.add_development_dependency "rake"
30 | spec.add_development_dependency "rspec", ">= 3.0"
31 | end
32 |
--------------------------------------------------------------------------------
/tasks/console.rake:
--------------------------------------------------------------------------------
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 ]
12 |
--------------------------------------------------------------------------------
/tasks/coverage.rake:
--------------------------------------------------------------------------------
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
12 |
--------------------------------------------------------------------------------
/tasks/spec.rake:
--------------------------------------------------------------------------------
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
35 |
--------------------------------------------------------------------------------