├── .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 | strings logo 3 |
4 | 5 | # Strings::Numeral 6 | 7 | [![Gem Version](https://badge.fury.io/rb/strings-numeral.svg)][gem] 8 | [![Actions CI](https://github.com/piotrmurach/strings-numeral/workflows/CI/badge.svg?branch=master)][gh_actions_ci] 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](https://inch-ci.org/github/piotrmurach/strings-numeral.svg?branch=master)][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 | --------------------------------------------------------------------------------