├── .codeclimate.yml ├── .csslintrc ├── .eslintignore ├── .eslintrc ├── .github ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── tests.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── .solargraph.yml ├── .tool-versions ├── CHANGELOG.txt ├── CODE_OF_CONDUCT.md ├── Gemfile ├── Gemfile.lock ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── ruby-units.rb └── ruby_units │ ├── array.rb │ ├── cache.rb │ ├── configuration.rb │ ├── date.rb │ ├── definition.rb │ ├── math.rb │ ├── namespaced.rb │ ├── numeric.rb │ ├── string.rb │ ├── time.rb │ ├── unit.rb │ ├── unit_definitions.rb │ ├── unit_definitions │ ├── base.rb │ ├── prefix.rb │ └── standard.rb │ └── version.rb ├── ruby-units.gemspec └── spec ├── benchmarks └── bigdecimal.rb ├── ruby_units ├── array_spec.rb ├── bugs_spec.rb ├── cache_spec.rb ├── complex_spec.rb ├── configuration_spec.rb ├── date_spec.rb ├── definition_spec.rb ├── math_spec.rb ├── numeric_spec.rb ├── parsing_spec.rb ├── range_spec.rb ├── string_spec.rb ├── subclass_spec.rb ├── temperature_spec.rb ├── time_spec.rb ├── unit_spec.rb └── utf-8 │ └── unit_spec.rb └── spec_helper.rb /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" # required to adjust maintainability checks 2 | checks: 3 | argument-count: 4 | config: 5 | threshold: 4 6 | complex-logic: 7 | config: 8 | threshold: 4 9 | file-lines: 10 | config: 11 | threshold: 250 12 | method-complexity: 13 | config: 14 | threshold: 5 15 | method-count: 16 | config: 17 | threshold: 20 18 | method-lines: 19 | config: 20 | threshold: 25 21 | nested-control-flow: 22 | config: 23 | threshold: 4 24 | return-statements: 25 | config: 26 | threshold: 4 27 | similar-code: 28 | config: 29 | threshold: # language-specific defaults. an override will affect all languages. 30 | identical-code: 31 | config: 32 | threshold: # language-specific defaults. an override will affect all languages. 33 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | --exclude-exts=.min.css 2 | --ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*{.,-}min.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | ecmaFeatures: 2 | modules: true 3 | jsx: true 4 | 5 | env: 6 | amd: true 7 | browser: true 8 | es6: true 9 | jquery: true 10 | node: true 11 | 12 | # http://eslint.org/docs/rules/ 13 | rules: 14 | # Possible Errors 15 | comma-dangle: [2, never] 16 | no-cond-assign: 2 17 | no-console: 0 18 | no-constant-condition: 2 19 | no-control-regex: 2 20 | no-debugger: 2 21 | no-dupe-args: 2 22 | no-dupe-keys: 2 23 | no-duplicate-case: 2 24 | no-empty: 2 25 | no-empty-character-class: 2 26 | no-ex-assign: 2 27 | no-extra-boolean-cast: 2 28 | no-extra-parens: 0 29 | no-extra-semi: 2 30 | no-func-assign: 2 31 | no-inner-declarations: [2, functions] 32 | no-invalid-regexp: 2 33 | no-irregular-whitespace: 2 34 | no-negated-in-lhs: 2 35 | no-obj-calls: 2 36 | no-regex-spaces: 2 37 | no-sparse-arrays: 2 38 | no-unexpected-multiline: 2 39 | no-unreachable: 2 40 | use-isnan: 2 41 | valid-jsdoc: 0 42 | valid-typeof: 2 43 | 44 | # Best Practices 45 | accessor-pairs: 2 46 | block-scoped-var: 0 47 | complexity: [2, 6] 48 | consistent-return: 0 49 | curly: 0 50 | default-case: 0 51 | dot-location: 0 52 | dot-notation: 0 53 | eqeqeq: 2 54 | guard-for-in: 2 55 | no-alert: 2 56 | no-caller: 2 57 | no-case-declarations: 2 58 | no-div-regex: 2 59 | no-else-return: 0 60 | no-empty-label: 2 61 | no-empty-pattern: 2 62 | no-eq-null: 2 63 | no-eval: 2 64 | no-extend-native: 2 65 | no-extra-bind: 2 66 | no-fallthrough: 2 67 | no-floating-decimal: 0 68 | no-implicit-coercion: 0 69 | no-implied-eval: 2 70 | no-invalid-this: 0 71 | no-iterator: 2 72 | no-labels: 0 73 | no-lone-blocks: 2 74 | no-loop-func: 2 75 | no-magic-number: 0 76 | no-multi-spaces: 0 77 | no-multi-str: 0 78 | no-native-reassign: 2 79 | no-new-func: 2 80 | no-new-wrappers: 2 81 | no-new: 2 82 | no-octal-escape: 2 83 | no-octal: 2 84 | no-proto: 2 85 | no-redeclare: 2 86 | no-return-assign: 2 87 | no-script-url: 2 88 | no-self-compare: 2 89 | no-sequences: 0 90 | no-throw-literal: 0 91 | no-unused-expressions: 2 92 | no-useless-call: 2 93 | no-useless-concat: 2 94 | no-void: 2 95 | no-warning-comments: 0 96 | no-with: 2 97 | radix: 2 98 | vars-on-top: 0 99 | wrap-iife: 2 100 | yoda: 0 101 | 102 | # Strict 103 | strict: 0 104 | 105 | # Variables 106 | init-declarations: 0 107 | no-catch-shadow: 2 108 | no-delete-var: 2 109 | no-label-var: 2 110 | no-shadow-restricted-names: 2 111 | no-shadow: 0 112 | no-undef-init: 2 113 | no-undef: 0 114 | no-undefined: 0 115 | no-unused-vars: 0 116 | no-use-before-define: 0 117 | 118 | # Node.js and CommonJS 119 | callback-return: 2 120 | global-require: 2 121 | handle-callback-err: 2 122 | no-mixed-requires: 0 123 | no-new-require: 0 124 | no-path-concat: 2 125 | no-process-exit: 2 126 | no-restricted-modules: 0 127 | no-sync: 0 128 | 129 | # Stylistic Issues 130 | array-bracket-spacing: 0 131 | block-spacing: 0 132 | brace-style: 0 133 | camelcase: 0 134 | comma-spacing: 0 135 | comma-style: 0 136 | computed-property-spacing: 0 137 | consistent-this: 0 138 | eol-last: 0 139 | func-names: 0 140 | func-style: 0 141 | id-length: 0 142 | id-match: 0 143 | indent: 0 144 | jsx-quotes: 0 145 | key-spacing: 0 146 | linebreak-style: 0 147 | lines-around-comment: 0 148 | max-depth: 0 149 | max-len: 0 150 | max-nested-callbacks: 0 151 | max-params: 0 152 | max-statements: [2, 30] 153 | new-cap: 0 154 | new-parens: 0 155 | newline-after-var: 0 156 | no-array-constructor: 0 157 | no-bitwise: 0 158 | no-continue: 0 159 | no-inline-comments: 0 160 | no-lonely-if: 0 161 | no-mixed-spaces-and-tabs: 0 162 | no-multiple-empty-lines: 0 163 | no-negated-condition: 0 164 | no-nested-ternary: 0 165 | no-new-object: 0 166 | no-plusplus: 0 167 | no-restricted-syntax: 0 168 | no-spaced-func: 0 169 | no-ternary: 0 170 | no-trailing-spaces: 0 171 | no-underscore-dangle: 0 172 | no-unneeded-ternary: 0 173 | object-curly-spacing: 0 174 | one-var: 0 175 | operator-assignment: 0 176 | operator-linebreak: 0 177 | padded-blocks: 0 178 | quote-props: 0 179 | quotes: 0 180 | require-jsdoc: 0 181 | semi-spacing: 0 182 | semi: 0 183 | sort-vars: 0 184 | space-after-keywords: 0 185 | space-before-blocks: 0 186 | space-before-function-paren: 0 187 | space-before-keywords: 0 188 | space-in-parens: 0 189 | space-infix-ops: 0 190 | space-return-throw-case: 0 191 | space-unary-ops: 0 192 | spaced-comment: 0 193 | wrap-regex: 0 194 | 195 | # ECMAScript 6 196 | arrow-body-style: 0 197 | arrow-parens: 0 198 | arrow-spacing: 0 199 | constructor-super: 0 200 | generator-star-spacing: 0 201 | no-arrow-condition: 0 202 | no-class-assign: 0 203 | no-const-assign: 0 204 | no-dupe-class-members: 0 205 | no-this-before-super: 0 206 | no-var: 0 207 | object-shorthand: 0 208 | prefer-arrow-callback: 0 209 | prefer-const: 0 210 | prefer-reflect: 0 211 | prefer-spread: 0 212 | prefer-template: 0 213 | require-yield: 0 214 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: bundler 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | ignore: 10 | - dependency-name: solargraph 11 | versions: 12 | - 0.40.2 13 | - 0.40.3 14 | - dependency-name: nokogiri 15 | versions: 16 | - 1.11.1 17 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '15 17 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'ruby' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v2 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v2 71 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | workflow_dispatch: 9 | schedule: 10 | # 00:00 on the 1st of every month 11 | - cron: '0 0 1 * *' 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | ruby: ['3.1', '3.2', '3.3', '3.4', 'jruby-9.4'] 19 | env: 20 | BUNDLE_WITHOUT: optional 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | bundler-cache: true 27 | - name: Tests for Ruby ${{ matrix.ruby }} 28 | run: bundle exec rake 29 | coverage: 30 | needs: test 31 | runs-on: ubuntu-latest 32 | env: 33 | BUNDLE_WITHOUT: optional 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: ruby/setup-ruby@v1 37 | with: 38 | bundler-cache: true 39 | - name: Publish code coverage 40 | uses: paambaati/codeclimate-action@v9 41 | env: 42 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 43 | with: 44 | coverageCommand: bundle exec rake 45 | yard: 46 | runs-on: ubuntu-latest 47 | env: 48 | BUNDLE_WITHOUT: optional 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: ruby/setup-ruby@v1 52 | with: 53 | bundler-cache: true 54 | - name: Build YARD docs 55 | run: bundle exec yard doc --fail-on-warning 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | /bin/ 10 | # rspec failure tracking 11 | .rspec_status 12 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | require: 4 | - rubocop-rake 5 | - rubocop-rspec 6 | 7 | AllCops: 8 | NewCops: enable 9 | Exclude: 10 | - ruby-units.gemspec 11 | - bin/* 12 | Style/StringLiterals: 13 | EnforcedStyle: double_quotes 14 | Layout/LineLength: 15 | Enabled: false 16 | Naming/FileName: 17 | Exclude: 18 | - 'lib/ruby-units.rb' 19 | Naming/MethodParameterName: 20 | Enabled: false 21 | Style/FormatString: 22 | Enabled: false 23 | Metrics/ClassLength: 24 | Enabled: false 25 | Metrics/MethodLength: 26 | Enabled: false 27 | Metrics/CyclomaticComplexity: 28 | Enabled: false 29 | Metrics/PerceivedComplexity: 30 | Enabled: false 31 | Metrics/AbcSize: 32 | Enabled: false 33 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2024-09-01 18:40:26 UTC using RuboCop version 1.66.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | Lint/BinaryOperatorWithIdenticalOperands: 11 | Exclude: 12 | - 'spec/ruby_units/unit_spec.rb' 13 | 14 | # Offense count: 2 15 | # Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers, AllowedPatterns. 16 | # SupportedStyles: snake_case, normalcase, non_integer 17 | # AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339, x86_64 18 | Naming/VariableNumber: 19 | Exclude: 20 | - 'spec/ruby_units/unit_spec.rb' 21 | 22 | # Offense count: 3 23 | # This cop supports unsafe autocorrection (--autocorrect-all). 24 | RSpec/BeEq: 25 | Exclude: 26 | - 'spec/ruby_units/unit_spec.rb' 27 | 28 | # Offense count: 2 29 | RSpec/BeforeAfterAll: 30 | Exclude: 31 | - '**/spec/spec_helper.rb' 32 | - '**/spec/rails_helper.rb' 33 | - '**/spec/support/**/*.rb' 34 | - 'spec/ruby_units/temperature_spec.rb' 35 | 36 | # Offense count: 95 37 | # Configuration parameters: Prefixes, AllowedPatterns. 38 | # Prefixes: when, with, without 39 | RSpec/ContextWording: 40 | Exclude: 41 | - 'spec/ruby_units/temperature_spec.rb' 42 | - 'spec/ruby_units/unit_spec.rb' 43 | - 'spec/ruby_units/utf-8/unit_spec.rb' 44 | 45 | # Offense count: 12 46 | # Configuration parameters: IgnoredMetadata. 47 | RSpec/DescribeClass: 48 | Exclude: 49 | - '**/spec/features/**/*' 50 | - '**/spec/requests/**/*' 51 | - '**/spec/routing/**/*' 52 | - '**/spec/system/**/*' 53 | - '**/spec/views/**/*' 54 | - 'spec/ruby_units/bugs_spec.rb' 55 | - 'spec/ruby_units/definition_spec.rb' 56 | - 'spec/ruby_units/temperature_spec.rb' 57 | - 'spec/ruby_units/unit_spec.rb' 58 | 59 | # Offense count: 1 60 | RSpec/DescribeMethod: 61 | Exclude: 62 | - 'spec/ruby_units/utf-8/unit_spec.rb' 63 | 64 | # Offense count: 22 65 | # This cop supports unsafe autocorrection (--autocorrect-all). 66 | # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. 67 | # SupportedStyles: described_class, explicit 68 | RSpec/DescribedClass: 69 | Exclude: 70 | - 'spec/ruby_units/complex_spec.rb' 71 | - 'spec/ruby_units/unit_spec.rb' 72 | 73 | # Offense count: 8 74 | # Configuration parameters: CountAsOne. 75 | RSpec/ExampleLength: 76 | Max: 7 77 | 78 | # Offense count: 2 79 | # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. 80 | RSpec/IndexedLet: 81 | Exclude: 82 | - 'spec/ruby_units/unit_spec.rb' 83 | 84 | # Offense count: 11 85 | # Configuration parameters: AssignmentOnly. 86 | RSpec/InstanceVariable: 87 | Exclude: 88 | - 'spec/ruby_units/unit_spec.rb' 89 | 90 | # Offense count: 1 91 | RSpec/MultipleDescribes: 92 | Exclude: 93 | - 'spec/ruby_units/unit_spec.rb' 94 | 95 | # Offense count: 10 96 | RSpec/MultipleExpectations: 97 | Max: 6 98 | 99 | # Offense count: 59 100 | # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. 101 | # SupportedStyles: always, named_only 102 | RSpec/NamedSubject: 103 | Exclude: 104 | - 'spec/ruby_units/complex_spec.rb' 105 | - 'spec/ruby_units/range_spec.rb' 106 | - 'spec/ruby_units/unit_spec.rb' 107 | 108 | # Offense count: 50 109 | # Configuration parameters: AllowedGroups. 110 | RSpec/NestedGroups: 111 | Max: 5 112 | 113 | # Offense count: 6 114 | # This cop supports unsafe autocorrection (--autocorrect-all). 115 | # Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers. 116 | # SupportedStyles: inflected, explicit 117 | RSpec/PredicateMatcher: 118 | Exclude: 119 | - 'spec/ruby_units/unit_spec.rb' 120 | 121 | # Offense count: 2 122 | RSpec/RepeatedExample: 123 | Exclude: 124 | - 'spec/ruby_units/unit_spec.rb' 125 | 126 | # Offense count: 8 127 | RSpec/RepeatedExampleGroupBody: 128 | Exclude: 129 | - 'spec/ruby_units/unit_spec.rb' 130 | 131 | # Offense count: 8 132 | RSpec/RepeatedExampleGroupDescription: 133 | Exclude: 134 | - 'spec/ruby_units/unit_spec.rb' 135 | 136 | # Offense count: 1 137 | # Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata. 138 | # Include: **/*_spec.rb 139 | RSpec/SpecFilePathFormat: 140 | Exclude: 141 | - '**/spec/routing/**/*' 142 | - 'spec/ruby_units/utf-8/unit_spec.rb' 143 | 144 | # Offense count: 1 145 | # This cop supports unsafe autocorrection (--autocorrect-all). 146 | # Configuration parameters: EnforcedStyle. 147 | # SupportedStyles: nested, compact 148 | Style/ClassAndModuleChildren: 149 | Exclude: 150 | - 'lib/ruby_units/definition.rb' 151 | 152 | # Offense count: 1 153 | # Configuration parameters: AllowedConstants. 154 | Style/Documentation: 155 | Exclude: 156 | - 'spec/**/*' 157 | - 'test/**/*' 158 | - 'lib/ruby_units/configuration.rb' 159 | 160 | # Offense count: 5 161 | # This cop supports unsafe autocorrection (--autocorrect-all). 162 | Style/GlobalStdStream: 163 | Exclude: 164 | - 'spec/benchmarks/bigdecimal.rb' 165 | - 'spec/ruby_units/unit_spec.rb' 166 | 167 | # Offense count: 1 168 | Style/OpenStructUse: 169 | Exclude: 170 | - 'Guardfile' 171 | 172 | # Offense count: 1 173 | # Configuration parameters: AllowedMethods. 174 | # AllowedMethods: respond_to_missing? 175 | Style/OptionalBooleanParameter: 176 | Exclude: 177 | - 'lib/ruby_units/date.rb' 178 | 179 | # Offense count: 1 180 | # This cop supports unsafe autocorrection (--autocorrect-all). 181 | # Configuration parameters: AllowSend. 182 | Style/SendWithLiteralMethodName: 183 | Exclude: 184 | - 'lib/ruby_units/time.rb' 185 | 186 | # Offense count: 3 187 | # This cop supports unsafe autocorrection (--autocorrect-all). 188 | # Configuration parameters: Mode. 189 | Style/StringConcatenation: 190 | Exclude: 191 | - 'spec/ruby_units/bugs_spec.rb' 192 | - 'spec/ruby_units/unit_spec.rb' 193 | - 'spec/ruby_units/utf-8/unit_spec.rb' 194 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.1.6 2 | -------------------------------------------------------------------------------- /.solargraph.yml: -------------------------------------------------------------------------------- 1 | --- 2 | include: 3 | - "**/*.rb" 4 | exclude: 5 | - spec/**/* 6 | - test/**/* 7 | - vendor/**/* 8 | - ".bundle/**/*" 9 | require: [] 10 | domains: [] 11 | reporters: 12 | - require_not_found 13 | - typecheck 14 | require_paths: [] 15 | plugins: [] 16 | max_files: 5000 17 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | java adoptopenjdk-18.0.1+10 2 | ruby 3.1.6 3 | -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | Change Log for Ruby-units 2 | ========================= 3 | 4 | see GitHub releases (https://github.com/olbrich/ruby-units/releases) for more recent releases. This file will no longer 5 | be updated. 6 | 7 | 2020-12-29 2.3.2 * Remove Jeweler (see #178) also adds Code of Conduct 8 | * Fix specs related to Complex comparisons (see #213) 9 | * Add support for Ruby 3.0 (also drop support for 2.3 and 2.4) (see #211) 10 | 2018-09-26 2.3.1 * Addition and subtraction of unitless values. (see #175) 11 | 2018-03-06 2.3.0 * Fix add (+) and subtract (-) for BigDecimal scalars. (see #167) 12 | * Update ruby versions (#163) 13 | * fix: temperature converting rational issue (#164) 14 | 2017-12-07 2.2.1 * fix an issue with formatting of rational scalars (see #159) 15 | 2017-08-07 2.2.0 * add support for ruby 2.4.1 16 | * drop support for ruby 2.1.0 17 | * remove dependency on mathn (see #157) 18 | 2016-12-28 2.1.0 * add support for ruby 2.4.0 19 | * allow configuration for optional separator on output 20 | * Fix issue #105 -- change 'grad' to 'gon' 21 | 2015-11-07 2.0.0 * remove support for ruby versions less than 2.0 22 | * remove `.unit` and `.u` from String 23 | * remove `.unit` from Date 24 | * Fix an issue with redefining units that have already been cached 25 | * remove 'U()' and 'u()' constructors 26 | * Fix issue #123 -- Fixes for singular unit parsing 27 | 2015-07-16 * Fix issue #129 -- doesn't handle number in the denominator 28 | 2015-05-09 * update test harness to use rspec 3 29 | 2014-02-21 1.4.5 * Fix issue #98 -- add mcg as a valid unit 30 | 2013-07-19 1.4.4 * Fix issue #4 -- .best_prefix method 31 | * Fix issue #60 -- Consider placing Unit in a module 32 | * Fix issue #75 -- Siemens is kind of conductance not resistance 33 | * Fix issue #36 -- Don't require spaces in units 34 | * Fix issue #68 -- ditto 35 | * Fix issue #16 -- ditto 36 | 2013-06-11 1.4.3 * Fix issue #70 -- Support passing Time objects to Time.at 37 | * Fix issue #72 -- Remove non-existent RakeFile from gemspec 38 | * Fix issue #71 -- Fix YAML test failure 39 | * Fix issue #49 -- Unit instances constructed using other Unit instances are incompatible within the framework 40 | * Fix issue #61 -- Fix failing case of subtraction losing Units class 41 | * Fix issue #63 -- fixes an issue with to_date on 1.8.7 42 | * Fix issue #64 -- Aliases aren't considered in Unit.defined? method 43 | 2012-09-16 1.4.2 * Fix issue #54 -- pluralization of fluid-ounces 44 | * Fix issue #53, 51 -- incorrect definition for gee 45 | * Fix issue #52 -- add support for degree symbol 46 | * Fix issue #50 -- fix conversion to json 47 | 2012-05-13 1.4.1 * Fix issue #40 -- Unit parsing truncates invalid portions of the unit 48 | * Fix issue #41 -- initializing with a nil gives unexpected result 49 | 2012-02-01 * Fix issue #34 -- Time.at takes more than one parameter 50 | * Fix issue #35 -- 'pt' is ambiguous 51 | 2012-01-02 1.4.0 * Fix some definitions that were just wrong (amu, dalton) 52 | * Definition uses name of unit if no aliases provided 53 | * Refactor definition process. New units are immediately available 54 | 2011-12-31 * Define standard units in terms of base and other standard units -- more internally consistent 55 | and less prone to round-off errors. 56 | * add 'poundal' 57 | * remove 'wtpercent' 58 | 2011-12-30 * Bump version 59 | * Define compound units with base units for consistency 60 | * distinguish between a league and a nautical league 61 | * NOTE: the new unit definition DSL is not backwardly compatible with the old method 62 | (which is now deprecated). 63 | * Fix issue #27 64 | 2011-12-18 * Can define a display_name for units (fixes #26) 65 | 2011-12-04 * Documentation improvements 66 | * Add DSL for defining/redefining units 67 | 2011-11-24 * improve yard documentation 68 | * add 'tbsp' as an alias for tablespoon 69 | 2011-10-17 1.3.2 * deprecate some string helper functions (make the gem compatible with rails) 70 | * tighten up some time helper functions so they don't make as many assumptions 71 | * time helpers no longer attempt to convert strings to time/date objects 72 | 2011-10-09 * Farads are not a base unit 73 | * CFM added to default units 74 | * multi specs run against ruby-1.9.3 75 | * internally change Unit#to to Unit#convert_to, which is the preferred form 76 | 77 | 2011-04-23 1.3.0.a * Some internal restructuring 78 | * Implement specs for core behaviors 79 | * fixed several bugs found by specs 80 | * implemented a few new methods for completeness 81 | * specs run against 1.8.7, 1.9.2-head, jruby, and rubinius(rbx) using rvm 82 | 1.2.0 * Release 1.2.0 series 83 | 2010-11-07 1.2.0.a * a bunch of fixes to make ruby-units ruby 1.9 compatible 84 | (ruby 1.9.3dev (2010-11-07 trunk 29711) [i386-darwin9.8.0]) 85 | 2010-03-16 1.1.5 * another bugfix, and update url to point to github 86 | 2010-03-15 1.1.4 * fixed a couple of outstanding bugs 87 | 2007-12-13 1.1.3 * fixed a minor bug with string % 88 | 2007-12-12 1.1.2 * fixed a bug with format strings 89 | * detect if ruby 1.8.6 is installed and use its' to_date function 90 | 91 | 2007-07-14 1.1.1 * fixed bug that would prevent creating '' units, which 92 | prevented rounding from working 93 | * tests do not fail if Uncertain gem is not installed, you just get an 94 | annoying warning message 95 | 96 | 2007-01-28 1.1.0 * completely revamped the temperature handling system (see README) 97 | * fixed some spelling errors in some units 98 | * fixed to_datetime and to_date to convert durations to datetimes and dates' 99 | 100 | 2007-01-24 1.0.2 * Minor changes in the way powers are calculated to support Uncertain 101 | numbers better. 102 | * Fixed parsing bug with Uncertain Numbers 103 | * added resolution / typography units (pixels, points, pica) 104 | Note that 'pt' means 'pints' and not 'points' 105 | * added some pressure units ('inHg' & 'inH2O') 106 | * changed default abbreviation of 'knots' to 'kt' 107 | * Changed directory layout 108 | * fixed a minor bug with Time.to_date so comparisons work properly 109 | 110 | 2007-01-17 1.0.1 * Force units are now defined correctly. 111 | 112 | 2007-01-12 1.0.0 * Improved handling of complex numbers. Now you can specify 113 | '1+1i mm'.unit to get a complex unit. 114 | * Taking the root of a negative unit will give you a complex unit 115 | * fixed unary minus to work again 116 | * Math.hypot now takes units. Both parameters must be the compatible 117 | units or it will assert. Units will be converted to a common base 118 | before use. 119 | * Can now specify units in rational numbers, i.e., '1/4 cup'.unit 120 | * Seems like a good time to move to 1.0 status 121 | 122 | 2006-12-15 0.3.9 * forgot to increment the version in the gem file..ooops. 123 | 124 | 2006-12-15 0.3.8 * Any object that supports a 'to_unit' method will now be 125 | automatically coerced to a unit during math operations. 126 | 127 | 2006-12-14 0.3.7 * improved handling of percents and added a 'wt%' unit 128 | equivalent to 1 g/dl. 129 | * Improved handling for units with non-alphanumeric names 130 | (like ' for feet, # for pound) 131 | * Now you can enter durations as "HH:MM:SS, usec" or 132 | "HH:MM:SS:usec" 133 | 134 | 2006-12-05 0.3.6 * Fixed bug where (unit/unit).ceil would fail 135 | 136 | 2006-11-20 0.3.5 * Minor bug fixes 137 | * to_int now coerces the result to an actual Integer, 138 | but only works properly for unitless Units. 139 | 140 | 2006-10-27 0.3.4 * Fixed a few more parsing bugs so that it will properly 141 | complain about malformed units. 142 | * Fixed a bug that prevents proper use of percents 143 | * several minor tweaks 144 | * some improved Date and DateTime handling 145 | * can convert between Date, DateTime, and Time objects 146 | * Time math will now return a DateTime if it goes out of 147 | range. 148 | 149 | 2006-10-03 0.3.3 * Apparently I can't do math late at night. 150 | Fixed a bug that would cause problems when adding 151 | or subtracting units to a unit with a zero scalar. 152 | * Date and DateTime objects can be converted to 'units' 153 | 154 | 2006-10-03 0.3.2 * More minor bug fixes 155 | (now fixes a minor name collision with rails) 156 | 157 | 2006-10-02 0.3.1 * minor bug fixes 158 | 159 | 2006-10-02 0.3.0 * Performance enhanced by caching results of many 160 | functions (Thanks to Kurt Stephens for pushing this.) 161 | * Throws an exception if the unit is not recognized 162 | * units can now identify what 'kind' they are 163 | (:length, :mass, etc..) 164 | * New constructors: 165 | Unit(1,"mm") 166 | Unit(1,"mm/s") 167 | Unit(1,"mm","s") 168 | 169 | 2006-09-22 0.2.3 * added support for date/time parsing with the Chronic gem 170 | parsing will use Chronic if it is loaded 171 | * allows Date / Time / DateTime conversions 172 | * better test coverage 173 | * The 'string'.to_time returns a Time object 174 | * 'string'.to_datetime returns a DateTime object 175 | * 'string'.time returns a Time object or a DateTime if the 176 | Time object fails 177 | * 'string'.datetime returns a DateTime or a Time if the 178 | DateTime fails 179 | 180 | 2006-09-19 0.2.2 * tweaked temperature handling a bit. Now enter 181 | temperatures like this: 182 | '0 tempC'.unit #=> 273.15 degK 183 | They will always be converted to kelvin to avoid 184 | problems when temperatures are used in equations. 185 | * added Time.in("5 min") 186 | * added Unit.to_unit to simplify some calls 187 | 188 | 2006-09-18 0.2.1 * Trig math functions (sin, cos, tan, sinh, cosh, tanh) 189 | accept units that can be converted to radians 190 | Math.sin("90 deg".unit) => 1.0 191 | * Date and DateTime can be offset by a time unit 192 | (Date.today + "1 day".unit) => 2006-09-19 193 | Does not work with months since they aren't a consistent 194 | size 195 | * Tweaked time usage a bit 196 | Time.now + "1 hr".unit => Mon Sep 18 11:51:29 EDT 2006 197 | * can output time in 'hh:mm:ss' format by using 198 | 'unit.to_s(:time)' 199 | * added time helper methods 200 | ago, 201 | since(Time/DateTime), 202 | until(Time/DateTime), 203 | from(Time/DateTime), 204 | before(Time/DateTime), and 205 | after(Time/DateTime) 206 | * Time helpers also work on strings. In this case they 207 | are first converted to units 208 | '5 min'.from_now 209 | '1 week'.ago 210 | 'min'.since(time) 211 | 'min'.until(time) 212 | '1 day'.from() 213 | * Can pass Strings to time helpers and they will be parsed 214 | with ParseDate 215 | * Fixed most parsing bugs (I think) 216 | * Can pass a strftime format string to to_s to format time 217 | output 218 | * can use U'1 mm' or '1 mm'.u to specify units now 219 | 220 | 2006-09-17 * can now use the '%' format specifier like 221 | '%0.2f' % '1 mm'.unit #=> '1.00 mm' 222 | * works nicely with time now. 223 | '1 week'.unit + Time.now => 1.159e+09 s 224 | Time.at('1.159e+09 s'.unit) 225 | => Sat Sep 23 04:26:40 EDT 2006 226 | "1.159e9 s".unit.time 227 | => Sat Sep 23 04:26:40 EDT 2006 228 | * Time.now.unit => 1.159e9 s 229 | * works well with 'Uncertain' numerics 230 | (www.rubyforge.org/projects/uncertain) 231 | * Improved parsing 232 | 233 | 2006-08-28 0.2.0 * Added 'ruby_unit.rb' file so that requires will still 234 | work if the wrong name is used 235 | * Added 'to' as an alias to '>>' so conversions can be 236 | done as '1 m'.unit.to('1 cm') 237 | * Added ability to convert temperatures to absolute values 238 | using the following syntax: 239 | '37 degC'.unit.to('tempF') #=> '98.6 degF'.unit 240 | * Tweaked abbreviations a bit. 'ton' is now 'tn' instead 241 | of 't'. It was causing parse collisions with 'atm'. 242 | * fixed a bug in term elimination routine 243 | * fixed a bug in parsing of powers, and added support for 244 | 'm**2' format 245 | * Added support for taking roots of units. Just 246 | exponentiate with a fraction (0.5, 1.0/3, 0.25) 247 | * renamed 'quantity' to 'scalar' 248 | * any type of Numeric can be used to initialize a Unit, 249 | although this can't really be done with a string 250 | * Units can not be forced to a float using to_f unless 251 | they are unitless. This prevents some math functions 252 | from forcing the conversion. To get the scalar, just 253 | use 'unit.scalar' 254 | * 'inspect' returns string representation 255 | * better edge-case detection with math functions. 256 | "0 mm".unit**-1 now throws a ZeroDivisionError exception 257 | * Ranges can make a series of units, so long as the end 258 | points have integer scalars. 259 | * Fixed a parsing bug with feet/pounds and scientific 260 | numbers 261 | 262 | 2006-08-22 0.1.1 * Added new format option "1 mm".to_unit("in") now 263 | converts the result to the indicated units 264 | * Fixed some naming issues so that the gem name matches 265 | the require name. 266 | * Added CHANGELOG 267 | * Improved test coverage (100% code coverage via RCov) 268 | * fixed a bug that prevented units with a prefix in the 269 | denominator from converting properly 270 | * can use .unit method on a string to create a new unit 271 | object 272 | * can now coerce or define units from arrays, strings, 273 | numerics. 274 | "1 mm".unit + [1, 'mm'] === "2 mm".unit 275 | [1,'mm','s'].unit === "1 mm/s".unit 276 | 2.5.unit === "2.5".unit 277 | * Added instructions on how to add custom units 278 | 279 | 2006-08-22 0.1.0 * Initial Release 280 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at kevin.olbrich@mckesson.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source "https://rubygems.org" 4 | 5 | # These are gems that are only used for local development and don't need to be included at runtime or for running tests. 6 | # The CI process will not install them. 7 | group :optional do 8 | gem "debug", ">= 1.0.0", platform: :mri 9 | gem "gem-ctags" 10 | gem "guard-rspec" 11 | gem "pry" 12 | gem "redcarpet", platform: :mri # redcarpet doesn't support jruby 13 | gem "rubocop" 14 | gem "rubocop-rake" 15 | gem "rubocop-rspec" 16 | gem "ruby-maven", platform: :jruby 17 | gem "ruby-prof", platform: :mri 18 | gem "simplecov-html" 19 | gem "solargraph" 20 | gem "terminal-notifier" 21 | gem "terminal-notifier-guard" 22 | gem "webrick" 23 | end 24 | 25 | gem "bigdecimal" 26 | gem "rake" 27 | gem "rspec", "~> 3.0" 28 | gem "simplecov" 29 | gem "yard" 30 | 31 | gemspec 32 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ruby-units (4.1.0) 5 | 6 | GEM 7 | remote: https://rubygems.org/ 8 | specs: 9 | ast (2.4.2) 10 | backport (1.2.0) 11 | benchmark (0.4.0) 12 | bigdecimal (3.1.9) 13 | bigdecimal (3.1.9-java) 14 | coderay (1.1.3) 15 | date (3.4.1) 16 | date (3.4.1-java) 17 | debug (1.10.0) 18 | irb (~> 1.10) 19 | reline (>= 0.3.8) 20 | diff-lcs (1.5.1) 21 | docile (1.4.1) 22 | e2mmap (0.1.0) 23 | ffi (1.17.0-arm64-darwin) 24 | ffi (1.17.0-java) 25 | ffi (1.17.0-x86_64-darwin) 26 | ffi (1.17.0-x86_64-linux-gnu) 27 | formatador (1.1.0) 28 | gem-ctags (1.0.9) 29 | guard (2.19.0) 30 | formatador (>= 0.2.4) 31 | listen (>= 2.7, < 4.0) 32 | lumberjack (>= 1.0.12, < 2.0) 33 | nenv (~> 0.1) 34 | notiffany (~> 0.0) 35 | pry (>= 0.13.0) 36 | shellany (~> 0.0) 37 | thor (>= 0.18.1) 38 | guard-compat (1.2.1) 39 | guard-rspec (4.7.3) 40 | guard (~> 2.1) 41 | guard-compat (~> 1.1) 42 | rspec (>= 2.99.0, < 4.0) 43 | io-console (0.8.0) 44 | io-console (0.8.0-java) 45 | irb (1.14.3) 46 | rdoc (>= 4.0.0) 47 | reline (>= 0.4.2) 48 | jar-dependencies (0.5.1) 49 | jaro_winkler (1.6.0) 50 | jaro_winkler (1.6.0-java) 51 | json (2.9.1) 52 | json (2.9.1-java) 53 | kramdown (2.5.1) 54 | rexml (>= 3.3.9) 55 | kramdown-parser-gfm (1.1.0) 56 | kramdown (~> 2.0) 57 | language_server-protocol (3.17.0.3) 58 | listen (3.9.0) 59 | rb-fsevent (~> 0.10, >= 0.10.3) 60 | rb-inotify (~> 0.9, >= 0.9.10) 61 | lumberjack (1.2.10) 62 | method_source (1.1.0) 63 | nenv (0.3.0) 64 | nokogiri (1.18.0-arm64-darwin) 65 | racc (~> 1.4) 66 | nokogiri (1.18.0-java) 67 | racc (~> 1.4) 68 | nokogiri (1.18.0-x86_64-darwin) 69 | racc (~> 1.4) 70 | nokogiri (1.18.0-x86_64-linux-gnu) 71 | racc (~> 1.4) 72 | notiffany (0.1.3) 73 | nenv (~> 0.1) 74 | shellany (~> 0.0) 75 | parallel (1.26.3) 76 | parser (3.3.6.0) 77 | ast (~> 2.4.1) 78 | racc 79 | pry (0.15.2) 80 | coderay (~> 1.1) 81 | method_source (~> 1.0) 82 | pry (0.15.2-java) 83 | coderay (~> 1.1) 84 | method_source (~> 1.0) 85 | spoon (~> 0.0) 86 | psych (5.2.2) 87 | date 88 | stringio 89 | psych (5.2.2-java) 90 | date 91 | jar-dependencies (>= 0.1.7) 92 | racc (1.8.1) 93 | racc (1.8.1-java) 94 | rainbow (3.1.1) 95 | rake (13.2.1) 96 | rb-fsevent (0.11.2) 97 | rb-inotify (0.11.1) 98 | ffi (~> 1.0) 99 | rbs (2.8.4) 100 | rdoc (6.10.0) 101 | psych (>= 4.0.0) 102 | redcarpet (3.6.0) 103 | regexp_parser (2.10.0) 104 | reline (0.6.0) 105 | io-console (~> 0.5) 106 | reverse_markdown (2.1.1) 107 | nokogiri 108 | rexml (3.4.0) 109 | rspec (3.13.0) 110 | rspec-core (~> 3.13.0) 111 | rspec-expectations (~> 3.13.0) 112 | rspec-mocks (~> 3.13.0) 113 | rspec-core (3.13.2) 114 | rspec-support (~> 3.13.0) 115 | rspec-expectations (3.13.3) 116 | diff-lcs (>= 1.2.0, < 2.0) 117 | rspec-support (~> 3.13.0) 118 | rspec-mocks (3.13.2) 119 | diff-lcs (>= 1.2.0, < 2.0) 120 | rspec-support (~> 3.13.0) 121 | rspec-support (3.13.2) 122 | rubocop (1.69.2) 123 | json (~> 2.3) 124 | language_server-protocol (>= 3.17.0) 125 | parallel (~> 1.10) 126 | parser (>= 3.3.0.2) 127 | rainbow (>= 2.2.2, < 4.0) 128 | regexp_parser (>= 2.9.3, < 3.0) 129 | rubocop-ast (>= 1.36.2, < 2.0) 130 | ruby-progressbar (~> 1.7) 131 | unicode-display_width (>= 2.4.0, < 4.0) 132 | rubocop-ast (1.37.0) 133 | parser (>= 3.3.1.0) 134 | rubocop-rake (0.6.0) 135 | rubocop (~> 1.0) 136 | rubocop-rspec (3.3.0) 137 | rubocop (~> 1.61) 138 | ruby-maven (3.9.3) 139 | ruby-maven-libs (~> 3.9.9) 140 | ruby-maven-libs (3.9.9) 141 | ruby-prof (1.7.1) 142 | ruby-progressbar (1.13.0) 143 | shellany (0.0.1) 144 | simplecov (0.22.0) 145 | docile (~> 1.1) 146 | simplecov-html (~> 0.11) 147 | simplecov_json_formatter (~> 0.1) 148 | simplecov-html (0.13.1) 149 | simplecov_json_formatter (0.1.4) 150 | solargraph (0.50.0) 151 | backport (~> 1.2) 152 | benchmark 153 | bundler (~> 2.0) 154 | diff-lcs (~> 1.4) 155 | e2mmap 156 | jaro_winkler (~> 1.5) 157 | kramdown (~> 2.3) 158 | kramdown-parser-gfm (~> 1.1) 159 | parser (~> 3.0) 160 | rbs (~> 2.0) 161 | reverse_markdown (~> 2.0) 162 | rubocop (~> 1.38) 163 | thor (~> 1.0) 164 | tilt (~> 2.0) 165 | yard (~> 0.9, >= 0.9.24) 166 | spoon (0.0.6) 167 | ffi 168 | stringio (3.1.2) 169 | terminal-notifier (2.0.0) 170 | terminal-notifier-guard (1.7.0) 171 | thor (1.3.2) 172 | tilt (2.5.0) 173 | unicode-display_width (3.1.3) 174 | unicode-emoji (~> 4.0, >= 4.0.4) 175 | unicode-emoji (4.0.4) 176 | webrick (1.9.1) 177 | yard (0.9.37) 178 | 179 | PLATFORMS 180 | arm64-darwin-21 181 | arm64-darwin-22 182 | arm64-darwin-23 183 | arm64-darwin-24 184 | java 185 | universal-java-11 186 | universal-java-18 187 | x86_64-darwin-19 188 | x86_64-linux 189 | 190 | DEPENDENCIES 191 | bigdecimal 192 | debug (>= 1.0.0) 193 | gem-ctags 194 | guard-rspec 195 | pry 196 | rake 197 | redcarpet 198 | rspec (~> 3.0) 199 | rubocop 200 | rubocop-rake 201 | rubocop-rspec 202 | ruby-maven 203 | ruby-prof 204 | ruby-units! 205 | simplecov 206 | simplecov-html 207 | solargraph 208 | terminal-notifier 209 | terminal-notifier-guard 210 | webrick 211 | yard 212 | 213 | BUNDLED WITH 214 | 2.6.2 215 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # A sample Guardfile 4 | # More info at https://github.com/guard/guard#readme 5 | 6 | ## Uncomment and set this to only include directories you want to watch 7 | directories %w[lib spec] 8 | 9 | ## Uncomment to clear the screen before every task 10 | clearing :on 11 | 12 | # NOTE: The cmd option is now required due to the increasing number of ways 13 | # rspec may be run, below are examples of the most common uses. 14 | # * bundler: 'bundle exec rspec' 15 | # * bundler binstubs: 'bin/rspec' 16 | # * spring: 'bin/rspec' (This will use spring if running and you have 17 | # installed the spring binstubs per the docs) 18 | # * zeus: 'zeus rspec' (requires the server to be started separately) 19 | # * 'just' rspec: 'rspec' 20 | 21 | guard :rspec, cmd: "bundle exec rspec" do 22 | require "ostruct" 23 | 24 | # Generic Ruby apps 25 | rspec = OpenStruct.new 26 | rspec.spec = ->(m) { "spec/#{m}_spec.rb" } 27 | rspec.spec_dir = "spec" 28 | rspec.spec_helper = "spec/spec_helper.rb" 29 | 30 | watch(%r{^spec/.+_spec\.rb$}) 31 | watch(%r{^lib/(.+)\.rb$}) { |m| rspec.spec.call(m[1]) } 32 | watch(rspec.spec_helper) { rspec.spec_dir } 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Kevin Olbrich 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 | # Ruby Units 2 | 3 | [![Maintainability](https://api.codeclimate.com/v1/badges/4e858d14a07dd453f748/maintainability.svg)](https://codeclimate.com/github/olbrich/ruby-units/maintainability) 4 | [![CodeClimate Status](https://api.codeclimate.com/v1/badges/4e858d14a07dd453f748/test_coverage.svg)](https://codeclimate.com/github/olbrich/ruby-units/test_coverage) 5 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Folbrich%2Fruby-units.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Folbrich%2Fruby-units?ref=badge_shield) 6 | 7 | Kevin C. Olbrich, Ph.D. 8 | 9 | Project page: 10 | [http://github.com/olbrich/ruby-units](http://github.com/olbrich/ruby-units) 11 | 12 | ## Introduction 13 | 14 | Many technical applications make use of specialized calculations at some point. 15 | Frequently, these calculations require unit conversions to ensure accurate 16 | results. Needless to say, this is a pain to properly keep track of, and is prone 17 | to numerous errors. 18 | 19 | ## Solution 20 | 21 | The 'Ruby units' gem is designed to simplify the handling of units for 22 | scientific calculations. The units of each quantity are specified when a Unit 23 | object is created and the Unit class will handle all subsequent conversions and 24 | manipulations to ensure an accurate result. 25 | 26 | ## Installation 27 | 28 | This package may be installed using: 29 | 30 | ```bash 31 | gem install ruby-units 32 | ``` 33 | 34 | or add this to your `Gemfile` 35 | 36 | ```ruby 37 | gem 'ruby-units' 38 | ``` 39 | 40 | ## Usage 41 | 42 | ```ruby 43 | unit = Unit.new("1") # constant only 44 | unit = Unit.new("mm") # unit only (defaults to a scalar of 1) 45 | unit = Unit.new("1 mm") # create a simple unit 46 | unit = Unit.new("1 mm/s") # a compound unit 47 | unit = Unit.new("1 mm s^-1") # in exponent notation 48 | unit = Unit.new("1 kg*m^2/s^2") # complex unit 49 | unit = Unit.new("1 kg m^2 s^-2") # complex unit 50 | unit = Unit.new("1 mm") # shorthand 51 | unit = "1 mm".to_unit # convert string object 52 | unit = object.to_unit # convert any object using object.to_s 53 | unit = Unit.new('1/4 cup') # Rational number 54 | unit = Unit.new('1+1i mm') # Complex Number 55 | ``` 56 | 57 | ### Rules 58 | 59 | 1. only 1 quantity per unit (with 2 exceptions... 6'5" and '8 lbs 8 oz') 60 | 2. use SI notation when possible 61 | 3. spaces in units are allowed, but ones like '11/m' will be recognized as '11 62 | 1/m'. 63 | 64 | ### Unit compatibility 65 | 66 | Many methods require that the units of two operands are compatible. Compatible 67 | units are those that can be easily converted into each other, such as 'meters' 68 | and 'feet'. 69 | 70 | ```ruby 71 | unit1 =~ unit2 #=> true if units are compatible 72 | unit1.compatible?(unit2) #=> true if units are compatible 73 | ``` 74 | 75 | ### Unit Math 76 | 77 | ```text 78 | Unit#+() # Add. only works if units are compatible 79 | Unit#-() # Subtract. only works if units are compatible 80 | Unit#*() # Multiply. 81 | Unit#/() # Divide. 82 | Unit#**() # Exponentiate. Exponent must be an integer, can be positive, negative, or zero 83 | Unit#inverse # Returns 1/unit 84 | Unit#abs # Returns absolute value of the unit quantity. Strips off the units 85 | Unit#ceil # rounds quantity to next highest integer 86 | Unit#floor # rounds quantity down to next lower integer 87 | Unit#round # rounds quantity to nearest integer 88 | Unit#to_int # returns the quantity as an integer 89 | ``` 90 | 91 | Unit will coerce other objects into a Unit if used in a formula. This means... 92 | 93 | ```ruby 94 | Unit.new("1 mm") + "2 mm" == Unit.new("3 mm") 95 | ``` 96 | 97 | This will work as expected so long as you start the formula with a `Unit` 98 | object. 99 | 100 | ### Conversions & Comparisons 101 | 102 | Units can be converted to other units in a couple of ways. 103 | 104 | ```ruby 105 | unit.convert_to('ft') # convert 106 | unit1 = unit >> "ft" # convert to 'feet' 107 | unit >>= "ft" # convert and overwrite original object 108 | unit3 = unit1 + unit2 # resulting object will have the units of unit1 109 | unit3 = unit1 - unit2 # resulting object will have the units of unit1 110 | unit1 <=> unit2 # does comparison on quantities in base units, throws an exception if not compatible 111 | unit1 === unit2 # true if units and quantity are the same, even if 'equivalent' by <=> 112 | unit1 + unit2 >> "ft" # converts result of math to 'ft' 113 | (unit1 + unit2).convert_to('ft') # converts result to 'ft' 114 | ``` 115 | 116 | Any object that defines a `to_unit` method will be automatically coerced to a 117 | unit during calculations. 118 | 119 | ### Text Output 120 | 121 | Units will display themselves nicely based on the display_name for the units and 122 | prefixes. Since `Unit` implements a `Unit#to_s`, all that is needed in most 123 | cases is: 124 | 125 | ```ruby 126 | "#{Unit.new('1 mm')}" #=> "1 mm" 127 | ``` 128 | 129 | The `to_s` also accepts some options. 130 | 131 | ```ruby 132 | Unit.new('1.5 mm').to_s("%0.2f") # "1.50 mm". Enter any valid format 133 | # string. Also accepts strftime format 134 | Unit.new('10 mm').to_s("%0.2f in")# "0.39 in". can also format and convert in 135 | # the same time. 136 | Unit.new('1.5 mm').to_s("in") # converts to inches before printing 137 | Unit.new("2 m").to_s(:ft) # returns 6'7" 138 | Unit.new("100 kg").to_s(:lbs) # returns 220 lbs, 7 oz 139 | Unit.new("100 kg").to_s(:stone) # returns 15 stone, 10 lb 140 | ``` 141 | 142 | ### Time Helpers 143 | 144 | `Time`, `Date`, and `DateTime` objects can have time units added or subtracted. 145 | 146 | ```ruby 147 | Time.now + Unit.new("10 min") 148 | ``` 149 | 150 | Several helpers have also been defined. Note: If you include the 'Chronic' gem, 151 | you can specify times in natural language. 152 | 153 | ```ruby 154 | Unit.new('min').since(DateTime.parse('9/18/06 3:00pm')) 155 | ``` 156 | 157 | Durations may be entered as 'HH:MM:SS, usec' and will be returned in 'hours'. 158 | 159 | ```ruby 160 | Unit.new('1:00') #=> 1 h 161 | Unit.new('0:30') #=> 0.5 h 162 | Unit.new('0:30:30') #=> 0.5 h + 30 sec 163 | ``` 164 | 165 | If only one ":" is present, it is interpreted as the separator between hours and 166 | minutes. 167 | 168 | ### Ranges 169 | 170 | ```ruby 171 | [Unit.new('0 h')..Unit.new('10 h')].each {|x| p x} 172 | ``` 173 | 174 | works so long as the starting point has an integer scalar 175 | 176 | ### Math functions 177 | 178 | All Trig math functions (sin, cos, sinh, hypot...) can take a unit as their 179 | parameter. It will be converted to radians and then used if possible. 180 | 181 | ### Temperatures 182 | 183 | Ruby-units makes a distinction between a temperature (which technically is a 184 | property) and degrees of temperature (which temperatures are measured in). 185 | 186 | Temperature units (i.e., 'tempK') can be converted back and forth, and will take 187 | into account the differences in the zero points of the various scales. 188 | Differential temperature (e.g., Unit.new('100 degC')) units behave like most 189 | other units. 190 | 191 | ```ruby 192 | Unit.new('37 tempC').convert_to('tempF') #=> 98.6 tempF 193 | ``` 194 | 195 | Ruby-units will raise an exception if you attempt to create a temperature unit 196 | that would fall below absolute zero. 197 | 198 | Unit math on temperatures is fairly limited. 199 | 200 | ```ruby 201 | Unit.new('100 tempC') + Unit.new('10 degC') # '110 tempC'.to_unit 202 | Unit.new('100 tempC') - Unit.new('10 degC') # '90 tempC'.to_unit 203 | Unit.new('100 tempC') + Unit.new('50 tempC') # exception (can't add two temperatures) 204 | Unit.new('100 tempC') - Unit.new('50 tempC') # '50 degC'.to_unit (get the difference between two temperatures) 205 | Unit.new('50 tempC') - Unit.new('100 tempC') # '-50 degC'.to_unit 206 | Unit.new('100 tempC') * scalar # '100*scalar tempC'.to_unit 207 | Unit.new('100 tempC') / scalar # '100/scalar tempC'.to_unit 208 | Unit.new('100 tempC') * unit # exception 209 | Unit.new('100 tempC') / unit # exception 210 | Unit.new('100 tempC') ** N # exception 211 | 212 | Unit.new('100 tempC').convert_to('degC') #=> Unit.new('100 degC') 213 | ``` 214 | 215 | This conversion references the 0 point on the scale of the temperature unit 216 | 217 | ```ruby 218 | Unit.new('100 degC').convert_to('tempC') #=> '-173 tempC'.to_unit 219 | ``` 220 | 221 | These conversions are always interpreted as being relative to absolute zero. 222 | Conversions are probably better done like this... 223 | 224 | ```ruby 225 | Unit.new('0 tempC') + Unit.new('100 degC') #=> Unit.new('100 tempC') 226 | ``` 227 | 228 | ### Defining Units 229 | 230 | It is possible to define new units or redefine existing ones. 231 | 232 | #### Define New Unit 233 | 234 | The easiest approach is to define a unit in terms of other units. 235 | 236 | ```ruby 237 | Unit.define("foobar") do |foobar| 238 | foobar.definition = Unit.new("1 foo") * Unit.new("1 bar") # anything that results in a Unit object 239 | foobar.aliases = %w{foobar fb} # array of synonyms for the unit 240 | foobar.display_name = "Foobar" # How unit is displayed when output 241 | end 242 | ``` 243 | 244 | #### Redefine Existing Unit 245 | 246 | Redefining a unit allows the user to change a single aspect of a definition 247 | without having to re-create the entire definition. This is useful for changing 248 | display names, adding aliases, etc. 249 | 250 | ```ruby 251 | Unit.redefine!("cup") do |cup| 252 | cup.display_name = "cup" 253 | end 254 | ``` 255 | 256 | ### Useful methods 257 | 258 | 1. `scalar` will return the numeric portion of the unit without the attached 259 | units 260 | 2. `base_scalar` will return the scalar in base units (SI) 261 | 3. `units` will return the name of the units (without the scalar) 262 | 4. `base` will return the unit converted to base units (SI) 263 | 264 | ### Storing in a database 265 | 266 | Units can be stored in a database as either the string representation or in two 267 | separate columns defining the scalar and the units. Note that if sorting by 268 | units is desired you will want to ensure that you are storing the scalars in a 269 | consistent unit (i.e, the base units). 270 | 271 | ### Namespaced Class 272 | 273 | Sometimes the default class 'Unit' may conflict with other gems or applications. 274 | Internally ruby-units defines itself using the RubyUnits namespace. The actual 275 | class of a unit is the RubyUnits::Unit. For simplicity and backwards 276 | compatibility, the `::Unit` class is defined as an alias to `::RubyUnits::Unit`. 277 | 278 | To load ruby-units without this alias... 279 | 280 | ```ruby 281 | require 'ruby_units/namespaced' 282 | ``` 283 | 284 | When using bundler... 285 | 286 | ```ruby 287 | gem 'ruby-units', require: 'ruby_units/namespaced' 288 | ``` 289 | 290 | Note: when using the namespaced version, the `Unit.new('unit string')` helper 291 | will not be defined. 292 | 293 | ### Configuration 294 | 295 | Configuration options can be set like: 296 | 297 | ```ruby 298 | RubyUnits.configure do |config| 299 | config.format = :rational 300 | config.separator = false 301 | end 302 | ``` 303 | 304 | | Option | Description | Valid Values | Default | 305 | |-----------|------------------------------------------------------------------------------------------------------------------------|---------------------------|-------------| 306 | | format | Only used for output formatting. `:rational` is formatted like `3 m/s^2`. `:exponential` is formatted like `3 m*s^-2`. | `:rational, :exponential` | `:rational` | 307 | | separator | Use a space separator for output. `true` is formatted like `3 m/s`, `false` is like `3m/s`. | `true, false` | `true` | 308 | 309 | ### NOTES 310 | 311 | #### Performance vs. Accuracy 312 | 313 | Ruby units was originally intended to provide a robust and accurate way to do 314 | arbitrary unit conversions. In some cases, these conversions can result in the 315 | creation and garbage collection of a lot of intermediate objects during 316 | calculations. This in turn can have a negative impact on performance. The design 317 | of ruby-units has emphasized accuracy over speed. YMMV if you are doing a lot of 318 | math involving units. 319 | 320 | ## Support Policy 321 | 322 | Only currently maintained versions of ruby and jruby are supported. 323 | 324 | ## License 325 | 326 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Folbrich%2Fruby-units.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2Folbrich%2Fruby-units?ref=badge_large) 327 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bundler/gem_tasks" 4 | require "rspec/core/rake_task" 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | task default: :spec 9 | -------------------------------------------------------------------------------- /lib/ruby-units.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "ruby_units/namespaced" 4 | Unit = RubyUnits::Unit 5 | -------------------------------------------------------------------------------- /lib/ruby_units/array.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyUnits 4 | # Extra methods for [::Array] to support conversion to [RubyUnits::Unit] 5 | module Array 6 | # Construct a unit from an array 7 | # 8 | # @example [1, 'mm'].to_unit => RubyUnits::Unit.new("1 mm") 9 | # @param [RubyUnits::Unit, String] other convert to same units as passed 10 | # @return [RubyUnits::Unit] 11 | def to_unit(other = nil) 12 | other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) 13 | end 14 | end 15 | end 16 | 17 | # @note Do this instead of Array.prepend(RubyUnits::Array) to avoid YARD warnings 18 | # @see https://github.com/lsegal/yard/issues/1353 19 | class Array 20 | prepend(RubyUnits::Array) 21 | end 22 | -------------------------------------------------------------------------------- /lib/ruby_units/cache.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyUnits 4 | # Performance optimizations to avoid creating units unnecessarily 5 | class Cache 6 | attr_accessor :data 7 | 8 | def initialize 9 | clear 10 | end 11 | 12 | # @param key [String, #to_unit] 13 | # @return [RubyUnits::Unit, nil] 14 | def get(key) 15 | key = key&.to_unit&.units unless key.is_a?(String) 16 | data[key] 17 | end 18 | 19 | # @param key [String, #to_unit] 20 | # @return [void] 21 | def set(key, value) 22 | key = key.to_unit.units unless key.is_a?(String) 23 | data[key] = value 24 | end 25 | 26 | # @return [Array] 27 | def keys 28 | data.keys 29 | end 30 | 31 | # Reset the cache 32 | def clear 33 | @data = {} 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/ruby_units/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyUnits 4 | class << self 5 | attr_writer :configuration 6 | end 7 | 8 | def self.configuration 9 | @configuration ||= Configuration.new 10 | end 11 | 12 | # Reset the configuration to the default values 13 | def self.reset 14 | @configuration = Configuration.new 15 | end 16 | 17 | # allow for optional configuration of RubyUnits 18 | # 19 | # Usage: 20 | # 21 | # RubyUnits.configure do |config| 22 | # config.separator = false 23 | # end 24 | def self.configure 25 | yield configuration 26 | end 27 | 28 | # holds actual configuration values for RubyUnits 29 | class Configuration 30 | # Used to separate the scalar from the unit when generating output. A value 31 | # of `true` will insert a single space, and `false` will prevent adding a 32 | # space to the string representation of a unit. 33 | # 34 | # @!attribute [rw] separator 35 | # @return [Boolean] whether to include a space between the scalar and the unit 36 | attr_reader :separator 37 | 38 | # The style of format to use by default when generating output. When set to `:exponential`, all units will be 39 | # represented in exponential notation instead of using a numerator and denominator. 40 | # 41 | # @!attribute [rw] format 42 | # @return [Symbol] the format to use when generating output (:rational or :exponential) (default: :rational) 43 | attr_reader :format 44 | 45 | def initialize 46 | self.format = :rational 47 | self.separator = true 48 | end 49 | 50 | # Use a space for the separator to use when generating output. 51 | # 52 | # @param value [Boolean] whether to include a space between the scalar and the unit 53 | # @return [void] 54 | def separator=(value) 55 | raise ArgumentError, "configuration 'separator' may only be true or false" unless [true, false].include?(value) 56 | 57 | @separator = value ? " " : nil 58 | end 59 | 60 | # Set the format to use when generating output. 61 | # The `:rational` style will generate units string like `3 m/s^2` and the `:exponential` style will generate units 62 | # like `3 m*s^-2`. 63 | # 64 | # @param value [Symbol] the format to use when generating output (:rational or :exponential) 65 | # @return [void] 66 | def format=(value) 67 | raise ArgumentError, "configuration 'format' may only be :rational or :exponential" unless %i[rational exponential].include?(value) 68 | 69 | @format = value 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/ruby_units/date.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | 5 | module RubyUnits 6 | # Extra methods for [::Date] to allow it to be used as a [RubyUnits::Unit] 7 | module Date 8 | # Allow date objects to do offsets by a time unit 9 | # 10 | # @example Date.today + Unit.new("1 week") => gives today+1 week 11 | # @param [RubyUnits::Unit, Object] other 12 | # @return [RubyUnits::Unit] 13 | def +(other) 14 | case other 15 | when RubyUnits::Unit 16 | other = other.convert_to("d").round if %w[y decade century].include? other.units 17 | super(other.convert_to("day").scalar) 18 | else 19 | super 20 | end 21 | end 22 | 23 | # Allow date objects to do offsets by a time unit 24 | # 25 | # @example Date.today - Unit.new("1 week") => gives today-1 week 26 | # @param [RubyUnits::Unit, Object] other 27 | # @return [RubyUnits::Unit] 28 | def -(other) 29 | case other 30 | when RubyUnits::Unit 31 | other = other.convert_to("d").round if %w[y decade century].include? other.units 32 | super(other.convert_to("day").scalar) 33 | else 34 | super 35 | end 36 | end 37 | 38 | # Construct a unit from a Date. This returns the number of days since the 39 | # start of the Julian calendar as a Unit. 40 | # 41 | # @example Date.today.to_unit => Unit 42 | # @return [RubyUnits::Unit] 43 | # @param other [RubyUnits::Unit, String] convert to same units as passed 44 | def to_unit(other = nil) 45 | other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) 46 | end 47 | 48 | # @deprecated 49 | def inspect(dump = false) 50 | dump ? super : to_s 51 | end 52 | end 53 | end 54 | 55 | # @note Do this instead of Date.prepend(RubyUnits::Date) to avoid YARD warnings 56 | # @see https://github.com/lsegal/yard/issues/1353 57 | class Date 58 | prepend RubyUnits::Date 59 | end 60 | -------------------------------------------------------------------------------- /lib/ruby_units/definition.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class RubyUnits::Unit < Numeric 4 | # Handle the definition of units 5 | class Definition 6 | # @return [Array] 7 | attr_writer :aliases 8 | 9 | # @return [Symbol] 10 | attr_accessor :kind 11 | 12 | # @return [Numeric] 13 | attr_accessor :scalar 14 | 15 | # @return [Array] 16 | attr_accessor :numerator 17 | 18 | # @return [Array] 19 | attr_accessor :denominator 20 | 21 | # Unit name to be used when generating output. This MUST be a parseable 22 | # string or it won't be possible to round trip the unit to a String and 23 | # back. 24 | # 25 | # @return [String] 26 | attr_accessor :display_name 27 | 28 | # @example Raw definition from a hash 29 | # Unit::Definition.new("rack-unit",[%w{U rack-U}, (6405920109971793/144115188075855872), :length, %w{} ]) 30 | # 31 | # @example Block form 32 | # Unit::Definition.new("rack-unit") do |unit| 33 | # unit.aliases = %w{U rack-U} 34 | # unit.definition = RubyUnits::Unit.new("7/4 inches") 35 | # end 36 | # 37 | def initialize(name, definition = []) 38 | yield self if block_given? 39 | self.name ||= name.gsub(/[<>]/, "") 40 | @aliases ||= definition[0] || [name] 41 | @scalar ||= definition[1] 42 | @kind ||= definition[2] 43 | @numerator ||= definition[3] || RubyUnits::Unit::UNITY_ARRAY 44 | @denominator ||= definition[4] || RubyUnits::Unit::UNITY_ARRAY 45 | @display_name ||= @aliases.first 46 | end 47 | 48 | # name of the unit 49 | # nil if name is not set, adds '<' and '>' around the name 50 | # @return [String, nil] 51 | # @todo refactor Unit and Unit::Definition so we don't need to wrap units with angle brackets 52 | def name 53 | "<#{@name}>" if defined?(@name) && @name 54 | end 55 | 56 | # set the name, strip off '<' and '>' 57 | # @param name_value [String] 58 | # @return [String] 59 | def name=(name_value) 60 | @name = name_value.gsub(/[<>]/, "") 61 | end 62 | 63 | # alias array must contain the name of the unit and entries must be unique 64 | # @return [Array] 65 | def aliases 66 | [[@aliases], @name, @display_name].flatten.compact.uniq 67 | end 68 | 69 | # define a unit in terms of another unit 70 | # @param [Unit] unit 71 | # @return [Unit::Definition] 72 | def definition=(unit) 73 | base = unit.to_base 74 | @scalar = base.scalar 75 | @kind = base.kind 76 | @numerator = base.numerator 77 | @denominator = base.denominator 78 | end 79 | 80 | # is this definition for a prefix? 81 | # @return [Boolean] 82 | def prefix? 83 | kind == :prefix 84 | end 85 | 86 | # Is this definition the unity definition? 87 | # @return [Boolean] 88 | def unity? 89 | prefix? && scalar == 1 90 | end 91 | 92 | # is this a base unit? 93 | # units are base units if the scalar is one, and the unit is defined in terms of itself. 94 | # @return [Boolean] 95 | def base? 96 | (denominator == RubyUnits::Unit::UNITY_ARRAY) && 97 | (numerator != RubyUnits::Unit::UNITY_ARRAY) && 98 | (numerator.size == 1) && 99 | (scalar == 1) && 100 | (numerator.first == self.name) 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/ruby_units/math.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyUnits 4 | # Math will convert unit objects to radians and then attempt to use the value for 5 | # trigonometric functions. 6 | module Math 7 | # Take the square root of a unit or number 8 | # 9 | # @param number [Numeric, RubyUnits::Unit] 10 | # @return [Numeric, RubyUnits::Unit] 11 | def sqrt(number) 12 | if number.is_a?(RubyUnits::Unit) 13 | (number**Rational(1, 2)).to_unit 14 | else 15 | super 16 | end 17 | end 18 | 19 | # Take the cube root of a unit or number 20 | # 21 | # @param number [Numeric, RubyUnits::Unit] 22 | # @return [Numeric, RubyUnits::Unit] 23 | def cbrt(number) 24 | if number.is_a?(RubyUnits::Unit) 25 | (number**Rational(1, 3)).to_unit 26 | else 27 | super 28 | end 29 | end 30 | 31 | # @param angle [Numeric, RubyUnits::Unit] 32 | # @return [Numeric] 33 | def sin(angle) 34 | angle.is_a?(RubyUnits::Unit) ? super(angle.convert_to("radian").scalar) : super 35 | end 36 | 37 | # @param number [Numeric, RubyUnits::Unit] 38 | # @return [Numeric, RubyUnits::Unit] 39 | def asin(number) 40 | if number.is_a?(RubyUnits::Unit) 41 | [super, "radian"].to_unit 42 | else 43 | super 44 | end 45 | end 46 | 47 | # @param angle [Numeric, RubyUnits::Unit] 48 | # @return [Numeric] 49 | def cos(angle) 50 | angle.is_a?(RubyUnits::Unit) ? super(angle.convert_to("radian").scalar) : super 51 | end 52 | 53 | # @param number [Numeric, RubyUnits::Unit] 54 | # @return [Numeric, RubyUnits::Unit] 55 | def acos(number) 56 | if number.is_a?(RubyUnits::Unit) 57 | [super, "radian"].to_unit 58 | else 59 | super 60 | end 61 | end 62 | 63 | # @param number [Numeric, RubyUnits::Unit] 64 | # @return [Numeric] 65 | def sinh(number) 66 | number.is_a?(RubyUnits::Unit) ? super(number.convert_to("radian").scalar) : super 67 | end 68 | 69 | # @param number [Numeric, RubyUnits::Unit] 70 | # @return [Numeric] 71 | def cosh(number) 72 | number.is_a?(RubyUnits::Unit) ? super(number.convert_to("radian").scalar) : super 73 | end 74 | 75 | # @param angle [Numeric, RubyUnits::Unit] 76 | # @return [Numeric] 77 | def tan(angle) 78 | angle.is_a?(RubyUnits::Unit) ? super(angle.convert_to("radian").scalar) : super 79 | end 80 | 81 | # @param number [Numeric, RubyUnits::Unit] 82 | # @return [Numeric] 83 | def tanh(number) 84 | number.is_a?(RubyUnits::Unit) ? super(number.convert_to("radian").scalar) : super 85 | end 86 | 87 | # @param x [Numeric, RubyUnits::Unit] 88 | # @param y [Numeric, RubyUnits::Unit] 89 | # @return [Numeric] 90 | def hypot(x, y) 91 | if x.is_a?(RubyUnits::Unit) && y.is_a?(RubyUnits::Unit) 92 | ((x**2) + (y**2))**Rational(1, 2) 93 | else 94 | super 95 | end 96 | end 97 | 98 | # @param number [Numeric, RubyUnits::Unit] 99 | # @return [Numeric] if argument is a number 100 | # @return [RubyUnits::Unit] if argument is a unit 101 | def atan(number) 102 | if number.is_a?(RubyUnits::Unit) 103 | [super, "radian"].to_unit 104 | else 105 | super 106 | end 107 | end 108 | 109 | # @param x [Numeric, RubyUnits::Unit] 110 | # @param y [Numeric, RubyUnits::Unit] 111 | # @return [Numeric] if all parameters are numbers 112 | # @return [RubyUnits::Unit] if parameters are units 113 | # @raise [ArgumentError] if parameters are not numbers or compatible units 114 | def atan2(x, y) 115 | raise ArgumentError, "Incompatible RubyUnits::Units" if (x.is_a?(RubyUnits::Unit) && y.is_a?(RubyUnits::Unit)) && !x.compatible?(y) 116 | 117 | if (x.is_a?(RubyUnits::Unit) && y.is_a?(RubyUnits::Unit)) && x.compatible?(y) 118 | [super(x.base_scalar, y.base_scalar), "radian"].to_unit 119 | else 120 | super 121 | end 122 | end 123 | 124 | # @param number [Numeric, RubyUnits::Unit] 125 | # @return [Numeric] 126 | def log10(number) 127 | if number.is_a?(RubyUnits::Unit) 128 | super(number.to_f) 129 | else 130 | super 131 | end 132 | end 133 | 134 | # @param number [Numeric, RubyUnits::Unit] 135 | # @param base [Numeric] 136 | # @return [Numeric] 137 | def log(number, base = ::Math::E) 138 | if number.is_a?(RubyUnits::Unit) 139 | super(number.to_f, base) 140 | else 141 | super 142 | end 143 | end 144 | end 145 | end 146 | 147 | # @see https://github.com/lsegal/yard/issues/1353 148 | module Math 149 | singleton_class.prepend(RubyUnits::Math) 150 | end 151 | -------------------------------------------------------------------------------- /lib/ruby_units/namespaced.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # require_relative this file to avoid creating an class alias from Unit to RubyUnits::Unit 4 | require_relative "version" 5 | require_relative "configuration" 6 | require_relative "definition" 7 | require_relative "cache" 8 | require_relative "array" 9 | require_relative "date" 10 | require_relative "time" 11 | require_relative "math" 12 | require_relative "numeric" 13 | require_relative "string" 14 | require_relative "unit" 15 | require_relative "unit_definitions" 16 | -------------------------------------------------------------------------------- /lib/ruby_units/numeric.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyUnits 4 | # Extra methods for [::Numeric] to allow it to be used as a [RubyUnits::Unit] 5 | module Numeric 6 | # Make a unitless unit with a given scalar. 7 | # > In ruby-units <= 2.3.2 this method would create a new [RubyUnits::Unit] 8 | # > with the scalar and passed in units. This was changed to be more 9 | # > consistent with the behavior of [#to_unit]. Specifically the argument is 10 | # > used as a convenience method to convert the unitless scalar unit to a 11 | # > compatible unitless unit. 12 | # 13 | # @param other [RubyUnits::Unit, String] convert to same units as passed 14 | # @return [RubyUnits::Unit] 15 | def to_unit(other = nil) 16 | other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) 17 | end 18 | end 19 | end 20 | 21 | # @note Do this instead of Numeric.prepend(RubyUnits::Numeric) to avoid YARD warnings 22 | # @see https://github.com/lsegal/yard/issues/1353 23 | class Numeric 24 | prepend(RubyUnits::Numeric) 25 | end 26 | -------------------------------------------------------------------------------- /lib/ruby_units/string.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "time" 4 | 5 | module RubyUnits 6 | # Extra methods for converting [String] objects to [RubyUnits::Unit] objects 7 | # and using string formatting with Units. 8 | module String 9 | # Make a string into a unit 10 | # 11 | # @param other [RubyUnits::Unit, String] unit to convert to 12 | # @return [RubyUnits::Unit] 13 | def to_unit(other = nil) 14 | other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) 15 | end 16 | 17 | # Format unit output using formatting codes 18 | # @example '%0.2f' % '1 mm'.to_unit => '1.00 mm' 19 | # 20 | # @param other [RubyUnits::Unit, Object] 21 | # @return [String] 22 | def %(*other) 23 | if other.first.is_a?(RubyUnits::Unit) 24 | other.first.to_s(self) 25 | else 26 | super 27 | end 28 | end 29 | 30 | # @param (see RubyUnits::Unit#convert_to) 31 | # @return (see RubyUnits::Unit#convert_to) 32 | def convert_to(other) 33 | to_unit.convert_to(other) 34 | end 35 | end 36 | end 37 | 38 | # @note Do this instead of String.prepend(RubyUnits::String) to avoid YARD warnings 39 | # @see https://github.com/lsegal/yard/issues/1353 40 | class String 41 | prepend(RubyUnits::String) 42 | end 43 | -------------------------------------------------------------------------------- /lib/ruby_units/time.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "time" 4 | 5 | module RubyUnits 6 | # Time math is handled slightly differently. The difference is considered to be an exact duration if 7 | # the subtracted value is in hours, minutes, or seconds. It is rounded to the nearest day if the offset 8 | # is in years, decades, or centuries. This leads to less precise values, but ones that match the 9 | # calendar better. 10 | module Time 11 | # Class methods for [Time] objects 12 | module ClassMethods 13 | # Convert a duration to a [::Time] object by considering the duration to be 14 | # the number of seconds since the epoch 15 | # 16 | # @param [Array] args 17 | # @return [::Time] 18 | def at(*args, **kwargs) 19 | case args.first 20 | when RubyUnits::Unit 21 | options = args.last.is_a?(Hash) ? args.pop : kwargs 22 | secondary_unit = args[2] || "microsecond" 23 | case args[1] 24 | when Numeric 25 | super((args.first + RubyUnits::Unit.new(args[1], secondary_unit.to_s)).convert_to("second").scalar, **options) 26 | else 27 | super(args.first.convert_to("second").scalar, **options) 28 | end 29 | else 30 | super 31 | end 32 | end 33 | 34 | # @example 35 | # Time.in '5 min' 36 | # @param duration [#to_unit] 37 | # @return [::Time] 38 | def in(duration) 39 | ::Time.now + duration.to_unit 40 | end 41 | end 42 | 43 | # Convert a [::Time] object to a [RubyUnits::Unit] object. The time is 44 | # considered to be a duration with the number of seconds since the epoch. 45 | # 46 | # @param other [String, RubyUnits::Unit] 47 | # @return [RubyUnits::Unit] 48 | def to_unit(other = nil) 49 | other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) 50 | end 51 | 52 | # @param other [::Time, RubyUnits::Unit] 53 | # @return [RubyUnits::Unit, ::Time] 54 | def +(other) 55 | case other 56 | when RubyUnits::Unit 57 | other = other.convert_to("d").round.convert_to("s") if %w[y decade century].include? other.units 58 | begin 59 | super(other.convert_to("s").scalar) 60 | rescue RangeError 61 | to_datetime + other 62 | end 63 | else 64 | super 65 | end 66 | end 67 | 68 | # @param other [::Time, RubyUnits::Unit] 69 | # @return [RubyUnits::Unit, ::Time] 70 | def -(other) 71 | case other 72 | when RubyUnits::Unit 73 | other = other.convert_to("d").round.convert_to("s") if %w[y decade century].include? other.units 74 | begin 75 | super(other.convert_to("s").scalar) 76 | rescue RangeError 77 | public_send(:to_datetime) - other 78 | end 79 | else 80 | super 81 | end 82 | end 83 | end 84 | end 85 | 86 | # @note Do this instead of Time.prepend(RubyUnits::Time) to avoid YARD warnings 87 | # @see https://github.com/lsegal/yard/issues/1353 88 | class Time 89 | prepend(RubyUnits::Time) 90 | singleton_class.prepend(RubyUnits::Time::ClassMethods) 91 | end 92 | -------------------------------------------------------------------------------- /lib/ruby_units/unit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "date" 4 | module RubyUnits 5 | # Copyright 2006-2024 6 | # @author Kevin C. Olbrich, Ph.D. 7 | # @see https://github.com/olbrich/ruby-units 8 | # 9 | # @note The accuracy of unit conversions depends on the precision of the conversion factor. 10 | # If you have more accurate estimates for particular conversion factors, please send them 11 | # to me and I will incorporate them into the next release. It is also incumbent on the end-user 12 | # to ensure that the accuracy of any conversions is sufficient for their intended application. 13 | # 14 | # While there are a large number of unit specified in the base package, 15 | # there are also a large number of units that are not included. 16 | # This package covers nearly all SI, Imperial, and units commonly used 17 | # in the United States. If your favorite units are not listed here, file an issue on GitHub. 18 | # 19 | # To add or override a unit definition, add a code block like this.. 20 | # @example Define a new unit 21 | # RubyUnits::Unit.define("foobar") do |unit| 22 | # unit.aliases = %w{foo fb foo-bar} 23 | # unit.definition = RubyUnits::Unit.new("1 baz") 24 | # end 25 | # 26 | class Unit < ::Numeric 27 | class << self 28 | # return a list of all defined units 29 | # @return [Hash{Symbol=>RubyUnits::Units::Definition}] 30 | attr_accessor :definitions 31 | 32 | # @return [Hash{Symbol => String}] the list of units and their prefixes 33 | attr_accessor :prefix_values 34 | 35 | # @return [Hash{Symbol => String}] 36 | attr_accessor :prefix_map 37 | 38 | # @return [Hash{Symbol => String}] 39 | attr_accessor :unit_map 40 | 41 | # @return [Hash{Symbol => String}] 42 | attr_accessor :unit_values 43 | 44 | # @return [Hash{Integer => Symbol}] 45 | attr_reader :kinds 46 | end 47 | self.definitions = {} 48 | self.prefix_values = {} 49 | self.prefix_map = {} 50 | self.unit_map = {} 51 | self.unit_values = {} 52 | @unit_regex = nil 53 | @unit_match_regex = nil 54 | UNITY = "<1>" 55 | UNITY_ARRAY = [UNITY].freeze 56 | 57 | SIGN_REGEX = /(?:[+-])?/.freeze # +, -, or nothing 58 | 59 | # regex for matching an integer number but not a fraction 60 | INTEGER_DIGITS_REGEX = %r{(?#{SIGN_REGEX}#{DECIMAL_REGEX})[ -])?(?#{SIGN_REGEX}#{DECIMAL_REGEX})/(?#{SIGN_REGEX}#{DECIMAL_REGEX})\)?}.freeze # 1 2/3, -1 2/3, 5/3, 1-2/3, (1/2) etc. 67 | # Scientific notation: 1, -1, +1, 1.2, +1.2, -1.2, 123.4E5, +123.4e5, 68 | # -123.4E+5, -123.4e-5, etc. 69 | SCI_NUMBER = /([+-]?\d*[.]?\d+(?:[Ee][+-]?\d+(?![.]))?)/.freeze 70 | # ideally we would like to generate this regex from the alias for a 'feet' 71 | # and 'inches', but they aren't defined at the point in the code where we 72 | # need this regex. 73 | FEET_INCH_UNITS_REGEX = /(?:'|ft|feet)\s*(?#{RATIONAL_NUMBER}|#{SCI_NUMBER})\s*(?:"|in|inch(?:es)?)/.freeze 74 | FEET_INCH_REGEX = /(?#{INTEGER_REGEX})\s*#{FEET_INCH_UNITS_REGEX}/.freeze 75 | # ideally we would like to generate this regex from the alias for a 'pound' 76 | # and 'ounce', but they aren't defined at the point in the code where we 77 | # need this regex. 78 | LBS_OZ_UNIT_REGEX = /(?:#|lbs?|pounds?|pound-mass)+[\s,]*(?#{RATIONAL_NUMBER}|#{UNSIGNED_INTEGER_REGEX})\s*(?:ozs?|ounces?)/.freeze 79 | LBS_OZ_REGEX = /(?#{INTEGER_REGEX})\s*#{LBS_OZ_UNIT_REGEX}/.freeze 80 | # ideally we would like to generate this regex from the alias for a 'stone' 81 | # and 'pound', but they aren't defined at the point in the code where we 82 | # need this regex. also note that the plural of 'stone' is still 'stone', 83 | # but we accept 'stones' anyway. 84 | STONE_LB_UNIT_REGEX = /(?:sts?|stones?)+[\s,]*(?#{RATIONAL_NUMBER}|#{UNSIGNED_INTEGER_REGEX})\s*(?:#|lbs?|pounds?|pound-mass)*/.freeze 85 | STONE_LB_REGEX = /(?#{INTEGER_REGEX})\s*#{STONE_LB_UNIT_REGEX}/.freeze 86 | # Time formats: 12:34:56,78, (hh:mm:ss,msec) etc. 87 | TIME_REGEX = /(?\d+):(?\d+):?(?:(?\d+))?(?:[.](?\d+))?/.freeze 88 | # Complex numbers: 1+2i, 1.0+2.0i, -1-1i, etc. 89 | COMPLEX_NUMBER = /(?#{SCI_NUMBER})?(?#{SCI_NUMBER})i\b/.freeze 90 | # Any Complex, Rational, or scientific number 91 | ANY_NUMBER = /(#{COMPLEX_NUMBER}|#{RATIONAL_NUMBER}|#{SCI_NUMBER})/.freeze 92 | ANY_NUMBER_REGEX = /(?:#{ANY_NUMBER})?\s?([^-\d.].*)?/.freeze 93 | NUMBER_REGEX = /(?#{SCI_NUMBER}*)\s*(?.+)?/.freeze # a number followed by a unit 94 | UNIT_STRING_REGEX = %r{#{SCI_NUMBER}*\s*([^/]*)/*(.+)*}.freeze 95 | TOP_REGEX = /([^ *]+)(?:\^|\*\*)([\d-]+)/.freeze 96 | BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/.freeze 97 | NUMBER_UNIT_REGEX = /#{SCI_NUMBER}?(.*)/.freeze 98 | COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(?.+)?/.freeze 99 | RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(?.+)?/.freeze 100 | KELVIN = [""].freeze 101 | FAHRENHEIT = [""].freeze 102 | RANKINE = [""].freeze 103 | CELSIUS = [""].freeze 104 | @temp_regex = nil 105 | SIGNATURE_VECTOR = %i[ 106 | length 107 | time 108 | temperature 109 | mass 110 | current 111 | substance 112 | luminosity 113 | currency 114 | information 115 | angle 116 | ].freeze 117 | @kinds = { 118 | -312_078 => :elastance, 119 | -312_058 => :resistance, 120 | -312_038 => :inductance, 121 | -152_040 => :magnetism, 122 | -152_038 => :magnetism, 123 | -152_058 => :potential, 124 | -7997 => :specific_volume, 125 | -79 => :snap, 126 | -59 => :jolt, 127 | -39 => :acceleration, 128 | -38 => :radiation, 129 | -20 => :frequency, 130 | -19 => :speed, 131 | -18 => :viscosity, 132 | -17 => :volumetric_flow, 133 | -1 => :wavenumber, 134 | 0 => :unitless, 135 | 1 => :length, 136 | 2 => :area, 137 | 3 => :volume, 138 | 20 => :time, 139 | 400 => :temperature, 140 | 7941 => :yank, 141 | 7942 => :power, 142 | 7959 => :pressure, 143 | 7962 => :energy, 144 | 7979 => :viscosity, 145 | 7961 => :force, 146 | 7981 => :momentum, 147 | 7982 => :angular_momentum, 148 | 7997 => :density, 149 | 7998 => :area_density, 150 | 8000 => :mass, 151 | 152_020 => :radiation_exposure, 152 | 159_999 => :magnetism, 153 | 160_000 => :current, 154 | 160_020 => :charge, 155 | 312_058 => :conductance, 156 | 312_078 => :capacitance, 157 | 3_199_980 => :activity, 158 | 3_199_997 => :molar_concentration, 159 | 3_200_000 => :substance, 160 | 63_999_998 => :illuminance, 161 | 64_000_000 => :luminous_power, 162 | 1_280_000_000 => :currency, 163 | 25_600_000_000 => :information, 164 | 511_999_999_980 => :angular_velocity, 165 | 512_000_000_000 => :angle 166 | }.freeze 167 | 168 | # Class Methods 169 | 170 | # Callback triggered when a subclass is created. This properly sets up the internal variables, and copies 171 | # definitions from the parent class. 172 | # 173 | # @param [Class] subclass 174 | def self.inherited(subclass) 175 | super 176 | subclass.definitions = definitions.dup 177 | subclass.instance_variable_set(:@kinds, @kinds.dup) 178 | subclass.setup 179 | end 180 | 181 | # setup internal arrays and hashes 182 | # @return [Boolean] 183 | def self.setup 184 | clear_cache 185 | self.prefix_values = {} 186 | self.prefix_map = {} 187 | self.unit_map = {} 188 | self.unit_values = {} 189 | @unit_regex = nil 190 | @unit_match_regex = nil 191 | @prefix_regex = nil 192 | 193 | definitions.each_value do |definition| 194 | use_definition(definition) 195 | end 196 | 197 | new(1) 198 | true 199 | end 200 | 201 | # determine if a unit is already defined 202 | # @param [String] unit 203 | # @return [Boolean] 204 | def self.defined?(unit) 205 | definitions.values.any? { _1.aliases.include?(unit) } 206 | end 207 | 208 | # return the unit definition for a unit 209 | # @param unit_name [String] 210 | # @return [RubyUnits::Unit::Definition, nil] 211 | def self.definition(unit_name) 212 | unit = unit_name =~ /^<.+>$/ ? unit_name : "<#{unit_name}>" 213 | definitions[unit] 214 | end 215 | 216 | # @param [RubyUnits::Unit::Definition, String] unit_definition 217 | # @param [Proc] block 218 | # @return [RubyUnits::Unit::Definition] 219 | # @raise [ArgumentError] when passed a non-string if using the block form 220 | # Unpack a unit definition and add it to the array of defined units 221 | # 222 | # @example Block form 223 | # RubyUnits::Unit.define('foobar') do |foobar| 224 | # foobar.definition = RubyUnits::Unit.new("1 baz") 225 | # end 226 | # 227 | # @example RubyUnits::Unit::Definition form 228 | # unit_definition = RubyUnits::Unit::Definition.new("foobar") {|foobar| foobar.definition = RubyUnits::Unit.new("1 baz")} 229 | # RubyUnits::Unit.define(unit_definition) 230 | def self.define(unit_definition, &block) 231 | if block_given? 232 | raise ArgumentError, "When using the block form of RubyUnits::Unit.define, pass the name of the unit" unless unit_definition.is_a?(String) 233 | 234 | unit_definition = RubyUnits::Unit::Definition.new(unit_definition, &block) 235 | end 236 | definitions[unit_definition.name] = unit_definition 237 | use_definition(unit_definition) 238 | unit_definition 239 | end 240 | 241 | # Get the definition for a unit and allow it to be redefined 242 | # 243 | # @param [String] name Name of unit to redefine 244 | # @param [Proc] _block 245 | # @raise [ArgumentError] if a block is not given 246 | # @yieldparam [RubyUnits::Unit::Definition] the definition of the unit being 247 | # redefined 248 | # @return (see RubyUnits::Unit.define) 249 | def self.redefine!(name, &_block) 250 | raise ArgumentError, "A block is required to redefine a unit" unless block_given? 251 | 252 | unit_definition = definition(name) 253 | raise(ArgumentError, "'#{name}' Unit not recognized") unless unit_definition 254 | 255 | yield unit_definition 256 | definitions.delete("<#{name}>") 257 | define(unit_definition) 258 | setup 259 | end 260 | 261 | # Undefine a unit. Will not raise an exception for unknown units. 262 | # 263 | # @param unit [String] name of unit to undefine 264 | # @return (see RubyUnits::Unit.setup) 265 | def self.undefine!(unit) 266 | definitions.delete("<#{unit}>") 267 | setup 268 | end 269 | 270 | # Unit cache 271 | # 272 | # @return [RubyUnits::Cache] 273 | def self.cached 274 | @cached ||= RubyUnits::Cache.new 275 | end 276 | 277 | # @return [Boolean] 278 | def self.clear_cache 279 | cached.clear 280 | base_unit_cache.clear 281 | new(1) 282 | true 283 | end 284 | 285 | # @return [RubyUnits::Cache] 286 | def self.base_unit_cache 287 | @base_unit_cache ||= RubyUnits::Cache.new 288 | end 289 | 290 | # @example parse strings 291 | # "1 minute in seconds" 292 | # @param [String] input 293 | # @return [Unit] 294 | def self.parse(input) 295 | first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first 296 | second.nil? ? new(first) : new(first).convert_to(second) 297 | end 298 | 299 | # @param q [Numeric] quantity 300 | # @param n [Array] numerator 301 | # @param d [Array] denominator 302 | # @return [Hash] 303 | def self.eliminate_terms(q, n, d) 304 | num = n.dup 305 | den = d.dup 306 | num.delete(UNITY) 307 | den.delete(UNITY) 308 | 309 | combined = ::Hash.new(0) 310 | 311 | [[num, 1], [den, -1]].each do |array, increment| 312 | array.chunk_while { |elt_before, _| definition(elt_before).prefix? } 313 | .to_a 314 | .each { combined[_1] += increment } 315 | end 316 | 317 | num = [] 318 | den = [] 319 | combined.each do |key, value| 320 | if value.positive? 321 | value.times { num << key } 322 | elsif value.negative? 323 | value.abs.times { den << key } 324 | end 325 | end 326 | num = UNITY_ARRAY if num.empty? 327 | den = UNITY_ARRAY if den.empty? 328 | { scalar: q, numerator: num.flatten, denominator: den.flatten } 329 | end 330 | 331 | # Creates a new unit from the current one with all common terms eliminated. 332 | # 333 | # @return [RubyUnits::Unit] 334 | def eliminate_terms 335 | self.class.new(self.class.eliminate_terms(@scalar, @numerator, @denominator)) 336 | end 337 | 338 | # return an array of base units 339 | # @return [Array] 340 | def self.base_units 341 | @base_units ||= definitions.dup.select { |_, definition| definition.base? }.keys.map { new(_1) } 342 | end 343 | 344 | # Parse a string consisting of a number and a unit string 345 | # NOTE: This does not properly handle units formatted like '12mg/6ml' 346 | # 347 | # @param [String] string 348 | # @return [Array(Numeric, String)] consisting of [number, "unit"] 349 | def self.parse_into_numbers_and_units(string) 350 | num, unit = string.scan(ANY_NUMBER_REGEX).first 351 | 352 | [ 353 | case num 354 | when nil # This happens when no number is passed and we are parsing a pure unit string 355 | 1 356 | when COMPLEX_NUMBER 357 | num.to_c 358 | when RATIONAL_NUMBER 359 | # We use this method instead of relying on `to_r` because it does not 360 | # handle improper fractions correctly. 361 | sign = Regexp.last_match(1) == "-" ? -1 : 1 362 | n = Regexp.last_match(2).to_i 363 | f = Rational(Regexp.last_match(3).to_i, Regexp.last_match(4).to_i) 364 | sign * (n + f) 365 | else 366 | num.to_f 367 | end, 368 | unit.to_s.strip 369 | ] 370 | end 371 | 372 | # return a fragment of a regex to be used for matching units or reconstruct it if hasn't been used yet. 373 | # Unit names are reverse sorted by length so the regexp matcher will prefer longer and more specific names 374 | # @return [String] 375 | def self.unit_regex 376 | @unit_regex ||= unit_map.keys.sort_by { [_1.length, _1] }.reverse.join("|") 377 | end 378 | 379 | # return a regex used to match units 380 | # @return [Regexp] 381 | def self.unit_match_regex 382 | @unit_match_regex ||= /(#{prefix_regex})??(#{unit_regex})\b/ 383 | end 384 | 385 | # return a regexp fragment used to match prefixes 386 | # @return [String] 387 | # @private 388 | def self.prefix_regex 389 | @prefix_regex ||= prefix_map.keys.sort_by { [_1.length, _1] }.reverse.join("|") 390 | end 391 | 392 | # Generates (and memoizes) a regexp matching any of the temperature units or their aliases. 393 | # 394 | # @return [Regexp] 395 | def self.temp_regex 396 | @temp_regex ||= begin 397 | temp_units = %w[tempK tempC tempF tempR degK degC degF degR] 398 | aliases = temp_units.map do |unit| 399 | d = definition(unit) 400 | d&.aliases 401 | end.flatten.compact 402 | regex_str = aliases.empty? ? "(?!x)x" : aliases.join("|") 403 | Regexp.new "(?:#{regex_str})" 404 | end 405 | end 406 | 407 | # inject a definition into the internal array and set it up for use 408 | # 409 | # @param definition [RubyUnits::Unit::Definition] 410 | def self.use_definition(definition) 411 | @unit_match_regex = nil # invalidate the unit match regex 412 | @temp_regex = nil # invalidate the temp regex 413 | if definition.prefix? 414 | prefix_values[definition.name] = definition.scalar 415 | definition.aliases.each { prefix_map[_1] = definition.name } 416 | @prefix_regex = nil # invalidate the prefix regex 417 | else 418 | unit_values[definition.name] = {} 419 | unit_values[definition.name][:scalar] = definition.scalar 420 | unit_values[definition.name][:numerator] = definition.numerator if definition.numerator 421 | unit_values[definition.name][:denominator] = definition.denominator if definition.denominator 422 | definition.aliases.each { unit_map[_1] = definition.name } 423 | @unit_regex = nil # invalidate the unit regex 424 | end 425 | end 426 | 427 | include Comparable 428 | 429 | # @return [Numeric] 430 | attr_accessor :scalar 431 | 432 | # @return [Array] 433 | attr_accessor :numerator 434 | 435 | # @return [Array] 436 | attr_accessor :denominator 437 | 438 | # @return [Integer] 439 | attr_accessor :signature 440 | 441 | # @return [Numeric] 442 | attr_accessor :base_scalar 443 | 444 | # @return [Array] 445 | attr_accessor :base_numerator 446 | 447 | # @return [Array] 448 | attr_accessor :base_denominator 449 | 450 | # @return [String] 451 | attr_accessor :output 452 | 453 | # @return [String] 454 | attr_accessor :unit_name 455 | 456 | # Used to copy one unit to another 457 | # @param from [RubyUnits::Unit] Unit to copy definition from 458 | # @return [RubyUnits::Unit] 459 | def copy(from) 460 | @scalar = from.scalar 461 | @numerator = from.numerator 462 | @denominator = from.denominator 463 | @base = from.base? 464 | @signature = from.signature 465 | @base_scalar = from.base_scalar 466 | @unit_name = from.unit_name 467 | self 468 | end 469 | 470 | # Create a new Unit object. Can be initialized using a String, a Hash, an Array, Time, DateTime 471 | # 472 | # @example Valid options include: 473 | # "5.6 kg*m/s^2" 474 | # "5.6 kg*m*s^-2" 475 | # "5.6 kilogram*meter*second^-2" 476 | # "2.2 kPa" 477 | # "37 degC" 478 | # "1" -- creates a unitless constant with value 1 479 | # "GPa" -- creates a unit with scalar 1 with units 'GPa' 480 | # "6'4\""" -- recognized as 6 feet + 4 inches 481 | # "8 lbs 8 oz" -- recognized as 8 lbs + 8 ounces 482 | # [1, 'kg'] 483 | # {scalar: 1, numerator: 'kg'} 484 | # 485 | # @param [Unit,String,Hash,Array,Date,Time,DateTime] options 486 | # @return [Unit] 487 | # @raise [ArgumentError] if absolute value of a temperature is less than absolute zero 488 | # @raise [ArgumentError] if no unit is specified 489 | # @raise [ArgumentError] if an invalid unit is specified 490 | def initialize(*options) 491 | @scalar = nil 492 | @base_scalar = nil 493 | @unit_name = nil 494 | @signature = nil 495 | @output = {} 496 | raise ArgumentError, "Invalid Unit Format" if options[0].nil? 497 | 498 | if options.size == 2 499 | # options[0] is the scalar 500 | # options[1] is a unit string 501 | cached = self.class.cached.get(options[1]) 502 | if cached.nil? 503 | initialize("#{options[0]} #{options[1]}") 504 | else 505 | copy(cached * options[0]) 506 | end 507 | return 508 | end 509 | if options.size == 3 510 | options[1] = options[1].join if options[1].is_a?(Array) 511 | options[2] = options[2].join if options[2].is_a?(Array) 512 | cached = self.class.cached.get("#{options[1]}/#{options[2]}") 513 | if cached.nil? 514 | initialize("#{options[0]} #{options[1]}/#{options[2]}") 515 | else 516 | copy(cached) * options[0] 517 | end 518 | return 519 | end 520 | 521 | case options[0] 522 | when Unit 523 | copy(options[0]) 524 | return 525 | when Hash 526 | @scalar = options[0][:scalar] || 1 527 | @numerator = options[0][:numerator] || UNITY_ARRAY 528 | @denominator = options[0][:denominator] || UNITY_ARRAY 529 | @signature = options[0][:signature] 530 | when Array 531 | initialize(*options[0]) 532 | return 533 | when Numeric 534 | @scalar = options[0] 535 | @numerator = @denominator = UNITY_ARRAY 536 | when Time 537 | @scalar = options[0].to_f 538 | @numerator = [""] 539 | @denominator = UNITY_ARRAY 540 | when DateTime, Date 541 | @scalar = options[0].ajd 542 | @numerator = [""] 543 | @denominator = UNITY_ARRAY 544 | when /^\s*$/ 545 | raise ArgumentError, "No Unit Specified" 546 | when String 547 | parse(options[0]) 548 | else 549 | raise ArgumentError, "Invalid Unit Format" 550 | end 551 | update_base_scalar 552 | raise ArgumentError, "Temperatures must not be less than absolute zero" if temperature? && base_scalar.negative? 553 | 554 | unary_unit = units || "" 555 | if options.first.instance_of?(String) 556 | _opt_scalar, opt_units = self.class.parse_into_numbers_and_units(options[0]) 557 | if !(self.class.cached.keys.include?(opt_units) || 558 | (opt_units =~ %r{\D/[\d+.]+}) || 559 | (opt_units =~ %r{(#{self.class.temp_regex})|(#{STONE_LB_UNIT_REGEX})|(#{LBS_OZ_UNIT_REGEX})|(#{FEET_INCH_UNITS_REGEX})|%|(#{TIME_REGEX})|i\s?(.+)?|±|\+/-})) && (opt_units && !opt_units.empty?) 560 | self.class.cached.set(opt_units, scalar == 1 ? self : opt_units.to_unit) 561 | end 562 | end 563 | unless self.class.cached.keys.include?(unary_unit) || (unary_unit =~ self.class.temp_regex) 564 | self.class.cached.set(unary_unit, scalar == 1 ? self : unary_unit.to_unit) 565 | end 566 | [@scalar, @numerator, @denominator, @base_scalar, @signature, @base].each(&:freeze) 567 | super() 568 | end 569 | 570 | # @todo: figure out how to handle :counting units. This method should probably return :counting instead of :unitless for 'each' 571 | # return the kind of the unit (:mass, :length, etc...) 572 | # @return [Symbol] 573 | def kind 574 | self.class.kinds[signature] 575 | end 576 | 577 | # Convert the unit to a Unit, possibly performing a conversion. 578 | # > The ability to pass a Unit to convert to was added in v3.0.0 for 579 | # > consistency with other uses of #to_unit. 580 | # 581 | # @param other [RubyUnits::Unit, String] unit to convert to 582 | # @return [RubyUnits::Unit] 583 | def to_unit(other = nil) 584 | other ? convert_to(other) : self 585 | end 586 | 587 | alias unit to_unit 588 | 589 | # Is this unit in base form? 590 | # @return [Boolean] 591 | def base? 592 | return @base if defined? @base 593 | 594 | @base = (@numerator + @denominator) 595 | .compact 596 | .uniq 597 | .map { self.class.definition(_1) } 598 | .all? { _1.unity? || _1.base? } 599 | @base 600 | end 601 | 602 | alias is_base? base? 603 | 604 | # convert to base SI units 605 | # results of the conversion are cached so subsequent calls to this will be fast 606 | # @return [Unit] 607 | # @todo this is brittle as it depends on the display_name of a unit, which can be changed 608 | def to_base 609 | return self if base? 610 | 611 | if self.class.unit_map[units] =~ /\A<(?:temp|deg)[CRF]>\Z/ 612 | @signature = self.class.kinds.key(:temperature) 613 | base = if temperature? 614 | convert_to("tempK") 615 | elsif degree? 616 | convert_to("degK") 617 | end 618 | return base 619 | end 620 | 621 | cached_unit = self.class.base_unit_cache.get(units) 622 | return cached_unit * scalar unless cached_unit.nil? 623 | 624 | num = [] 625 | den = [] 626 | q = Rational(1) 627 | @numerator.compact.each do |num_unit| 628 | if self.class.prefix_values[num_unit] 629 | q *= self.class.prefix_values[num_unit] 630 | else 631 | q *= self.class.unit_values[num_unit][:scalar] if self.class.unit_values[num_unit] 632 | num << self.class.unit_values[num_unit][:numerator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:numerator] 633 | den << self.class.unit_values[num_unit][:denominator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:denominator] 634 | end 635 | end 636 | @denominator.compact.each do |num_unit| 637 | if self.class.prefix_values[num_unit] 638 | q /= self.class.prefix_values[num_unit] 639 | else 640 | q /= self.class.unit_values[num_unit][:scalar] if self.class.unit_values[num_unit] 641 | den << self.class.unit_values[num_unit][:numerator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:numerator] 642 | num << self.class.unit_values[num_unit][:denominator] if self.class.unit_values[num_unit] && self.class.unit_values[num_unit][:denominator] 643 | end 644 | end 645 | 646 | num = num.flatten.compact 647 | den = den.flatten.compact 648 | num = UNITY_ARRAY if num.empty? 649 | base = self.class.new(self.class.eliminate_terms(q, num, den)) 650 | self.class.base_unit_cache.set(units, base) 651 | base * @scalar 652 | end 653 | 654 | alias base to_base 655 | 656 | # Generate human readable output. 657 | # If the name of a unit is passed, the unit will first be converted to the target unit before output. 658 | # some named conversions are available 659 | # 660 | # @example 661 | # unit.to_s(:ft) - outputs in feet and inches (e.g., 6'4") 662 | # unit.to_s(:lbs) - outputs in pounds and ounces (e.g, 8 lbs, 8 oz) 663 | # 664 | # You can also pass a standard format string (i.e., '%0.2f') 665 | # or a strftime format string. 666 | # 667 | # output is cached so subsequent calls for the same format will be fast 668 | # 669 | # @note Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..) 670 | # @param [Symbol] target_units 671 | # @param [Float] precision - the precision to use when converting to a rational 672 | # @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format 673 | # 674 | # @return [String] 675 | def to_s(target_units = nil, precision: 0.0001, format: RubyUnits.configuration.format) 676 | out = @output[target_units] 677 | return out if out 678 | 679 | separator = RubyUnits.configuration.separator 680 | case target_units 681 | when :ft 682 | feet, inches = convert_to("in").scalar.abs.divmod(12) 683 | improper, frac = inches.divmod(1) 684 | frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}" 685 | out = "#{negative? ? '-' : nil}#{feet}'#{improper}#{frac}\"" 686 | when :lbs 687 | pounds, ounces = convert_to("oz").scalar.abs.divmod(16) 688 | improper, frac = ounces.divmod(1) 689 | frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}" 690 | out = "#{negative? ? '-' : nil}#{pounds}#{separator}lbs #{improper}#{frac}#{separator}oz" 691 | when :stone 692 | stone, pounds = convert_to("lbs").scalar.abs.divmod(14) 693 | improper, frac = pounds.divmod(1) 694 | frac = frac.zero? ? "" : "-#{frac.rationalize(precision)}" 695 | out = "#{negative? ? '-' : nil}#{stone}#{separator}stone #{improper}#{frac}#{separator}lbs" 696 | when String 697 | out = case target_units.strip 698 | when /\A\s*\Z/ # whitespace only 699 | "" 700 | when /(%[-+.\w#]+)\s*(.+)*/ # format string like '%0.2f in' 701 | begin 702 | if Regexp.last_match(2) # unit specified, need to convert 703 | convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1), format: format) 704 | else 705 | "#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units(format: format)}".strip 706 | end 707 | rescue StandardError # parse it like a strftime format string 708 | (DateTime.new(0) + self).strftime(target_units) 709 | end 710 | when /(\S+)/ # unit only 'mm' or '1/mm' 711 | convert_to(Regexp.last_match(1)).to_s(format: format) 712 | else 713 | raise "unhandled case" 714 | end 715 | else 716 | out = case @scalar 717 | when Complex 718 | "#{@scalar}#{separator}#{units(format: format)}" 719 | when Rational 720 | "#{@scalar == @scalar.to_i ? @scalar.to_i : @scalar}#{separator}#{units(format: format)}" 721 | else 722 | "#{'%g' % @scalar}#{separator}#{units(format: format)}" 723 | end.strip 724 | end 725 | @output[target_units] = out 726 | out 727 | end 728 | 729 | # Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump' 730 | # @deprecated 731 | # @return [String] 732 | def inspect(dump = nil) 733 | return super() if dump 734 | 735 | to_s 736 | end 737 | 738 | # true if unit is a 'temperature', false if a 'degree' or anything else 739 | # @return [Boolean] 740 | # @todo use unit definition to determine if it's a temperature instead of a regex 741 | def temperature? 742 | degree? && units.match?(self.class.temp_regex) 743 | end 744 | 745 | alias is_temperature? temperature? 746 | 747 | # true if a degree unit or equivalent. 748 | # @return [Boolean] 749 | def degree? 750 | kind == :temperature 751 | end 752 | 753 | alias is_degree? degree? 754 | 755 | # returns the 'degree' unit associated with a temperature unit 756 | # @example '100 tempC'.to_unit.temperature_scale #=> 'degC' 757 | # @return [String] possible values: degC, degF, degR, or degK 758 | def temperature_scale 759 | return nil unless temperature? 760 | 761 | "deg#{self.class.unit_map[units][/temp([CFRK])/, 1]}" 762 | end 763 | 764 | # returns true if no associated units 765 | # false, even if the units are "unitless" like 'radians, each, etc' 766 | # @return [Boolean] 767 | def unitless? 768 | @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY 769 | end 770 | 771 | # Compare two Unit objects. Throws an exception if they are not of compatible types. 772 | # Comparisons are done based on the value of the unit in base SI units. 773 | # @param [Object] other 774 | # @return [Integer,nil] 775 | # @raise [NoMethodError] when other does not define <=> 776 | # @raise [ArgumentError] when units are not compatible 777 | def <=>(other) 778 | raise NoMethodError, "undefined method `<=>' for #{base_scalar.inspect}" unless base_scalar.respond_to?(:<=>) 779 | 780 | if other.nil? 781 | base_scalar <=> nil 782 | elsif !temperature? && other.respond_to?(:zero?) && other.zero? 783 | base_scalar <=> 0 784 | elsif other.instance_of?(Unit) 785 | raise ArgumentError, "Incompatible Units ('#{units}' not compatible with '#{other.units}')" unless self =~ other 786 | 787 | base_scalar <=> other.base_scalar 788 | else 789 | x, y = coerce(other) 790 | y <=> x 791 | end 792 | end 793 | 794 | # Compare Units for equality 795 | # this is necessary mostly for Complex units. Complex units do not have a <=> operator 796 | # so we define this one here so that we can properly check complex units for equality. 797 | # Units of incompatible types are not equal, except when they are both zero and neither is a temperature 798 | # Equality checks can be tricky since round off errors may make essentially equivalent units 799 | # appear to be different. 800 | # @param [Object] other 801 | # @return [Boolean] 802 | def ==(other) 803 | if other.respond_to?(:zero?) && other.zero? 804 | zero? 805 | elsif other.instance_of?(Unit) 806 | return false unless self =~ other 807 | 808 | base_scalar == other.base_scalar 809 | else 810 | begin 811 | x, y = coerce(other) 812 | x == y 813 | rescue ArgumentError # return false when object cannot be coerced 814 | false 815 | end 816 | end 817 | end 818 | 819 | # Check to see if units are compatible, ignoring the scalar part. This check is done by comparing unit signatures 820 | # for performance reasons. If passed a string, this will create a [Unit] object with the string and then do the 821 | # comparison. 822 | # 823 | # @example this permits a syntax like: 824 | # unit =~ "mm" 825 | # @note if you want to do a regexp comparison of the unit string do this ... 826 | # unit.units =~ /regexp/ 827 | # @param [Object] other 828 | # @return [Boolean] 829 | def =~(other) 830 | return signature == other.signature if other.is_a?(Unit) 831 | 832 | x, y = coerce(other) 833 | x =~ y 834 | rescue ArgumentError # return false when `other` cannot be converted to a [Unit] 835 | false 836 | end 837 | 838 | alias compatible? =~ 839 | alias compatible_with? =~ 840 | 841 | # Compare two units. Returns true if quantities and units match 842 | # @example 843 | # RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("100 cm") # => true 844 | # RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("1 m") # => false 845 | # @param [Object] other 846 | # @return [Boolean] 847 | def ===(other) 848 | case other 849 | when Unit 850 | (scalar == other.scalar) && (units == other.units) 851 | else 852 | begin 853 | x, y = coerce(other) 854 | x.same_as?(y) 855 | rescue ArgumentError 856 | false 857 | end 858 | end 859 | end 860 | 861 | alias same? === 862 | alias same_as? === 863 | 864 | # Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately 865 | # throws an exception if the units are not compatible. 866 | # It is possible to add Time objects to units of time 867 | # @param [Object] other 868 | # @return [Unit] 869 | # @raise [ArgumentError] when two temperatures are added 870 | # @raise [ArgumentError] when units are not compatible 871 | # @raise [ArgumentError] when adding a fixed time or date to a time span 872 | def +(other) 873 | case other 874 | when Unit 875 | if zero? 876 | other.dup 877 | elsif self =~ other 878 | raise ArgumentError, "Cannot add two temperatures" if [self, other].all?(&:temperature?) 879 | 880 | if temperature? 881 | self.class.new(scalar: (scalar + other.convert_to(temperature_scale).scalar), numerator: @numerator, denominator: @denominator, signature: @signature) 882 | elsif other.temperature? 883 | self.class.new(scalar: (other.scalar + convert_to(other.temperature_scale).scalar), numerator: other.numerator, denominator: other.denominator, signature: other.signature) 884 | else 885 | self.class.new(scalar: (base_scalar + other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self) 886 | end 887 | else 888 | raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" 889 | end 890 | when Date, Time 891 | raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit" 892 | else 893 | x, y = coerce(other) 894 | y + x 895 | end 896 | end 897 | 898 | # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately 899 | # @param [Numeric] other 900 | # @return [Unit] 901 | # @raise [ArgumentError] when subtracting a temperature from a degree 902 | # @raise [ArgumentError] when units are not compatible 903 | # @raise [ArgumentError] when subtracting a fixed time from a time span 904 | def -(other) 905 | case other 906 | when Unit 907 | if zero? 908 | if other.zero? 909 | other.dup * -1 # preserve Units class 910 | else 911 | -other.dup 912 | end 913 | elsif self =~ other 914 | if [self, other].all?(&:temperature?) 915 | self.class.new(scalar: (base_scalar - other.base_scalar), numerator: KELVIN, denominator: UNITY_ARRAY, signature: @signature).convert_to(temperature_scale) 916 | elsif temperature? 917 | self.class.new(scalar: (base_scalar - other.base_scalar), numerator: [""], denominator: UNITY_ARRAY, signature: @signature).convert_to(self) 918 | elsif other.temperature? 919 | raise ArgumentError, "Cannot subtract a temperature from a differential degree unit" 920 | else 921 | self.class.new(scalar: (base_scalar - other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self) 922 | end 923 | else 924 | raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" 925 | end 926 | when Time 927 | raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be subtracted from a Unit" 928 | else 929 | x, y = coerce(other) 930 | y - x 931 | end 932 | end 933 | 934 | # Multiply two units. 935 | # @param [Numeric] other 936 | # @return [Unit] 937 | # @raise [ArgumentError] when attempting to multiply two temperatures 938 | def *(other) 939 | case other 940 | when Unit 941 | raise ArgumentError, "Cannot multiply by temperatures" if [other, self].any?(&:temperature?) 942 | 943 | opts = self.class.eliminate_terms(@scalar * other.scalar, @numerator + other.numerator, @denominator + other.denominator) 944 | opts[:signature] = @signature + other.signature 945 | self.class.new(opts) 946 | when Numeric 947 | self.class.new(scalar: @scalar * other, numerator: @numerator, denominator: @denominator, signature: @signature) 948 | else 949 | x, y = coerce(other) 950 | x * y 951 | end 952 | end 953 | 954 | # Divide two units. 955 | # Throws an exception if divisor is 0 956 | # @param [Numeric] other 957 | # @return [Unit] 958 | # @raise [ZeroDivisionError] if divisor is zero 959 | # @raise [ArgumentError] if attempting to divide a temperature by another temperature 960 | def /(other) 961 | case other 962 | when Unit 963 | raise ZeroDivisionError if other.zero? 964 | raise ArgumentError, "Cannot divide with temperatures" if [other, self].any?(&:temperature?) 965 | 966 | sc = Rational(@scalar, other.scalar) 967 | sc = sc.numerator if sc.denominator == 1 968 | opts = self.class.eliminate_terms(sc, @numerator + other.denominator, @denominator + other.numerator) 969 | opts[:signature] = @signature - other.signature 970 | self.class.new(opts) 971 | when Numeric 972 | raise ZeroDivisionError if other.zero? 973 | 974 | sc = Rational(@scalar, other) 975 | sc = sc.numerator if sc.denominator == 1 976 | self.class.new(scalar: sc, numerator: @numerator, denominator: @denominator, signature: @signature) 977 | else 978 | x, y = coerce(other) 979 | y / x 980 | end 981 | end 982 | 983 | # Returns the remainder when one unit is divided by another 984 | # 985 | # @param [Unit] other 986 | # @return [Unit] 987 | # @raise [ArgumentError] if units are not compatible 988 | def remainder(other) 989 | raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other) 990 | 991 | self.class.new(base_scalar.remainder(other.to_unit.base_scalar), to_base.units).convert_to(self) 992 | end 993 | 994 | # Divide two units and return quotient and remainder 995 | # 996 | # @param [Unit] other 997 | # @return [Array(Integer, Unit)] 998 | # @raise [ArgumentError] if units are not compatible 999 | def divmod(other) 1000 | raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other) 1001 | 1002 | [quo(other).to_base.floor, self % other] 1003 | end 1004 | 1005 | # Perform a modulo on a unit, will raise an exception if the units are not compatible 1006 | # 1007 | # @param [Unit] other 1008 | # @return [Integer] 1009 | # @raise [ArgumentError] if units are not compatible 1010 | def %(other) 1011 | raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other) 1012 | 1013 | self.class.new(base_scalar % other.to_unit.base_scalar, to_base.units).convert_to(self) 1014 | end 1015 | alias modulo % 1016 | 1017 | # @param [Object] other 1018 | # @return [Unit] 1019 | # @raise [ZeroDivisionError] if other is zero 1020 | def quo(other) 1021 | self / other 1022 | end 1023 | alias fdiv quo 1024 | 1025 | # Exponentiation. Only takes integer powers. 1026 | # Note that anything raised to the power of 0 results in a [Unit] object with a scalar of 1, and no units. 1027 | # Throws an exception if exponent is not an integer. 1028 | # Ideally this routine should accept a float for the exponent 1029 | # It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator 1030 | # but, sadly, floats can't be converted to rationals. 1031 | # 1032 | # For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1 1033 | # @param [Numeric] other 1034 | # @return [Unit] 1035 | # @raise [ArgumentError] when raising a temperature to a power 1036 | # @raise [ArgumentError] when n not in the set integers from (1..9) 1037 | # @raise [ArgumentError] when attempting to raise to a complex number 1038 | # @raise [ArgumentError] when an invalid exponent is passed 1039 | def **(other) 1040 | raise ArgumentError, "Cannot raise a temperature to a power" if temperature? 1041 | 1042 | if other.is_a?(Numeric) 1043 | return inverse if other == -1 1044 | return self if other == 1 1045 | return 1 if other.zero? 1046 | end 1047 | case other 1048 | when Rational 1049 | power(other.numerator).root(other.denominator) 1050 | when Integer 1051 | power(other) 1052 | when Float 1053 | return self**other.to_i if other == other.to_i 1054 | 1055 | valid = (1..9).map { Rational(1, _1) } 1056 | raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs 1057 | 1058 | root(Rational(1, other).to_int) 1059 | when Complex 1060 | raise ArgumentError, "exponentiation of complex numbers is not supported." 1061 | else 1062 | raise ArgumentError, "Invalid Exponent" 1063 | end 1064 | end 1065 | 1066 | # returns the unit raised to the n-th power 1067 | # @param [Integer] n 1068 | # @return [Unit] 1069 | # @raise [ArgumentError] when attempting to raise a temperature to a power 1070 | # @raise [ArgumentError] when n is not an integer 1071 | def power(n) 1072 | raise ArgumentError, "Cannot raise a temperature to a power" if temperature? 1073 | raise ArgumentError, "Exponent must an Integer" unless n.is_a?(Integer) 1074 | return inverse if n == -1 1075 | return 1 if n.zero? 1076 | return self if n == 1 1077 | return (1..(n - 1).to_i).inject(self) { |acc, _elem| acc * self } if n >= 0 1078 | 1079 | (1..-(n - 1).to_i).inject(self) { |acc, _elem| acc / self } 1080 | end 1081 | 1082 | # Calculates the n-th root of a unit 1083 | # if n < 0, returns 1/unit^(1/n) 1084 | # @param [Integer] n 1085 | # @return [Unit] 1086 | # @raise [ArgumentError] when attempting to take the root of a temperature 1087 | # @raise [ArgumentError] when n is not an integer 1088 | # @raise [ArgumentError] when n is 0 1089 | def root(n) 1090 | raise ArgumentError, "Cannot take the root of a temperature" if temperature? 1091 | raise ArgumentError, "Exponent must an Integer" unless n.is_a?(Integer) 1092 | raise ArgumentError, "0th root undefined" if n.zero? 1093 | return self if n == 1 1094 | return root(n.abs).inverse if n.negative? 1095 | 1096 | vec = unit_signature_vector 1097 | vec = vec.map { _1 % n } 1098 | raise ArgumentError, "Illegal root" unless vec.max.zero? 1099 | 1100 | num = @numerator.dup 1101 | den = @denominator.dup 1102 | 1103 | @numerator.uniq.each do |item| 1104 | x = num.find_all { _1 == item }.size 1105 | r = ((x / n) * (n - 1)).to_int 1106 | r.times { num.delete_at(num.index(item)) } 1107 | end 1108 | 1109 | @denominator.uniq.each do |item| 1110 | x = den.find_all { _1 == item }.size 1111 | r = ((x / n) * (n - 1)).to_int 1112 | r.times { den.delete_at(den.index(item)) } 1113 | end 1114 | self.class.new(scalar: @scalar**Rational(1, n), numerator: num, denominator: den) 1115 | end 1116 | 1117 | # returns inverse of Unit (1/unit) 1118 | # @return [Unit] 1119 | def inverse 1120 | self.class.new("1") / self 1121 | end 1122 | 1123 | # convert to a specified unit string or to the same units as another Unit 1124 | # 1125 | # unit.convert_to "kg" will covert to kilograms 1126 | # unit1.convert_to unit2 converts to same units as unit2 object 1127 | # 1128 | # To convert a Unit object to match another Unit object, use: 1129 | # unit1 >>= unit2 1130 | # 1131 | # Special handling for temperature conversions is supported. If the Unit 1132 | # object is converted from one temperature unit to another, the proper 1133 | # temperature offsets will be used. Supports Kelvin, Celsius, Fahrenheit, 1134 | # and Rankine scales. 1135 | # 1136 | # @note If temperature is part of a compound unit, the temperature will be 1137 | # treated as a differential and the units will be scaled appropriately. 1138 | # @note When converting units with Integer scalars, the scalar will be 1139 | # converted to a Rational to avoid unexpected behavior caused by Integer 1140 | # division. 1141 | # @param other [Unit, String] 1142 | # @return [Unit] 1143 | # @raise [ArgumentError] when attempting to convert a degree to a temperature 1144 | # @raise [ArgumentError] when target unit is unknown 1145 | # @raise [ArgumentError] when target unit is incompatible 1146 | def convert_to(other) 1147 | return self if other.nil? 1148 | return self if other.is_a?(TrueClass) 1149 | return self if other.is_a?(FalseClass) 1150 | 1151 | if (other.is_a?(Unit) && other.temperature?) || (other.is_a?(String) && other =~ self.class.temp_regex) 1152 | raise ArgumentError, "Receiver is not a temperature unit" unless degree? 1153 | 1154 | start_unit = units 1155 | # @type [String] 1156 | target_unit = case other 1157 | when Unit 1158 | other.units 1159 | when String 1160 | other 1161 | else 1162 | raise ArgumentError, "Unknown target units" 1163 | end 1164 | return self if target_unit == start_unit 1165 | 1166 | # @type [Numeric] 1167 | @base_scalar ||= case self.class.unit_map[start_unit] 1168 | when "" 1169 | @scalar + 273.15 1170 | when "" 1171 | @scalar 1172 | when "" 1173 | (@scalar + 459.67).to_r * Rational(5, 9) 1174 | when "" 1175 | @scalar.to_r * Rational(5, 9) 1176 | end 1177 | # @type [Numeric] 1178 | q = case self.class.unit_map[target_unit] 1179 | when "" 1180 | @base_scalar - 273.15 1181 | when "" 1182 | @base_scalar 1183 | when "" 1184 | (@base_scalar.to_r * Rational(9, 5)) - 459.67r 1185 | when "" 1186 | @base_scalar.to_r * Rational(9, 5) 1187 | end 1188 | self.class.new("#{q} #{target_unit}") 1189 | else 1190 | # @type [Unit] 1191 | target = case other 1192 | when Unit 1193 | other 1194 | when String 1195 | self.class.new(other) 1196 | else 1197 | raise ArgumentError, "Unknown target units" 1198 | end 1199 | return self if target.units == units 1200 | 1201 | raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ target 1202 | 1203 | numerator1 = @numerator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact 1204 | denominator1 = @denominator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact 1205 | numerator2 = target.numerator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact 1206 | denominator2 = target.denominator.map { self.class.prefix_values[_1] || _1 }.map { _1.is_a?(Numeric) ? _1 : self.class.unit_values[_1][:scalar] }.compact 1207 | 1208 | # If the scalar is an Integer, convert it to a Rational number so that 1209 | # if the value is scaled during conversion, resolution is not lost due 1210 | # to integer math 1211 | # @type [Rational, Numeric] 1212 | conversion_scalar = @scalar.is_a?(Integer) ? @scalar.to_r : @scalar 1213 | q = conversion_scalar * (numerator1 + denominator2).reduce(1, :*) / (numerator2 + denominator1).reduce(1, :*) 1214 | # Convert the scalar to an Integer if the result is equivalent to an 1215 | # integer 1216 | q = q.to_i if @scalar.is_a?(Integer) && q.to_i == q 1217 | self.class.new(scalar: q, numerator: target.numerator, denominator: target.denominator, signature: target.signature) 1218 | end 1219 | end 1220 | 1221 | alias >> convert_to 1222 | alias to convert_to 1223 | 1224 | # converts the unit back to a float if it is unitless. Otherwise raises an exception 1225 | # @return [Float] 1226 | # @raise [RuntimeError] when not unitless 1227 | def to_f 1228 | return @scalar.to_f if unitless? 1229 | 1230 | raise "Cannot convert '#{self}' to Float unless unitless. Use Unit#scalar" 1231 | end 1232 | 1233 | # converts the unit back to a complex if it is unitless. Otherwise raises an exception 1234 | # @return [Complex] 1235 | # @raise [RuntimeError] when not unitless 1236 | def to_c 1237 | return Complex(@scalar) if unitless? 1238 | 1239 | raise "Cannot convert '#{self}' to Complex unless unitless. Use Unit#scalar" 1240 | end 1241 | 1242 | # if unitless, returns an int, otherwise raises an error 1243 | # @return [Integer] 1244 | # @raise [RuntimeError] when not unitless 1245 | def to_i 1246 | return @scalar.to_int if unitless? 1247 | 1248 | raise "Cannot convert '#{self}' to Integer unless unitless. Use Unit#scalar" 1249 | end 1250 | 1251 | alias to_int to_i 1252 | 1253 | # if unitless, returns a Rational, otherwise raises an error 1254 | # @return [Rational] 1255 | # @raise [RuntimeError] when not unitless 1256 | def to_r 1257 | return @scalar.to_r if unitless? 1258 | 1259 | raise "Cannot convert '#{self}' to Rational unless unitless. Use Unit#scalar" 1260 | end 1261 | 1262 | # Returns string formatted for json 1263 | # @return [String] 1264 | def as_json(*) 1265 | to_s 1266 | end 1267 | 1268 | # Returns the 'unit' part of the Unit object without the scalar 1269 | # 1270 | # @param with_prefix [Boolean] include prefixes in output 1271 | # @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format 1272 | # 1273 | # @return [String] 1274 | def units(with_prefix: true, format: nil) 1275 | return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY 1276 | 1277 | output_numerator = ["1"] 1278 | output_denominator = [] 1279 | num = @numerator.clone.compact 1280 | den = @denominator.clone.compact 1281 | 1282 | unless num == UNITY_ARRAY 1283 | definitions = num.map { self.class.definition(_1) } 1284 | definitions.reject!(&:prefix?) unless with_prefix 1285 | definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a 1286 | output_numerator = definitions.map { _1.map(&:display_name).join } 1287 | end 1288 | 1289 | unless den == UNITY_ARRAY 1290 | definitions = den.map { self.class.definition(_1) } 1291 | definitions.reject!(&:prefix?) unless with_prefix 1292 | definitions = definitions.chunk_while { |definition, _| definition.prefix? }.to_a 1293 | output_denominator = definitions.map { _1.map(&:display_name).join } 1294 | end 1295 | 1296 | on = output_numerator 1297 | .uniq 1298 | .map { [_1, output_numerator.count(_1)] } 1299 | .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : "")) } 1300 | 1301 | if format == :exponential 1302 | od = output_denominator 1303 | .uniq 1304 | .map { [_1, output_denominator.count(_1)] } 1305 | .map { |element, power| (element.to_s.strip + (power.positive? ? "^#{-power}" : "")) } 1306 | (on + od).join("*").strip 1307 | else 1308 | od = output_denominator 1309 | .uniq 1310 | .map { [_1, output_denominator.count(_1)] } 1311 | .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : "")) } 1312 | "#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip 1313 | end 1314 | end 1315 | 1316 | # negates the scalar of the Unit 1317 | # @return [Numeric,Unit] 1318 | def -@ 1319 | return -@scalar if unitless? 1320 | 1321 | dup * -1 1322 | end 1323 | 1324 | # absolute value of a unit 1325 | # @return [Numeric,Unit] 1326 | def abs 1327 | return @scalar.abs if unitless? 1328 | 1329 | self.class.new(@scalar.abs, @numerator, @denominator) 1330 | end 1331 | 1332 | # ceil of a unit 1333 | # @return [Numeric,Unit] 1334 | def ceil(*args) 1335 | return @scalar.ceil(*args) if unitless? 1336 | 1337 | self.class.new(@scalar.ceil(*args), @numerator, @denominator) 1338 | end 1339 | 1340 | # @return [Numeric,Unit] 1341 | def floor(*args) 1342 | return @scalar.floor(*args) if unitless? 1343 | 1344 | self.class.new(@scalar.floor(*args), @numerator, @denominator) 1345 | end 1346 | 1347 | # Round the unit according to the rules of the scalar's class. Call this 1348 | # with the arguments appropriate for the scalar's class (e.g., Integer, 1349 | # Rational, etc..). Because unit conversions can often result in Rational 1350 | # scalars (to preserve precision), it may be advisable to use +to_s+ to 1351 | # format output instead of using +round+. 1352 | # @example 1353 | # RubyUnits::Unit.new('21870 mm/min').convert_to('m/min').round(1) #=> 2187/100 m/min 1354 | # RubyUnits::Unit.new('21870 mm/min').convert_to('m/min').to_s('%0.1f') #=> 21.9 m/min 1355 | # 1356 | # @return [Numeric,Unit] 1357 | def round(*args, **kwargs) 1358 | return @scalar.round(*args, **kwargs) if unitless? 1359 | 1360 | self.class.new(@scalar.round(*args, **kwargs), @numerator, @denominator) 1361 | end 1362 | 1363 | # @return [Numeric, Unit] 1364 | def truncate(*args) 1365 | return @scalar.truncate(*args) if unitless? 1366 | 1367 | self.class.new(@scalar.truncate(*args), @numerator, @denominator) 1368 | end 1369 | 1370 | # returns next unit in a range. '1 mm'.to_unit.succ #=> '2 mm'.to_unit 1371 | # only works when the scalar is an integer 1372 | # @return [Unit] 1373 | # @raise [ArgumentError] when scalar is not equal to an integer 1374 | def succ 1375 | raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i 1376 | 1377 | self.class.new(@scalar.to_i.succ, @numerator, @denominator) 1378 | end 1379 | 1380 | alias next succ 1381 | 1382 | # returns previous unit in a range. '2 mm'.to_unit.pred #=> '1 mm'.to_unit 1383 | # only works when the scalar is an integer 1384 | # @return [Unit] 1385 | # @raise [ArgumentError] when scalar is not equal to an integer 1386 | def pred 1387 | raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i 1388 | 1389 | self.class.new(@scalar.to_i.pred, @numerator, @denominator) 1390 | end 1391 | 1392 | # Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch. 1393 | # @return [Time] 1394 | def to_time 1395 | Time.at(self) 1396 | end 1397 | 1398 | alias time to_time 1399 | 1400 | # convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date 1401 | # defined by DateTime 1402 | # @return [::DateTime] 1403 | def to_datetime 1404 | DateTime.new!(convert_to("d").scalar) 1405 | end 1406 | 1407 | # @return [Date] 1408 | def to_date 1409 | Date.new0(convert_to("d").scalar) 1410 | end 1411 | 1412 | # true if scalar is zero 1413 | # @return [Boolean] 1414 | def zero? 1415 | base_scalar.zero? 1416 | end 1417 | 1418 | # @example '5 min'.to_unit.ago 1419 | # @return [Unit] 1420 | def ago 1421 | before 1422 | end 1423 | 1424 | # @example '5 min'.before(time) 1425 | # @return [Unit] 1426 | def before(time_point = ::Time.now) 1427 | case time_point 1428 | when Time, Date, DateTime 1429 | (begin 1430 | time_point - self 1431 | rescue StandardError 1432 | time_point.to_datetime - self 1433 | end) 1434 | else 1435 | raise ArgumentError, "Must specify a Time, Date, or DateTime" 1436 | end 1437 | end 1438 | 1439 | alias before_now before 1440 | 1441 | # @example 'min'.since(time) 1442 | # @param [Time, Date, DateTime] time_point 1443 | # @return [Unit] 1444 | # @raise [ArgumentError] when time point is not a Time, Date, or DateTime 1445 | def since(time_point) 1446 | case time_point 1447 | when Time 1448 | self.class.new(::Time.now - time_point, "second").convert_to(self) 1449 | when DateTime, Date 1450 | self.class.new(::DateTime.now - time_point, "day").convert_to(self) 1451 | else 1452 | raise ArgumentError, "Must specify a Time, Date, or DateTime" 1453 | end 1454 | end 1455 | 1456 | # @example 'min'.until(time) 1457 | # @param [Time, Date, DateTime] time_point 1458 | # @return [Unit] 1459 | def until(time_point) 1460 | case time_point 1461 | when Time 1462 | self.class.new(time_point - ::Time.now, "second").convert_to(self) 1463 | when DateTime, Date 1464 | self.class.new(time_point - ::DateTime.now, "day").convert_to(self) 1465 | else 1466 | raise ArgumentError, "Must specify a Time, Date, or DateTime" 1467 | end 1468 | end 1469 | 1470 | # @example '5 min'.from(time) 1471 | # @param [Time, Date, DateTime] time_point 1472 | # @return [Time, Date, DateTime] 1473 | # @raise [ArgumentError] when passed argument is not a Time, Date, or DateTime 1474 | def from(time_point) 1475 | case time_point 1476 | when Time, DateTime, Date 1477 | (begin 1478 | time_point + self 1479 | rescue StandardError 1480 | time_point.to_datetime + self 1481 | end) 1482 | else 1483 | raise ArgumentError, "Must specify a Time, Date, or DateTime" 1484 | end 1485 | end 1486 | 1487 | alias after from 1488 | alias from_now from 1489 | 1490 | # Automatically coerce objects to [Unit] when possible. If an object defines a '#to_unit' method, it will be coerced 1491 | # using that method. 1492 | # 1493 | # @param other [Object, #to_unit] 1494 | # @return [Array(Unit, Unit)] 1495 | # @raise [ArgumentError] when `other` cannot be converted to a [Unit] 1496 | def coerce(other) 1497 | return [other.to_unit, self] if other.respond_to?(:to_unit) 1498 | 1499 | [self.class.new(other), self] 1500 | end 1501 | 1502 | # Returns a new unit that has been scaled to be more in line with typical usage. This is highly opinionated and not 1503 | # based on any standard. It is intended to be used to make the units more human readable. 1504 | # 1505 | # Some key points: 1506 | # * Units containing 'kg' will be returned as is. The prefix in 'kg' makes this an odd case. 1507 | # * It will use `centi` instead of `milli` when the scalar is between 0.01 and 0.001 1508 | # 1509 | # @return [Unit] 1510 | def best_prefix 1511 | return to_base if scalar.zero? 1512 | return self if units.include?("kg") 1513 | 1514 | best_prefix = if kind == :information 1515 | self.class.prefix_values.key(2**((::Math.log(base_scalar, 2) / 10.0).floor * 10)) 1516 | elsif ((1/100r)..(1/10r)).cover?(base_scalar) 1517 | self.class.prefix_values.key(1/100r) 1518 | else 1519 | self.class.prefix_values.key(10**((::Math.log10(base_scalar) / 3.0).floor * 3)) 1520 | end 1521 | to(self.class.new(self.class.prefix_map.key(best_prefix) + units(with_prefix: false))) 1522 | end 1523 | 1524 | # override hash method so objects with same values are considered equal 1525 | def hash 1526 | [ 1527 | @scalar, 1528 | @numerator, 1529 | @denominator, 1530 | @base, 1531 | @signature, 1532 | @base_scalar, 1533 | @unit_name 1534 | ].hash 1535 | end 1536 | 1537 | # Protected and Private Functions that should only be called from this class 1538 | protected 1539 | 1540 | # figure out what the scalar part of the base unit for this unit is 1541 | # @return [nil] 1542 | def update_base_scalar 1543 | if base? 1544 | @base_scalar = @scalar 1545 | @signature = unit_signature 1546 | else 1547 | base = to_base 1548 | @base_scalar = base.scalar 1549 | @signature = base.signature 1550 | end 1551 | end 1552 | 1553 | # calculates the unit signature vector used by unit_signature 1554 | # @return [Array] 1555 | # @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20 1556 | def unit_signature_vector 1557 | return to_base.unit_signature_vector unless base? 1558 | 1559 | vector = ::Array.new(SIGNATURE_VECTOR.size, 0) 1560 | # it's possible to have a kind that misses the array... kinds like :counting 1561 | # are more like prefixes, so don't use them to calculate the vector 1562 | @numerator.map { self.class.definition(_1) }.each do |definition| 1563 | index = SIGNATURE_VECTOR.index(definition.kind) 1564 | vector[index] += 1 if index 1565 | end 1566 | @denominator.map { self.class.definition(_1) }.each do |definition| 1567 | index = SIGNATURE_VECTOR.index(definition.kind) 1568 | vector[index] -= 1 if index 1569 | end 1570 | raise ArgumentError, "Power out of range (-20 < net power of a unit < 20)" if vector.any? { _1.abs >= 20 } 1571 | 1572 | vector 1573 | end 1574 | 1575 | private 1576 | 1577 | # used by #dup to duplicate a Unit 1578 | # @param [Unit] other 1579 | # @private 1580 | def initialize_copy(other) 1581 | @numerator = other.numerator.dup 1582 | @denominator = other.denominator.dup 1583 | end 1584 | 1585 | # calculates the unit signature id for use in comparing compatible units and simplification 1586 | # the signature is based on a simple classification of units and is based on the following publication 1587 | # 1588 | # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering, 21(8), Aug 1995, pp.651-661 1589 | # @see http://doi.ieeecomputersociety.org/10.1109/32.403789 1590 | # @return [Array] 1591 | def unit_signature 1592 | return @signature unless @signature.nil? 1593 | 1594 | vector = unit_signature_vector 1595 | vector.each_with_index { |item, index| vector[index] = item * (20**index) } 1596 | @signature = vector.inject(0) { |acc, elem| acc + elem } 1597 | @signature 1598 | end 1599 | 1600 | # parse a string into a unit object. 1601 | # Typical formats like : 1602 | # "5.6 kg*m/s^2" 1603 | # "5.6 kg*m*s^-2" 1604 | # "5.6 kilogram*meter*second^-2" 1605 | # "2.2 kPa" 1606 | # "37 degC" 1607 | # "1" -- creates a unitless constant with value 1 1608 | # "GPa" -- creates a unit with scalar 1 with units 'GPa' 1609 | # 6'4" -- recognized as 6 feet + 4 inches 1610 | # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces 1611 | # @return [nil,RubyUnits::Unit] 1612 | # @todo This should either be a separate class or at least a class method 1613 | def parse(passed_unit_string = "0") 1614 | unit_string = passed_unit_string.dup 1615 | unit_string = "#{Regexp.last_match(1)} USD" if unit_string =~ /\$\s*(#{NUMBER_REGEX})/ 1616 | unit_string.gsub!("\u00b0".encode("utf-8"), "deg") if unit_string.encoding == Encoding::UTF_8 1617 | 1618 | unit_string.gsub!(/(\d)[_,](\d)/, '\1\2') # remove underscores and commas in numbers 1619 | 1620 | unit_string.gsub!(/[%'"#]/, "%" => "percent", "'" => "feet", '"' => "inch", "#" => "pound") 1621 | if unit_string.start_with?(COMPLEX_NUMBER) 1622 | match = unit_string.match(COMPLEX_REGEX) 1623 | real = Float(match[:real]) if match[:real] 1624 | imaginary = Float(match[:imaginary]) 1625 | unit_s = match[:unit] 1626 | real = real.to_i if real.to_i == real 1627 | imaginary = imaginary.to_i if imaginary.to_i == imaginary 1628 | complex = Complex(real || 0, imaginary) 1629 | complex = complex.to_i if complex.imaginary.zero? && complex.real == complex.real.to_i 1630 | result = self.class.new(unit_s || 1) * complex 1631 | copy(result) 1632 | return 1633 | end 1634 | 1635 | if unit_string.start_with?(RATIONAL_NUMBER) 1636 | match = unit_string.match(RATIONAL_REGEX) 1637 | numerator = Integer(match[:numerator]) 1638 | denominator = Integer(match[:denominator]) 1639 | raise ArgumentError, "Improper fractions must have a whole number part" if !match[:proper].nil? && !match[:proper].match?(/^#{INTEGER_REGEX}$/) 1640 | 1641 | proper = match[:proper].to_i 1642 | unit_s = match[:unit] 1643 | rational = if proper.negative? 1644 | (proper - Rational(numerator, denominator)) 1645 | else 1646 | (proper + Rational(numerator, denominator)) 1647 | end 1648 | rational = rational.to_int if rational.to_int == rational 1649 | result = self.class.new(unit_s || 1) * rational 1650 | copy(result) 1651 | return 1652 | end 1653 | 1654 | match = unit_string.match(NUMBER_REGEX) 1655 | unit = self.class.cached.get(match[:unit]) 1656 | mult = match[:scalar] == "" ? 1.0 : match[:scalar].to_f 1657 | mult = mult.to_int if mult.to_int == mult 1658 | 1659 | if unit 1660 | copy(unit) 1661 | @scalar *= mult 1662 | @base_scalar *= mult 1663 | return self 1664 | end 1665 | 1666 | while unit_string.gsub!(/<(#{self.class.prefix_regex})><(#{self.class.unit_regex})>/, '<\1\2>') 1667 | # replace with 1668 | end 1669 | while unit_string.gsub!(/<#{self.class.unit_match_regex}><#{self.class.unit_match_regex}>/, '<\1\2>*<\3\4>') 1670 | # collapse into *... 1671 | end 1672 | # ... and then strip the remaining brackets for x*y*z 1673 | unit_string.gsub!(/[<>]/, "") 1674 | 1675 | if (match = unit_string.match(TIME_REGEX)) 1676 | hours = match[:hour] 1677 | minutes = match[:min] 1678 | seconds = match[:sec] 1679 | milliseconds = match[:msec] 1680 | raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, milliseconds].all?(&:nil?) 1681 | 1682 | result = self.class.new("#{hours || 0} hours") + 1683 | self.class.new("#{minutes || 0} minutes") + 1684 | self.class.new("#{seconds || 0} seconds") + 1685 | self.class.new("#{milliseconds || 0} milliseconds") 1686 | copy(result) 1687 | return 1688 | end 1689 | 1690 | # Special processing for unusual unit strings 1691 | # feet -- 6'5" 1692 | if (match = unit_string.match(FEET_INCH_REGEX)) 1693 | feet = Integer(match[:feet]) 1694 | inches = match[:inches] 1695 | result = if feet.negative? 1696 | self.class.new("#{feet} ft") - self.class.new("#{inches} inches") 1697 | else 1698 | self.class.new("#{feet} ft") + self.class.new("#{inches} inches") 1699 | end 1700 | copy(result) 1701 | return 1702 | end 1703 | 1704 | # weight -- 8 lbs 12 oz 1705 | if (match = unit_string.match(LBS_OZ_REGEX)) 1706 | pounds = Integer(match[:pounds]) 1707 | oz = match[:oz] 1708 | result = if pounds.negative? 1709 | self.class.new("#{pounds} lbs") - self.class.new("#{oz} oz") 1710 | else 1711 | self.class.new("#{pounds} lbs") + self.class.new("#{oz} oz") 1712 | end 1713 | copy(result) 1714 | return 1715 | end 1716 | 1717 | # stone -- 3 stone 5, 2 stone, 14 stone 3 pounds, etc. 1718 | if (match = unit_string.match(STONE_LB_REGEX)) 1719 | stone = Integer(match[:stone]) 1720 | pounds = match[:pounds] 1721 | result = if stone.negative? 1722 | self.class.new("#{stone} stone") - self.class.new("#{pounds} lbs") 1723 | else 1724 | self.class.new("#{stone} stone") + self.class.new("#{pounds} lbs") 1725 | end 1726 | copy(result) 1727 | return 1728 | end 1729 | 1730 | # more than one per. I.e., "1 m/s/s" 1731 | raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count("/") > 1 1732 | raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized #{unit_string}") if unit_string =~ /\s[02-9]/ 1733 | 1734 | @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] # parse the string into parts 1735 | top.scan(TOP_REGEX).each do |item| 1736 | n = item[1].to_i 1737 | x = "#{item[0]} " 1738 | if n >= 0 1739 | top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) { x * n } 1740 | elsif n.negative? 1741 | bottom = "#{bottom} #{x * -n}" 1742 | top.gsub!(/#{item[0]}(\^|\*\*)#{n}/, "") 1743 | end 1744 | end 1745 | if bottom 1746 | bottom.gsub!(BOTTOM_REGEX) { "#{Regexp.last_match(1)} " * Regexp.last_match(2).to_i } 1747 | # Separate leading decimal from denominator, if any 1748 | bottom_scalar, bottom = bottom.scan(NUMBER_UNIT_REGEX)[0] 1749 | end 1750 | 1751 | @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty? 1752 | @scalar = 1 unless @scalar.is_a? Numeric 1753 | @scalar = @scalar.to_int if @scalar.to_int == @scalar 1754 | 1755 | bottom_scalar = 1 if bottom_scalar.nil? || bottom_scalar.empty? 1756 | bottom_scalar = if bottom_scalar.to_i == bottom_scalar 1757 | bottom_scalar.to_i 1758 | else 1759 | bottom_scalar.to_f 1760 | end 1761 | 1762 | @scalar /= bottom_scalar 1763 | 1764 | @numerator ||= UNITY_ARRAY 1765 | @denominator ||= UNITY_ARRAY 1766 | @numerator = top.scan(self.class.unit_match_regex).delete_if(&:empty?).compact if top 1767 | @denominator = bottom.scan(self.class.unit_match_regex).delete_if(&:empty?).compact if bottom 1768 | 1769 | # eliminate all known terms from this string. This is a quick check to see if the passed unit 1770 | # contains terms that are not defined. 1771 | used = "#{top} #{bottom}".to_s.gsub(self.class.unit_match_regex, "").gsub(%r{[\d*, "'_^/$]}, "") 1772 | raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty? 1773 | 1774 | @numerator = @numerator.map do |item| 1775 | self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]] 1776 | end.flatten.compact.delete_if(&:empty?) 1777 | 1778 | @denominator = @denominator.map do |item| 1779 | self.class.prefix_map[item[0]] ? [self.class.prefix_map[item[0]], self.class.unit_map[item[1]]] : [self.class.unit_map[item[1]]] 1780 | end.flatten.compact.delete_if(&:empty?) 1781 | 1782 | @numerator = UNITY_ARRAY if @numerator.empty? 1783 | @denominator = UNITY_ARRAY if @denominator.empty? 1784 | self 1785 | end 1786 | end 1787 | end 1788 | -------------------------------------------------------------------------------- /lib/ruby_units/unit_definitions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "unit_definitions/prefix" 4 | require_relative "unit_definitions/base" 5 | require_relative "unit_definitions/standard" 6 | -------------------------------------------------------------------------------- /lib/ruby_units/unit_definitions/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # seed the cache 4 | RubyUnits::Unit.new("1") 5 | 6 | RubyUnits::Unit.define("meter") do |unit| 7 | unit.scalar = 1 8 | unit.numerator = %w[] 9 | unit.aliases = %w[m meter meters metre metres] 10 | unit.kind = :length 11 | end 12 | 13 | RubyUnits::Unit.define("kilogram") do |unit| 14 | unit.scalar = 1 15 | unit.numerator = %w[] 16 | unit.aliases = %w[kg kilogram kilograms] 17 | unit.kind = :mass 18 | end 19 | 20 | RubyUnits::Unit.define("second") do |unit| 21 | unit.scalar = 1 22 | unit.numerator = %w[] 23 | unit.aliases = %w[s sec second seconds] 24 | unit.kind = :time 25 | end 26 | 27 | RubyUnits::Unit.define("mole") do |unit| 28 | unit.scalar = 1 29 | unit.numerator = %w[] 30 | unit.aliases = %w[mol mole] 31 | unit.kind = :substance 32 | end 33 | 34 | RubyUnits::Unit.define("ampere") do |unit| 35 | unit.scalar = 1 36 | unit.numerator = %w[] 37 | unit.aliases = %w[A ampere amperes amp amps] 38 | unit.kind = :current 39 | end 40 | 41 | RubyUnits::Unit.define("radian") do |unit| 42 | unit.scalar = 1 43 | unit.numerator = %w[] 44 | unit.aliases = %w[rad radian radians] 45 | unit.kind = :angle 46 | end 47 | 48 | RubyUnits::Unit.define("kelvin") do |unit| 49 | unit.scalar = 1 50 | unit.numerator = %w[] 51 | unit.aliases = %w[degK kelvin] 52 | unit.kind = :temperature 53 | end 54 | 55 | RubyUnits::Unit.define("tempK") do |unit| 56 | unit.scalar = 1 57 | unit.numerator = %w[] 58 | unit.aliases = %w[tempK] 59 | unit.kind = :temperature 60 | end 61 | 62 | RubyUnits::Unit.define("byte") do |unit| 63 | unit.scalar = 1 64 | unit.numerator = %w[] 65 | unit.aliases = %w[B byte bytes] 66 | unit.kind = :information 67 | end 68 | 69 | RubyUnits::Unit.define("dollar") do |unit| 70 | unit.scalar = 1 71 | unit.numerator = %w[] 72 | unit.aliases = %w[USD dollar] 73 | unit.kind = :currency 74 | end 75 | 76 | RubyUnits::Unit.define("candela") do |unit| 77 | unit.scalar = 1 78 | unit.numerator = %w[] 79 | unit.aliases = %w[cd candela] 80 | unit.kind = :luminosity 81 | end 82 | 83 | RubyUnits::Unit.define("each") do |unit| 84 | unit.scalar = 1 85 | unit.numerator = %w[] 86 | unit.aliases = %w[each] 87 | unit.kind = :counting 88 | end 89 | 90 | RubyUnits::Unit.define("steradian") do |unit| 91 | unit.scalar = 1 92 | unit.numerator = %w[] 93 | unit.aliases = %w[sr steradian steradians] 94 | unit.kind = :solid_angle 95 | end 96 | 97 | RubyUnits::Unit.define("decibel") do |unit| 98 | unit.scalar = 1 99 | unit.numerator = %w[] 100 | unit.aliases = %w[dB decibel decibels] 101 | unit.kind = :logarithmic 102 | end 103 | -------------------------------------------------------------------------------- /lib/ruby_units/unit_definitions/prefix.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | { 4 | "googol" => [%w[googol], 1e100], 5 | "yobi" => [%w[Yi Yobi yobi], 2**80], 6 | "zebi" => [%w[Zi Zebi zebi], 2**70], 7 | "exbi" => [%w[Ei Exbi exbi], 2**60], 8 | "pebi" => [%w[Pi Pebi pebi], 2**50], 9 | "tebi" => [%w[Ti Tebi tebi], 2**40], 10 | "gibi" => [%w[Gi Gibi gibi], 2**30], 11 | "mebi" => [%w[Mi Mebi mebi], 2**20], 12 | "kibi" => [%w[Ki Kibi kibi], 2**10], 13 | "yotta" => [%w[Y Yotta yotta], 1e24], 14 | "zetta" => [%w[Z Zetta zetta], 1e21], 15 | "exa" => [%w[E Exa exa], 1e18], 16 | "peta" => [%w[P Peta peta], 1e15], 17 | "tera" => [%w[T Tera tera], 1e12], 18 | "giga" => [%w[G Giga giga], 1e9], 19 | "mega" => [%w[M Mega mega], 1e6], 20 | "kilo" => [%w[k kilo], 1e3], 21 | "hecto" => [%w[h Hecto hecto], 1e2], 22 | "deca" => [%w[da Deca deca deka], 1e1], 23 | "1" => [%w[1], 1], 24 | "deci" => [%w[d Deci deci], Rational(1, 1e1)], 25 | "centi" => [%w[c Centi centi], Rational(1, 1e2)], 26 | "milli" => [%w[m Milli milli], Rational(1, 1e3)], 27 | "micro" => [%w[u µ Micro micro mc], Rational(1, 1e6)], 28 | "nano" => [%w[n Nano nano], Rational(1, 1e9)], 29 | "pico" => [%w[p Pico pico], Rational(1, 1e12)], 30 | "femto" => [%w[f Femto femto], Rational(1, 1e15)], 31 | "atto" => [%w[a Atto atto], Rational(1, 1e18)], 32 | "zepto" => [%w[z Zepto zepto], Rational(1, 1e21)], 33 | "yocto" => [%w[y Yocto yocto], Rational(1, 1e24)] 34 | }.each do |name, definition| 35 | RubyUnits::Unit.define(name) do |unit| 36 | aliases, scalar = definition 37 | unit.aliases = aliases 38 | unit.scalar = scalar 39 | unit.kind = :prefix 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/ruby_units/unit_definitions/standard.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # length units 4 | 5 | RubyUnits::Unit.define("inch") do |inch| 6 | inch.definition = RubyUnits::Unit.new("254/10000 meter") 7 | inch.aliases = %w[in inch inches "] 8 | end 9 | 10 | RubyUnits::Unit.define("foot") do |foot| 11 | foot.definition = RubyUnits::Unit.new("12 inches") 12 | foot.aliases = %w[ft foot feet '] 13 | end 14 | 15 | RubyUnits::Unit.define("survey-foot") do |sft| 16 | sft.definition = RubyUnits::Unit.new("1200/3937 meter") 17 | sft.aliases = %w[sft sfoot sfeet] 18 | end 19 | 20 | RubyUnits::Unit.define("yard") do |yard| 21 | yard.definition = RubyUnits::Unit.new("3 ft") 22 | yard.aliases = %w[yd yard yards] 23 | end 24 | 25 | RubyUnits::Unit.define("mile") do |mile| 26 | mile.definition = RubyUnits::Unit.new("5280 ft") 27 | mile.aliases = %w[mi mile miles] 28 | end 29 | 30 | RubyUnits::Unit.define("naut-mile") do |naut| 31 | naut.definition = RubyUnits::Unit.new("1852 m") 32 | naut.aliases = %w[nmi NM] 33 | # Don't use the 'M' abbreviation here since it conflicts with 'Molar' 34 | end 35 | 36 | # on land 37 | RubyUnits::Unit.define("league") do |league| 38 | league.definition = RubyUnits::Unit.new("3 miles") 39 | league.aliases = %w[league leagues] 40 | end 41 | 42 | # at sea 43 | RubyUnits::Unit.define("naut-league") do |naut_league| 44 | naut_league.definition = RubyUnits::Unit.new("3 nmi") 45 | naut_league.aliases = %w[nleague nleagues] 46 | end 47 | 48 | RubyUnits::Unit.define("furlong") do |furlong| 49 | furlong.definition = RubyUnits::Unit.new("1/8 mile") 50 | furlong.aliases = %w[fur furlong furlongs] 51 | end 52 | 53 | RubyUnits::Unit.define("rod") do |rod| 54 | rod.definition = RubyUnits::Unit.new("33/2 feet") 55 | rod.aliases = %w[rd rod rods] 56 | end 57 | 58 | RubyUnits::Unit.define("fathom") do |fathom| 59 | fathom.definition = RubyUnits::Unit.new("6 ft") 60 | fathom.aliases = %w[fathom fathoms] 61 | end 62 | 63 | RubyUnits::Unit.define("mil") do |mil| 64 | mil.definition = RubyUnits::Unit.new("1/1000 inch") 65 | mil.aliases = %w[mil mils] 66 | end 67 | 68 | RubyUnits::Unit.define("angstrom") do |ang| 69 | ang.definition = RubyUnits::Unit.new("1/10 nm") 70 | ang.aliases = %w[ang angstrom angstroms] 71 | end 72 | 73 | # typesetting 74 | 75 | RubyUnits::Unit.define("pica") do |pica| 76 | pica.definition = RubyUnits::Unit.new("1/72 ft") 77 | pica.aliases = %w[pica picas] 78 | # Don't use 'P' as an abbreviation since it conflicts with 'Poise' 79 | # Don't use 'pc' as an abbreviation since it conflicts with 'parsec' 80 | end 81 | 82 | RubyUnits::Unit.define("point") do |point| 83 | point.definition = RubyUnits::Unit.new("1/12 pica") 84 | point.aliases = %w[point points] 85 | end 86 | 87 | RubyUnits::Unit.define("dot") do |dot| 88 | dot.definition = RubyUnits::Unit.new("1 each") 89 | dot.aliases = %w[dot dots] 90 | dot.kind = :counting 91 | end 92 | 93 | RubyUnits::Unit.define("pixel") do |pixel| 94 | pixel.definition = RubyUnits::Unit.new("1 each") 95 | pixel.aliases = %w[px pixel pixels] 96 | pixel.kind = :counting 97 | end 98 | 99 | RubyUnits::Unit.define("ppi") do |ppi| 100 | ppi.definition = RubyUnits::Unit.new("1 pixel/inch") 101 | end 102 | 103 | RubyUnits::Unit.define("dpi") do |dpi| 104 | dpi.definition = RubyUnits::Unit.new("1 dot/inch") 105 | end 106 | 107 | # Mass 108 | 109 | avagadro_constant = RubyUnits::Unit.new("6.02214129e23 1/mol") 110 | 111 | RubyUnits::Unit.define("AMU") do |amu| 112 | amu.definition = RubyUnits::Unit.new("0.012 kg/mol") / (12 * avagadro_constant) 113 | amu.aliases = %w[u AMU amu] 114 | end 115 | 116 | RubyUnits::Unit.define("dalton") do |dalton| 117 | dalton.definition = RubyUnits::Unit.new("1 amu") 118 | dalton.aliases = %w[Da dalton daltons] 119 | end 120 | 121 | RubyUnits::Unit.define("metric-ton") do |mton| 122 | mton.definition = RubyUnits::Unit.new("1000 kg") 123 | mton.aliases = %w[tonne] 124 | end 125 | 126 | # defined as a rational number to preserve accuracy and minimize round-off errors during 127 | # calculations 128 | RubyUnits::Unit.define("pound") do |pound| 129 | pound.definition = RubyUnits::Unit.new(Rational(45_359_237, 1e8), "kg") 130 | pound.aliases = %w[lbs lb lbm pound-mass pound pounds #] 131 | end 132 | 133 | RubyUnits::Unit.define("ounce") do |ounce| 134 | ounce.definition = RubyUnits::Unit.new("1/16 lbs") 135 | ounce.aliases = %w[oz ounce ounces] 136 | end 137 | 138 | RubyUnits::Unit.define("gram") do |gram| 139 | gram.definition = RubyUnits::Unit.new("1/1000 kg") 140 | gram.aliases = %w[g gram grams gramme grammes] 141 | end 142 | 143 | RubyUnits::Unit.define("short-ton") do |ton| 144 | ton.definition = RubyUnits::Unit.new("2000 lbs") 145 | ton.aliases = %w[tn ton tons short-tons] 146 | end 147 | 148 | RubyUnits::Unit.define("carat") do |carat| 149 | carat.definition = RubyUnits::Unit.new("1/5000 kg") 150 | carat.aliases = %w[ct carat carats] 151 | end 152 | 153 | RubyUnits::Unit.define("stone") do |stone| 154 | stone.definition = RubyUnits::Unit.new("14 lbs") 155 | stone.aliases = %w[st stone] 156 | end 157 | 158 | # time 159 | 160 | RubyUnits::Unit.define("minute") do |min| 161 | min.definition = RubyUnits::Unit.new("60 seconds") 162 | min.aliases = %w[min minute minutes] 163 | end 164 | 165 | RubyUnits::Unit.define("hour") do |hour| 166 | hour.definition = RubyUnits::Unit.new("60 minutes") 167 | hour.aliases = %w[h hr hrs hour hours] 168 | end 169 | 170 | RubyUnits::Unit.define("day") do |day| 171 | day.definition = RubyUnits::Unit.new("24 hours") 172 | day.aliases = %w[d day days] 173 | end 174 | 175 | RubyUnits::Unit.define("week") do |week| 176 | week.definition = RubyUnits::Unit.new("7 days") 177 | week.aliases = %w[wk week weeks] 178 | end 179 | 180 | RubyUnits::Unit.define("fortnight") do |fortnight| 181 | fortnight.definition = RubyUnits::Unit.new("2 weeks") 182 | fortnight.aliases = %w[fortnight fortnights] 183 | end 184 | 185 | RubyUnits::Unit.define("year") do |year| 186 | year.definition = RubyUnits::Unit.new("31556926 seconds") # works out to 365.24219907407405 days 187 | year.aliases = %w[y yr year years annum] 188 | end 189 | 190 | RubyUnits::Unit.define("decade") do |decade| 191 | decade.definition = RubyUnits::Unit.new("10 years") 192 | decade.aliases = %w[decade decades] 193 | end 194 | 195 | RubyUnits::Unit.define("century") do |century| 196 | century.definition = RubyUnits::Unit.new("100 years") 197 | century.aliases = %w[century centuries] 198 | end 199 | 200 | # area 201 | 202 | RubyUnits::Unit.define("hectare") do |hectare| 203 | hectare.definition = RubyUnits::Unit.new("10000 m^2") 204 | end 205 | 206 | RubyUnits::Unit.define("acre") do |acre| 207 | acre.definition = (RubyUnits::Unit.new("1 mi")**2) / 640 208 | acre.aliases = %w[acre acres] 209 | end 210 | 211 | RubyUnits::Unit.define("sqft") do |sqft| 212 | sqft.definition = RubyUnits::Unit.new("1 ft^2") 213 | end 214 | 215 | RubyUnits::Unit.define("sqin") do |sqin| 216 | sqin.definition = RubyUnits::Unit.new("1 in^2") 217 | end 218 | 219 | # volume 220 | 221 | RubyUnits::Unit.define("liter") do |liter| 222 | liter.definition = RubyUnits::Unit.new("1/1000 m^3") 223 | liter.aliases = %w[l L liter liters litre litres] 224 | end 225 | 226 | RubyUnits::Unit.define("gallon") do |gallon| 227 | gallon.definition = RubyUnits::Unit.new("231 in^3") 228 | gallon.aliases = %w[gal gallon gallons] 229 | end 230 | 231 | RubyUnits::Unit.define("quart") do |quart| 232 | quart.definition = RubyUnits::Unit.new("1/4 gal") 233 | quart.aliases = %w[qt quart quarts] 234 | end 235 | 236 | RubyUnits::Unit.define("pint") do |pint| 237 | pint.definition = RubyUnits::Unit.new("1/8 gal") 238 | pint.aliases = %w[pt pint pints] 239 | end 240 | 241 | RubyUnits::Unit.define("cup") do |cup| 242 | cup.definition = RubyUnits::Unit.new("1/16 gal") 243 | cup.aliases = %w[cu cup cups] 244 | end 245 | 246 | RubyUnits::Unit.define("fluid-ounce") do |floz| 247 | floz.definition = RubyUnits::Unit.new("1/128 gal") 248 | floz.aliases = %w[floz fluid-ounce fluid-ounces] 249 | end 250 | 251 | RubyUnits::Unit.define("tablespoon") do |tbsp| 252 | tbsp.definition = RubyUnits::Unit.new("1/2 floz") 253 | tbsp.aliases = %w[tbs tbsp tablespoon tablespoons] 254 | end 255 | 256 | RubyUnits::Unit.define("teaspoon") do |tsp| 257 | tsp.definition = RubyUnits::Unit.new("1/3 tablespoon") 258 | tsp.aliases = %w[tsp teaspoon teaspoons] 259 | end 260 | 261 | ## 262 | # The board-foot is a specialized unit of measure for the volume of lumber in 263 | # the United States and Canada. It is the volume of a one-foot length of a board 264 | # one foot wide and one inch thick. 265 | # http://en.wikipedia.org/wiki/Board_foot 266 | RubyUnits::Unit.define("bdft") do |bdft| 267 | bdft.definition = RubyUnits::Unit.new("1/12 ft^3") 268 | bdft.aliases = %w[fbm boardfoot boardfeet bf] 269 | end 270 | 271 | # volumetric flow 272 | 273 | RubyUnits::Unit.define("cfm") do |cfm| 274 | cfm.definition = RubyUnits::Unit.new("1 ft^3/minute") 275 | cfm.aliases = %w[cfm CFM CFPM] 276 | end 277 | 278 | # speed 279 | 280 | RubyUnits::Unit.define("kph") do |kph| 281 | kph.definition = RubyUnits::Unit.new("1 kilometer/hour") 282 | end 283 | 284 | RubyUnits::Unit.define("mph") do |mph| 285 | mph.definition = RubyUnits::Unit.new("1 mile/hour") 286 | end 287 | 288 | RubyUnits::Unit.define("fps") do |fps| 289 | fps.definition = RubyUnits::Unit.new("1 foot/second") 290 | end 291 | 292 | RubyUnits::Unit.define("knot") do |knot| 293 | knot.definition = RubyUnits::Unit.new("1 nmi/hour") 294 | knot.aliases = %w[kt kn kts knot knots] 295 | end 296 | 297 | RubyUnits::Unit.define("gee") do |gee| 298 | # approximated as a rational number to minimize round-off errors 299 | gee.definition = RubyUnits::Unit.new(Rational(196_133, 20_000), "m/s^2") # equivalent to 9.80665 m/s^2 300 | gee.aliases = %w[gee standard-gravitation] 301 | end 302 | 303 | # temperature differences 304 | 305 | RubyUnits::Unit.define("newton") do |newton| 306 | newton.definition = RubyUnits::Unit.new("1 kg*m/s^2") 307 | newton.aliases = %w[N newton newtons] 308 | end 309 | 310 | RubyUnits::Unit.define("dyne") do |dyne| 311 | dyne.definition = RubyUnits::Unit.new("1/100000 N") 312 | dyne.aliases = %w[dyn dyne] 313 | end 314 | 315 | RubyUnits::Unit.define("pound-force") do |lbf| 316 | lbf.definition = RubyUnits::Unit.new("1 lb") * RubyUnits::Unit.new("1 gee") 317 | lbf.aliases = %w[lbf pound-force] 318 | end 319 | 320 | RubyUnits::Unit.define("poundal") do |poundal| 321 | poundal.definition = RubyUnits::Unit.new("1 lb") * RubyUnits::Unit.new("1 ft/s^2") 322 | poundal.aliases = %w[pdl poundal poundals] 323 | end 324 | 325 | temp_convert_factor = Rational(2_501_999_792_983_609, 4_503_599_627_370_496) # approximates 1/1.8 326 | 327 | RubyUnits::Unit.define("celsius") do |celsius| 328 | celsius.definition = RubyUnits::Unit.new("1 degK") 329 | celsius.aliases = %w[degC celsius centigrade] 330 | end 331 | 332 | RubyUnits::Unit.define("fahrenheit") do |fahrenheit| 333 | fahrenheit.definition = RubyUnits::Unit.new(temp_convert_factor, "degK") 334 | fahrenheit.aliases = %w[degF fahrenheit] 335 | end 336 | 337 | RubyUnits::Unit.define("rankine") do |rankine| 338 | rankine.definition = RubyUnits::Unit.new("1 degF") 339 | rankine.aliases = %w[degR rankine] 340 | end 341 | 342 | RubyUnits::Unit.define("tempC") do |temp_c| 343 | temp_c.definition = RubyUnits::Unit.new("1 tempK") 344 | end 345 | 346 | RubyUnits::Unit.define("tempF") do |temp_f| 347 | temp_f.definition = RubyUnits::Unit.new(temp_convert_factor, "tempK") 348 | end 349 | 350 | RubyUnits::Unit.define("tempR") do |temp_r| 351 | temp_r.definition = RubyUnits::Unit.new("1 tempF") 352 | end 353 | 354 | # astronomy 355 | 356 | speed_of_light = RubyUnits::Unit.new("299792458 m/s") 357 | 358 | RubyUnits::Unit.define("light-second") do |ls| 359 | ls.definition = RubyUnits::Unit.new("1 s") * speed_of_light 360 | ls.aliases = %w[ls lsec light-second] 361 | end 362 | 363 | RubyUnits::Unit.define("light-minute") do |lmin| 364 | lmin.definition = RubyUnits::Unit.new("1 min") * speed_of_light 365 | lmin.aliases = %w[lmin light-minute] 366 | end 367 | 368 | RubyUnits::Unit.define("light-year") do |ly| 369 | ly.definition = RubyUnits::Unit.new("1 y") * speed_of_light 370 | ly.aliases = %w[ly light-year] 371 | end 372 | 373 | RubyUnits::Unit.define("parsec") do |parsec| 374 | parsec.definition = RubyUnits::Unit.new("3.26163626 ly") 375 | parsec.aliases = %w[pc parsec parsecs] 376 | end 377 | 378 | # once was '149597900000 m' but there appears to be a more accurate estimate according to wikipedia 379 | # see http://en.wikipedia.org/wiki/Astronomical_unit 380 | RubyUnits::Unit.define("AU") do |au| 381 | au.definition = RubyUnits::Unit.new("149597870700 m") 382 | au.aliases = %w[AU astronomical-unit] 383 | end 384 | 385 | RubyUnits::Unit.define("redshift") do |red| 386 | red.definition = RubyUnits::Unit.new("1.302773e26 m") 387 | red.aliases = %w[z red-shift] 388 | end 389 | 390 | # mass 391 | 392 | RubyUnits::Unit.define("slug") do |slug| 393 | slug.definition = RubyUnits::Unit.new("1 lbf*s^2/ft") 394 | slug.aliases = %w[slug slugs] 395 | end 396 | 397 | # pressure 398 | 399 | RubyUnits::Unit.define("pascal") do |pascal| 400 | pascal.definition = RubyUnits::Unit.new("1 kg/m*s^2") 401 | pascal.aliases = %w[Pa pascal pascals] 402 | end 403 | 404 | RubyUnits::Unit.define("bar") do |bar| 405 | bar.definition = RubyUnits::Unit.new("100 kPa") 406 | bar.aliases = %w[bar bars] 407 | end 408 | 409 | RubyUnits::Unit.define("atm") do |atm| 410 | atm.definition = RubyUnits::Unit.new("101325 Pa") 411 | atm.aliases = %w[atm ATM atmosphere atmospheres] 412 | end 413 | 414 | RubyUnits::Unit.define("mmHg") do |mmhg| 415 | density_of_mercury = RubyUnits::Unit.new("7653360911758079/562949953421312 g/cm^3") # 13.5951 g/cm^3 at 0 tempC 416 | mmhg.definition = RubyUnits::Unit.new("1 mm") * RubyUnits::Unit.new("1 gee") * density_of_mercury 417 | end 418 | 419 | RubyUnits::Unit.define("inHg") do |inhg| 420 | density_of_mercury = RubyUnits::Unit.new("7653360911758079/562949953421312 g/cm^3") # 13.5951 g/cm^3 at 0 tempC 421 | inhg.definition = RubyUnits::Unit.new("1 in") * RubyUnits::Unit.new("1 gee") * density_of_mercury 422 | end 423 | 424 | RubyUnits::Unit.define("torr") do |torr| 425 | torr.definition = RubyUnits::Unit.new("1/760 atm") 426 | torr.aliases = %w[Torr torr] 427 | end 428 | 429 | RubyUnits::Unit.define("psi") do |psi| 430 | psi.definition = RubyUnits::Unit.new("1 lbf/in^2") 431 | end 432 | 433 | RubyUnits::Unit.define("cmh2o") do |cmh2o| 434 | density_of_water = RubyUnits::Unit.new("1 g/cm^3") # at 4 tempC 435 | cmh2o.definition = RubyUnits::Unit.new("1 cm") * RubyUnits::Unit.new("1 gee") * density_of_water 436 | cmh2o.aliases = %w[cmH2O cmh2o cmAq] 437 | end 438 | 439 | RubyUnits::Unit.define("inh2o") do |inh2o| 440 | density_of_water = RubyUnits::Unit.new("1 g/cm^3") # at 4 tempC 441 | inh2o.definition = RubyUnits::Unit.new("1 in") * RubyUnits::Unit.new("1 gee") * density_of_water 442 | inh2o.aliases = %w[inH2O inh2o inAq] 443 | end 444 | 445 | # viscosity 446 | 447 | RubyUnits::Unit.define("poise") do |poise| 448 | poise.definition = RubyUnits::Unit.new("dPa*s") 449 | poise.aliases = %w[P poise] 450 | end 451 | 452 | RubyUnits::Unit.define("stokes") do |stokes| 453 | stokes.definition = RubyUnits::Unit.new("1 cm^2/s") 454 | stokes.aliases = %w[St stokes] 455 | end 456 | 457 | # #energy 458 | 459 | RubyUnits::Unit.define("joule") do |joule| 460 | joule.definition = RubyUnits::Unit.new("1 N*m") 461 | joule.aliases = %w[J joule joules] 462 | end 463 | 464 | RubyUnits::Unit.define("erg") do |erg| 465 | erg.definition = RubyUnits::Unit.new("1 g*cm^2/s^2") 466 | erg.aliases = %w[erg ergs] 467 | end 468 | 469 | # power 470 | 471 | RubyUnits::Unit.define("watt") do |watt| 472 | watt.definition = RubyUnits::Unit.new("1 N*m/s") 473 | watt.aliases = %w[W Watt watt watts] 474 | end 475 | 476 | RubyUnits::Unit.define("horsepower") do |hp| 477 | hp.definition = RubyUnits::Unit.new("33000 ft*lbf/min") 478 | hp.aliases = %w[hp horsepower] 479 | end 480 | 481 | # energy 482 | RubyUnits::Unit.define("btu") do |btu| 483 | btu.definition = RubyUnits::Unit.new("2320092679909671/2199023255552 J") # 1055.056 J --- ISO standard 484 | btu.aliases = %w[Btu btu Btus btus] 485 | end 486 | 487 | RubyUnits::Unit.define("therm") do |therm| 488 | therm.definition = RubyUnits::Unit.new("100 kBtu") 489 | therm.aliases = %w[thm therm therms Therm] 490 | end 491 | 492 | # "small" calorie 493 | RubyUnits::Unit.define("calorie") do |calorie| 494 | calorie.definition = RubyUnits::Unit.new("4.184 J") 495 | calorie.aliases = %w[cal calorie calories] 496 | end 497 | 498 | # "big" calorie 499 | RubyUnits::Unit.define("Calorie") do |calorie| 500 | calorie.definition = RubyUnits::Unit.new("1 kcal") 501 | calorie.aliases = %w[Cal Calorie Calories] 502 | end 503 | 504 | RubyUnits::Unit.define("molar") do |molar| 505 | molar.definition = RubyUnits::Unit.new("1 mole/l") 506 | molar.aliases = %w[M molar] 507 | end 508 | 509 | # potential 510 | RubyUnits::Unit.define("volt") do |volt| 511 | volt.definition = RubyUnits::Unit.new("1 W/A") 512 | volt.aliases = %w[V volt volts] 513 | end 514 | 515 | # capacitance 516 | RubyUnits::Unit.define("farad") do |farad| 517 | farad.definition = RubyUnits::Unit.new("1 A*s/V") 518 | farad.aliases = %w[F farad farads] 519 | end 520 | 521 | # charge 522 | RubyUnits::Unit.define("coulomb") do |coulomb| 523 | coulomb.definition = RubyUnits::Unit.new("1 A*s") 524 | coulomb.aliases = %w[C coulomb coulombs] 525 | end 526 | 527 | # conductance 528 | RubyUnits::Unit.define("siemens") do |siemens| 529 | siemens.definition = RubyUnits::Unit.new("1 A/V") 530 | siemens.aliases = %w[S siemens] 531 | end 532 | 533 | # inductance 534 | RubyUnits::Unit.define("henry") do |henry| 535 | henry.definition = RubyUnits::Unit.new("1 J/A^2") 536 | henry.aliases = %w[H henry henries] 537 | end 538 | 539 | # resistance 540 | RubyUnits::Unit.define("ohm") do |ohm| 541 | ohm.definition = RubyUnits::Unit.new("1 V/A") 542 | ohm.aliases = %w[Ohm ohm ohms] 543 | end 544 | 545 | # magnetism 546 | 547 | RubyUnits::Unit.define("weber") do |weber| 548 | weber.definition = RubyUnits::Unit.new("1 V*s") 549 | weber.aliases = %w[Wb weber webers] 550 | end 551 | 552 | RubyUnits::Unit.define("tesla") do |tesla| 553 | tesla.definition = RubyUnits::Unit.new("1 V*s/m^2") 554 | tesla.aliases = %w[T tesla teslas] 555 | end 556 | 557 | RubyUnits::Unit.define("gauss") do |gauss| 558 | gauss.definition = RubyUnits::Unit.new("100 microT") 559 | gauss.aliases = %w[G gauss] 560 | end 561 | 562 | RubyUnits::Unit.define("maxwell") do |maxwell| 563 | maxwell.definition = RubyUnits::Unit.new("1 gauss*cm^2") 564 | maxwell.aliases = %w[Mx maxwell maxwells] 565 | end 566 | 567 | RubyUnits::Unit.define("oersted") do |oersted| 568 | oersted.definition = RubyUnits::Unit.new(250.0 / Math::PI, "A/m") 569 | oersted.aliases = %w[Oe oersted oersteds] 570 | end 571 | 572 | # activity 573 | RubyUnits::Unit.define("katal") do |katal| 574 | katal.definition = RubyUnits::Unit.new("1 mole/sec") 575 | katal.aliases = %w[kat katal] 576 | end 577 | 578 | RubyUnits::Unit.define("unit") do |unit| 579 | unit.definition = RubyUnits::Unit.new("1/60 microkatal") 580 | unit.aliases = %w[U enzUnit units] 581 | end 582 | 583 | # frequency 584 | 585 | RubyUnits::Unit.define("hertz") do |hz| 586 | hz.definition = RubyUnits::Unit.new("1 1/s") 587 | hz.aliases = %w[Hz hertz] 588 | end 589 | 590 | # angle 591 | RubyUnits::Unit.define("degree") do |deg| 592 | deg.definition = RubyUnits::Unit.new(Math::PI / 180.0, "radian") 593 | deg.aliases = %w[deg degree degrees] 594 | end 595 | 596 | RubyUnits::Unit.define("gon") do |grad| 597 | grad.definition = RubyUnits::Unit.new(Math::PI / 200.0, "radian") 598 | grad.aliases = %w[gon grad gradian grads] 599 | end 600 | 601 | # rotation 602 | RubyUnits::Unit.define("rotation") do |rotation| 603 | rotation.definition = RubyUnits::Unit.new(2.0 * Math::PI, "radian") 604 | end 605 | 606 | RubyUnits::Unit.define("rpm") do |rpm| 607 | rpm.definition = RubyUnits::Unit.new("1 rotation/min") 608 | end 609 | 610 | # memory 611 | RubyUnits::Unit.define("bit") do |bit| 612 | bit.definition = RubyUnits::Unit.new("1/8 byte") 613 | bit.aliases = %w[b bit] 614 | end 615 | 616 | # currency 617 | RubyUnits::Unit.define("cents") do |cents| 618 | cents.definition = RubyUnits::Unit.new("1/100 dollar") 619 | end 620 | 621 | # luminosity 622 | RubyUnits::Unit.define("lumen") do |lumen| 623 | lumen.definition = RubyUnits::Unit.new("1 cd*steradian") 624 | lumen.aliases = %w[lm lumen] 625 | end 626 | 627 | RubyUnits::Unit.define("lux") do |lux| 628 | lux.definition = RubyUnits::Unit.new("1 lumen/m^2") 629 | end 630 | 631 | # radiation 632 | RubyUnits::Unit.define("gray") do |gray| 633 | gray.definition = RubyUnits::Unit.new("1 J/kg") 634 | gray.aliases = %w[Gy gray grays] 635 | end 636 | 637 | RubyUnits::Unit.define("roentgen") do |roentgen| 638 | roentgen.definition = RubyUnits::Unit.new("2.58e-4 C/kg") 639 | roentgen.aliases = %w[R roentgen] 640 | end 641 | 642 | RubyUnits::Unit.define("sievert") do |sievert| 643 | sievert.definition = RubyUnits::Unit.new("1 J/kg") 644 | sievert.aliases = %w[Sv sievert sieverts] 645 | end 646 | 647 | RubyUnits::Unit.define("becquerel") do |becquerel| 648 | becquerel.definition = RubyUnits::Unit.new("1 1/s") 649 | becquerel.aliases = %w[Bq becquerel becquerels] 650 | end 651 | 652 | RubyUnits::Unit.define("curie") do |curie| 653 | curie.definition = RubyUnits::Unit.new("37 GBq") 654 | curie.aliases = %w[Ci curie curies] 655 | end 656 | 657 | RubyUnits::Unit.define("count") do |count| 658 | count.definition = RubyUnits::Unit.new("1 each") 659 | count.kind = :counting 660 | end 661 | 662 | # rate 663 | RubyUnits::Unit.define("cpm") do |cpm| 664 | cpm.definition = RubyUnits::Unit.new("1 count/min") 665 | end 666 | 667 | RubyUnits::Unit.define("dpm") do |dpm| 668 | dpm.definition = RubyUnits::Unit.new("1 count/min") 669 | end 670 | 671 | RubyUnits::Unit.define("bpm") do |bpm| 672 | bpm.definition = RubyUnits::Unit.new("1 count/min") 673 | end 674 | 675 | # misc 676 | RubyUnits::Unit.define("dozen") do |dozen| 677 | dozen.definition = RubyUnits::Unit.new("12 each") 678 | dozen.aliases = %w[doz dz dozen] 679 | dozen.kind = :counting 680 | end 681 | 682 | RubyUnits::Unit.define("gross") do |gross| 683 | gross.definition = RubyUnits::Unit.new("12 dozen") 684 | gross.aliases = %w[gr gross] 685 | gross.kind = :counting 686 | end 687 | 688 | RubyUnits::Unit.define("cell") do |cell| 689 | cell.definition = RubyUnits::Unit.new("1 each") 690 | cell.aliases = %w[cells cell] 691 | cell.kind = :counting 692 | end 693 | 694 | RubyUnits::Unit.define("base-pair") do |bp| 695 | bp.definition = RubyUnits::Unit.new("1 each") 696 | bp.aliases = %w[bp base-pair] 697 | bp.kind = :counting 698 | end 699 | 700 | RubyUnits::Unit.define("nucleotide") do |nt| 701 | nt.definition = RubyUnits::Unit.new("1 each") 702 | nt.aliases = %w[nt] 703 | nt.kind = :counting 704 | end 705 | 706 | RubyUnits::Unit.define("molecule") do |molecule| 707 | molecule.definition = RubyUnits::Unit.new("1 each") 708 | molecule.aliases = %w[molecule molecules] 709 | molecule.kind = :counting 710 | end 711 | 712 | RubyUnits::Unit.define("percent") do |percent| 713 | percent.definition = RubyUnits::Unit.new("1/100") 714 | percent.aliases = %w[% percent] 715 | end 716 | 717 | RubyUnits::Unit.define("ppm") do |ppm| 718 | ppm.definition = RubyUnits::Unit.new(1) / 1_000_000 719 | end 720 | 721 | RubyUnits::Unit.define("ppb") do |ppb| 722 | ppb.definition = RubyUnits::Unit.new(1) / 1_000_000_000 723 | end 724 | -------------------------------------------------------------------------------- /lib/ruby_units/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module RubyUnits 4 | class Unit < Numeric 5 | VERSION = "4.1.0" 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /ruby-units.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path("lib", __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require "ruby_units/version" 6 | 7 | Gem::Specification.new do |spec| 8 | spec.name = "ruby-units" 9 | spec.version = RubyUnits::Unit::VERSION 10 | spec.authors = ["Kevin Olbrich"] 11 | spec.email = ["kevin.olbrich@gmail.com"] 12 | 13 | spec.required_rubygems_version = ">= 2.0" 14 | spec.required_ruby_version = ">= 2.7" 15 | spec.summary = "Provides classes and methods to perform unit math and conversions" 16 | spec.description = "Provides classes and methods to perform unit math and conversions" 17 | spec.homepage = "https://github.com/olbrich/ruby-units" 18 | spec.license = "MIT" 19 | 20 | spec.metadata["homepage_uri"] = spec.homepage 21 | spec.metadata["source_code_uri"] = "https://github.com/olbrich/ruby-units" 22 | spec.metadata["changelog_uri"] = "https://github.com/olbrich/ruby-units/releases" 23 | 24 | # Specify which files should be added to the gem when it is released. 25 | # The `git ls-files -z` loads the files in the RubyGem that have been added into git. 26 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 27 | `git ls-files -z`.split("\x0").reject { _1.match(%r{^(test|spec|features)/}) } 28 | end 29 | spec.require_paths = ["lib"] 30 | spec.metadata["rubygems_mfa_required"] = "true" 31 | end 32 | -------------------------------------------------------------------------------- /spec/benchmarks/bigdecimal.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../spec_helper" 4 | require "bigdecimal" 5 | require "bigdecimal/util" 6 | require "benchmark" 7 | require "ruby-prof" 8 | a = [ 9 | [2.025, "gal"], 10 | [5.575, "gal"], 11 | [8.975, "gal"], 12 | [1.5, "gal"], 13 | [9, "gal"], 14 | [1.85, "gal"], 15 | [2.25, "gal"], 16 | [1.05, "gal"], 17 | [4.725, "gal"], 18 | [3.55, "gal"], 19 | [4.725, "gal"], 20 | [3.75, "gal"], 21 | [6.275, "gal"], 22 | [0.525, "gal"], 23 | [3.475, "gal"], 24 | [0.85, "gal"] 25 | ] 26 | 27 | b = a.map { |ns, nu| Unit.new(ns.to_d, nu) } 28 | 29 | result = RubyProf.profile(merge_fibers: true) do 30 | puts b.reduce(:+) 31 | end 32 | 33 | # print a graph profile to text 34 | printer = RubyProf::GraphPrinter.new(result) 35 | printer.print(STDOUT, {}) 36 | 37 | result = RubyProf.profile(merge_fibers: true) do 38 | puts b.reduce(:-) 39 | end 40 | 41 | # print a graph profile to text 42 | printer = RubyProf::GraphPrinter.new(result) 43 | printer.print(STDOUT, {}) 44 | -------------------------------------------------------------------------------- /spec/ruby_units/array_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RubyUnits::Array do 4 | subject(:array_unit) { array.to_unit } 5 | 6 | let(:array) { [1, "cm"] } 7 | 8 | it { is_expected.to be_a RubyUnits::Unit } 9 | it { expect(array).to respond_to :to_unit } 10 | 11 | it { 12 | expect(array_unit).to have_attributes( 13 | kind: :length, 14 | scalar: 1, 15 | units: "cm" 16 | ) 17 | } 18 | end 19 | -------------------------------------------------------------------------------- /spec/ruby_units/bugs_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.dirname(__FILE__) + "/../spec_helper" 4 | 5 | describe "Github issue #49" do 6 | let(:a) { RubyUnits::Unit.new("3 cm^3") } 7 | let(:b) { RubyUnits::Unit.new(a) } 8 | 9 | it "subtracts a unit properly from one initialized with a unit" do 10 | expect(b - RubyUnits::Unit.new("1.5 cm^3")).to eq(RubyUnits::Unit.new("1.5 cm^3")) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/ruby_units/cache_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RubyUnits::Cache do 4 | subject(:cache) { described_class.new } 5 | 6 | let(:unit) { RubyUnits::Unit.new("1 m") } 7 | 8 | before do 9 | cache.clear 10 | cache.set("m", unit) 11 | end 12 | 13 | describe ".clear" do 14 | it "clears the cache" do 15 | cache.clear 16 | expect(cache.get("m")).to be_nil 17 | end 18 | end 19 | 20 | describe ".get" do 21 | it "retrieves values already in the cache" do 22 | expect(cache.get("m")).to eq(unit) 23 | end 24 | end 25 | 26 | describe ".set" do 27 | it "puts a unit into the cache" do 28 | cache.set("kg", RubyUnits::Unit.new("1 kg")) 29 | expect(cache.get("kg")).to eq(RubyUnits::Unit.new("1 kg")) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/ruby_units/complex_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Complex do 4 | subject { Complex(1.0, -1.0).to_unit } 5 | 6 | it { expect(Complex(1, 1)).to respond_to :to_unit } 7 | it { is_expected.to be_a Unit } 8 | it { expect(subject.scalar).to be_a Complex } 9 | 10 | it { is_expected.to eq("1-1i".to_unit) } 11 | 12 | # Complex numbers are a bit strange. Technically you can't really compare them 13 | # using :<=>, and Ruby < 2.7 does not implement this method for them so it 14 | # stands to reason that complex units should also not support :> or :<. 15 | # 16 | # This inconsistency was corrected in Ruby 2.7. 17 | # @see https://rubyreferences.github.io/rubychanges/2.7.html#complex 18 | # @see https://bugs.ruby-lang.org/issues/15857 19 | it "is not comparable" do 20 | if subject.scalar.respond_to?(:<=>) # this is true for Ruby >= 2.7 21 | expect { subject > "1+1i".to_unit }.to raise_error(ArgumentError) 22 | expect { subject < "1+1i".to_unit }.to raise_error(ArgumentError) 23 | else 24 | expect { subject > "1+1i".to_unit }.to raise_error(NoMethodError) 25 | expect { subject < "1+1i".to_unit }.to raise_error(NoMethodError) 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /spec/ruby_units/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | describe RubyUnits::Configuration do 6 | describe ".separator" do 7 | context "when set to true" do 8 | it "has a space between the scalar and the unit" do 9 | expect(RubyUnits::Unit.new("1 m").to_s).to eq "1 m" 10 | end 11 | end 12 | 13 | context "when set to false" do 14 | around do |example| 15 | RubyUnits.configure do |config| 16 | config.separator = false 17 | end 18 | example.run 19 | RubyUnits.reset 20 | end 21 | 22 | it "does not have a space between the scalar and the unit" do 23 | expect(RubyUnits::Unit.new("1 m").to_s).to eq "1m" 24 | expect(RubyUnits::Unit.new("14.5 lbs").to_s(:lbs)).to eq "14lbs 8oz" 25 | expect(RubyUnits::Unit.new("220 lbs").to_s(:stone)).to eq "15stone 10lbs" 26 | expect(RubyUnits::Unit.new("14.2 ft").to_s(:ft)).to eq %(14'2-2/5") 27 | expect(RubyUnits::Unit.new("1/2 cup").to_s).to eq "1/2cu" 28 | expect(RubyUnits::Unit.new("123.55 lbs").to_s("%0.2f")).to eq "123.55lbs" 29 | end 30 | end 31 | end 32 | 33 | describe ".format" do 34 | context "when set to :rational" do 35 | it "uses rational notation" do 36 | expect(RubyUnits::Unit.new("1 m/s^2").to_s).to eq "1 m/s^2" 37 | end 38 | end 39 | 40 | context "when set to :exponential" do 41 | around do |example| 42 | RubyUnits.configure do |config| 43 | config.format = :exponential 44 | end 45 | example.run 46 | RubyUnits.reset 47 | end 48 | 49 | it "uses exponential notation" do 50 | expect(RubyUnits::Unit.new("1 m/s^2").to_s).to eq "1 m*s^-2" 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /spec/ruby_units/date_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RubyUnits::Date do 4 | subject(:date_unit) { date.to_unit } 5 | 6 | let(:date) { Date.new(2011, 4, 1) } 7 | 8 | it { is_expected.to be_a Unit } 9 | it { is_expected.to respond_to :to_unit } 10 | it { is_expected.to respond_to :to_time } 11 | it { is_expected.to respond_to :to_date } 12 | it { is_expected.to have_attributes(scalar: date.ajd, units: "d", kind: :time) } 13 | 14 | describe "offsets" do 15 | specify { expect(date + "5 days".to_unit).to eq(Date.new(2011, 4, 6)) } 16 | specify { expect(date - "5 days".to_unit).to eq(Date.new(2011, 3, 27)) } 17 | # 2012 is a leap year... 18 | specify { expect(date + "1 year".to_unit).to eq(Date.new(2012, 3, 31)) } 19 | specify { expect(date - "1 year".to_unit).to eq(Date.new(2010, 4, 1)) } 20 | # adding Time or Date objects to a Duration don't make any sense, so raise 21 | # an error. 22 | specify { expect { date_unit + Date.new(2011, 4, 1) }.to raise_error(ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit") } 23 | specify { expect { date_unit + DateTime.new(2011, 4, 1, 12, 0, 0) }.to raise_error(ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit") } 24 | specify { expect { date_unit + Time.parse("2011-04-01 12:00:00") }.to raise_error(ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit") } 25 | 26 | specify { expect(date_unit - Date.new(2011, 4, 1)).to be_zero } 27 | specify { expect(date_unit - DateTime.new(2011, 4, 1, 0, 0, 0)).to be_zero } 28 | 29 | specify do 30 | expect { (date_unit - Time.parse("2011-04-01 00:00")) }.to raise_error(ArgumentError, 31 | "Date and Time objects represent fixed points in time and cannot be subtracted from a Unit") 32 | end 33 | 34 | specify { expect(Date.new(2011, 4, 1) + 1).to eq(Date.new(2011, 4, 2)) } 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /spec/ruby_units/definition_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative "../spec_helper" 4 | 5 | describe "Unit::Definition('eV')" do 6 | subject do 7 | Unit::Definition.new("eV") do |ev| 8 | ev.aliases = %w[eV electron-volt electron_volt] 9 | ev.definition = RubyUnits::Unit.new("1.602E-19 joule") 10 | ev.display_name = "electron-volt" 11 | end 12 | end 13 | 14 | describe "#name" do 15 | subject { super().name } 16 | 17 | it { is_expected.to eq("") } 18 | end 19 | 20 | describe "#aliases" do 21 | subject { super().aliases } 22 | 23 | it { is_expected.to eq(%w[eV electron-volt electron_volt]) } 24 | end 25 | 26 | describe "#scalar" do 27 | subject { super().scalar } 28 | 29 | it { is_expected.to eq(1.602E-19) } 30 | end 31 | 32 | describe "#numerator" do 33 | subject { super().numerator } 34 | 35 | it { is_expected.to include("", "", "") } 36 | end 37 | 38 | describe "#denominator" do 39 | subject { super().denominator } 40 | 41 | it { is_expected.to include("", "") } 42 | end 43 | 44 | describe "#display_name" do 45 | subject { super().display_name } 46 | 47 | it { is_expected.to eq("electron-volt") } 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/ruby_units/math_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RubyUnits::Math do 4 | describe "#sqrt" do 5 | specify { expect(Math.sqrt(RubyUnits::Unit.new("1 mm^6"))).to eq(RubyUnits::Unit.new("1 mm^3")) } 6 | specify { expect(Math.sqrt(4)).to eq(2) } 7 | specify { expect(Math.sqrt(RubyUnits::Unit.new("-9 mm^2")).scalar).to be_a(Complex) } 8 | end 9 | 10 | describe "#cbrt" do 11 | specify { expect(Math.cbrt(RubyUnits::Unit.new("1 mm^6"))).to eq(RubyUnits::Unit.new("1 mm^2")) } 12 | specify { expect(Math.cbrt(8)).to eq(2) } 13 | end 14 | 15 | describe "Trigonometry functions" do 16 | context "with '45 deg' unit" do 17 | subject(:angle) { RubyUnits::Unit.new("45 deg") } 18 | 19 | specify { expect(Math.sin(angle)).to be_within(0.01).of(0.70710678) } 20 | specify { expect(Math.cos(angle)).to be_within(0.01).of(0.70710678) } 21 | specify { expect(Math.tan(angle)).to be_within(0.01).of(1) } 22 | specify { expect(Math.sinh(angle)).to be_within(0.01).of(0.8686709614860095) } 23 | specify { expect(Math.cosh(angle)).to be_within(0.01).of(1.3246090892520057) } 24 | specify { expect(Math.tanh(angle)).to be_within(0.01).of(0.6557942026326724) } 25 | end 26 | 27 | context "with 'PI/4 radians' unit" do 28 | subject(:angle) { RubyUnits::Unit.new((Math::PI / 4), "radians") } 29 | 30 | specify { expect(Math.sin(angle)).to be_within(0.01).of(0.70710678) } 31 | specify { expect(Math.cos(angle)).to be_within(0.01).of(0.70710678) } 32 | specify { expect(Math.tan(angle)).to be_within(0.01).of(1) } 33 | specify { expect(Math.sinh(angle)).to be_within(0.01).of(0.8686709614860095) } 34 | specify { expect(Math.cosh(angle)).to be_within(0.01).of(1.3246090892520057) } 35 | specify { expect(Math.tanh(angle)).to be_within(0.01).of(0.6557942026326724) } 36 | end 37 | 38 | context "with 'PI/4' continues to work" do 39 | subject(:number) { (Math::PI / 4) } 40 | 41 | specify { expect(Math.sin(number)).to be_within(0.01).of(0.70710678) } 42 | specify { expect(Math.cos(number)).to be_within(0.01).of(0.70710678) } 43 | specify { expect(Math.tan(number)).to be_within(0.01).of(1) } 44 | specify { expect(Math.sinh(number)).to be_within(0.01).of(0.8686709614860095) } 45 | specify { expect(Math.cosh(number)).to be_within(0.01).of(1.3246090892520057) } 46 | specify { expect(Math.tanh(number)).to be_within(0.01).of(0.6557942026326724) } 47 | end 48 | 49 | specify do 50 | expect( 51 | Math.hypot( 52 | RubyUnits::Unit.new("1 m"), 53 | RubyUnits::Unit.new("2 m") 54 | ) 55 | ).to be_within(RubyUnits::Unit.new("0.01 m")).of(RubyUnits::Unit.new("2.23607 m")) 56 | end 57 | 58 | specify do 59 | expect( 60 | Math.hypot( 61 | RubyUnits::Unit.new("1 m"), 62 | RubyUnits::Unit.new("2 ft") 63 | ) 64 | ).to be_within(RubyUnits::Unit.new("0.01 m")).of(RubyUnits::Unit.new("1.17116 m")) 65 | end 66 | 67 | specify { expect(Math.hypot(3, 4)).to eq(5) } 68 | 69 | specify do 70 | expect do 71 | Math.hypot( 72 | RubyUnits::Unit.new("1 m"), 73 | RubyUnits::Unit.new("2 lbs") 74 | ) 75 | end.to raise_error(ArgumentError) 76 | end 77 | 78 | specify do 79 | expect( 80 | Math.atan2( 81 | RubyUnits::Unit.new("1 m"), 82 | RubyUnits::Unit.new("2 m") 83 | ) 84 | ).to be_within(RubyUnits::Unit.new("0.1 rad")).of("0.4636476090008061 rad".to_unit) 85 | end 86 | 87 | specify do 88 | expect( 89 | Math.atan2( 90 | RubyUnits::Unit.new("1 m"), 91 | RubyUnits::Unit.new("2 ft") 92 | ) 93 | ).to be_within(RubyUnits::Unit.new("0.1 rad")).of("1.0233478888629426 rad".to_unit) 94 | end 95 | 96 | specify { expect(Math.atan2(1, 1)).to be_within(0.01).of(0.785398163397448) } 97 | specify { expect { Math.atan2(RubyUnits::Unit.new("1 m"), RubyUnits::Unit.new("2 lbs")) }.to raise_error(ArgumentError) } 98 | end 99 | 100 | describe "Inverse trigonometry functions" do 101 | context "with unit" do 102 | subject(:unit) { RubyUnits::Unit.new("0.70710678") } 103 | 104 | it { expect(Math.asin(unit)).to be_within(RubyUnits::Unit.new(0.01, "rad")).of("0.785398 rad".to_unit) } 105 | it { expect(Math.acos(unit)).to be_within(RubyUnits::Unit.new(0.01, "rad")).of("0.785398 rad".to_unit) } 106 | it { expect(Math.atan(unit)).to be_within(RubyUnits::Unit.new(0.01, "rad")).of("0.61548 rad".to_unit) } 107 | end 108 | 109 | context "with a Numeric continues to work" do 110 | subject(:number) { 0.70710678 } 111 | 112 | it { expect(Math.asin(number)).to be_within(0.01).of(0.785398163397448) } 113 | it { expect(Math.acos(number)).to be_within(0.01).of(0.785398163397448) } 114 | it { expect(Math.atan(number)).to be_within(0.01).of(0.615479708670387) } 115 | end 116 | end 117 | 118 | describe "Exponential and logarithmic functions" do 119 | context "with a unit" do 120 | subject(:unit) { RubyUnits::Unit.new("2") } 121 | 122 | it { expect(Math.exp(unit)).to be_within(RubyUnits::Unit.new(0.01)).of("7.389056".to_unit) } 123 | it { expect(Math.log(unit)).to be_within(RubyUnits::Unit.new(0.01)).of("0.6931471805599453".to_unit) } 124 | it { expect(Math.log10(unit)).to be_within(RubyUnits::Unit.new(0.01)).of("0.3010299956639812".to_unit) } 125 | end 126 | 127 | context "with a Numeric continues to work" do 128 | subject(:number) { 2 } 129 | 130 | it { expect(Math.exp(number)).to be_within(0.01).of(7.38905609893065) } 131 | it { expect(Math.log(number)).to be_within(0.01).of(0.693147180559945) } 132 | it { expect(Math.log10(number)).to be_within(0.01).of(0.301029995663981) } 133 | end 134 | end 135 | end 136 | -------------------------------------------------------------------------------- /spec/ruby_units/numeric_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "bigdecimal" 4 | 5 | RSpec.describe RubyUnits::Numeric do 6 | specify { expect(Integer.instance_methods).to include(:to_unit) } 7 | specify { expect(Float.instance_methods).to include(:to_unit) } 8 | specify { expect(Complex.instance_methods).to include(:to_unit) } 9 | specify { expect(Rational.instance_methods).to include(:to_unit) } 10 | specify { expect(BigDecimal.instance_methods).to include(:to_unit) } 11 | 12 | describe "#to_unit" do 13 | context "when nothing is passed" do 14 | it "returns a unitless unit" do 15 | expect(1.to_unit).to eq(RubyUnits::Unit.new(1)) 16 | expect(1.0.to_unit).to eq(RubyUnits::Unit.new(1.0)) 17 | expect(Complex(1, 1).to_unit).to eq(RubyUnits::Unit.new(Complex(1, 1))) 18 | expect(Rational(1, 1).to_unit).to eq(RubyUnits::Unit.new(Rational(1, 1))) 19 | expect(BigDecimal(1).to_unit).to eq(RubyUnits::Unit.new(BigDecimal(1))) 20 | end 21 | end 22 | 23 | context "when converting to a unit" do 24 | it "returns the converted unit" do 25 | expect(0.1.to_unit("%")).to eq(RubyUnits::Unit.new(10, "%")) 26 | end 27 | 28 | it "raises an exception if the unit is not unitless" do 29 | expect { 0.1.to_unit("m") }.to raise_error(ArgumentError, "Incompatible Units ('0.1' not compatible with 'm')") 30 | expect { 0.1.to_unit(RubyUnits::Unit.new("1 m")) }.to raise_error(ArgumentError, "Incompatible Units ('0.1' not compatible with '1 m')") 31 | end 32 | end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /spec/ruby_units/parsing_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "spec_helper" 4 | 5 | RSpec.describe "Parsing" do 6 | describe "Parsing numbers" do 7 | context "with Integers" do 8 | it { expect(RubyUnits::Unit.new("1")).to have_attributes(scalar: 1) } 9 | it { expect(RubyUnits::Unit.new("-1")).to have_attributes(scalar: -1) } 10 | it { expect(RubyUnits::Unit.new("+1")).to have_attributes(scalar: 1) } 11 | it { expect(RubyUnits::Unit.new("01")).to have_attributes(scalar: 1) } 12 | it { expect(RubyUnits::Unit.new("1,000")).to have_attributes(scalar: 1000) } 13 | it { expect(RubyUnits::Unit.new("1_000")).to have_attributes(scalar: 1000) } 14 | end 15 | 16 | context "with Decimals" do 17 | # NOTE: that since this float is the same as an integer, the integer is returned 18 | it { expect(RubyUnits::Unit.new("1.0").scalar).to be(1) } 19 | it { expect(RubyUnits::Unit.new("-1.0").scalar).to be(-1) } 20 | 21 | it { expect(RubyUnits::Unit.new("1.1").scalar).to be(1.1) } 22 | it { expect(RubyUnits::Unit.new("-1.1").scalar).to be(-1.1) } 23 | it { expect(RubyUnits::Unit.new("+1.1").scalar).to be(1.1) } 24 | it { expect(RubyUnits::Unit.new("0.1").scalar).to be(0.1) } 25 | it { expect(RubyUnits::Unit.new("-0.1").scalar).to be(-0.1) } 26 | it { expect(RubyUnits::Unit.new("+0.1").scalar).to be(0.1) } 27 | it { expect(RubyUnits::Unit.new(".1").scalar).to be(0.1) } 28 | it { expect(RubyUnits::Unit.new("-.1").scalar).to be(-0.1) } 29 | it { expect(RubyUnits::Unit.new("+.1").scalar).to be(0.1) } 30 | 31 | it { expect { RubyUnits::Unit.new("0.1.") }.to raise_error(ArgumentError) } 32 | it { expect { RubyUnits::Unit.new("-0.1.") }.to raise_error(ArgumentError) } 33 | it { expect { RubyUnits::Unit.new("+0.1.") }.to raise_error(ArgumentError) } 34 | end 35 | 36 | context "with Fractions" do 37 | it { expect(RubyUnits::Unit.new("1/1").scalar).to be(1) } 38 | it { expect(RubyUnits::Unit.new("-1/1").scalar).to be(-1) } 39 | it { expect(RubyUnits::Unit.new("+1/1").scalar).to be(1) } 40 | 41 | # NOTE: eql? is used here because two equivalent Rational objects are not the same object, unlike Integers 42 | it { expect(RubyUnits::Unit.new("1/2").scalar).to eql(1/2r) } 43 | it { expect(RubyUnits::Unit.new("-1/2").scalar).to eql(-1/2r) } 44 | it { expect(RubyUnits::Unit.new("+1/2").scalar).to eql(1/2r) } 45 | it { expect(RubyUnits::Unit.new("(1/2)").scalar).to eql(1/2r) } 46 | it { expect(RubyUnits::Unit.new("(-1/2)").scalar).to eql(-1/2r) } 47 | it { expect(RubyUnits::Unit.new("(+1/2)").scalar).to eql(1/2r) } 48 | 49 | # improper fractions 50 | it { expect(RubyUnits::Unit.new("1 1/2").scalar).to eql(3/2r) } 51 | it { expect(RubyUnits::Unit.new("-1 1/2").scalar).to eql(-3/2r) } 52 | it { expect(RubyUnits::Unit.new("+1 1/2").scalar).to eql(3/2r) } 53 | it { expect(RubyUnits::Unit.new("1-1/2").scalar).to eql(3/2r) } 54 | it { expect(RubyUnits::Unit.new("-1-1/2").scalar).to eql(-3/2r) } 55 | it { expect(RubyUnits::Unit.new("+1-1/2").scalar).to eql(3/2r) } 56 | it { expect(RubyUnits::Unit.new("1 2/2").scalar).to be(2) } # weird, but not wrong 57 | it { expect(RubyUnits::Unit.new("1 3/2").scalar).to eql(5/2r) } # weird, but not wrong 58 | it { expect { RubyUnits::Unit.new("1.5 1/2") }.to raise_error(ArgumentError, "Improper fractions must have a whole number part") } 59 | it { expect { RubyUnits::Unit.new("1.5/2") }.to raise_error(ArgumentError, 'invalid value for Integer(): "1.5"') } 60 | it { expect { RubyUnits::Unit.new("1/2.5") }.to raise_error(ArgumentError, 'invalid value for Integer(): "2.5"') } 61 | end 62 | 63 | context "with Scientific Notation" do 64 | it { expect(RubyUnits::Unit.new("1e0").scalar).to be(1) } 65 | it { expect(RubyUnits::Unit.new("-1e0").scalar).to be(-1) } 66 | it { expect(RubyUnits::Unit.new("+1e0").scalar).to be(1) } 67 | it { expect(RubyUnits::Unit.new("1e1").scalar).to be(10) } 68 | it { expect(RubyUnits::Unit.new("-1e1").scalar).to be(-10) } 69 | it { expect(RubyUnits::Unit.new("+1e1").scalar).to be(10) } 70 | it { expect(RubyUnits::Unit.new("1e-1").scalar).to be(0.1) } 71 | it { expect(RubyUnits::Unit.new("-1e-1").scalar).to be(-0.1) } 72 | it { expect(RubyUnits::Unit.new("+1e-1").scalar).to be(0.1) } 73 | it { expect(RubyUnits::Unit.new("1E+1").scalar).to be(10) } 74 | it { expect(RubyUnits::Unit.new("-1E+1").scalar).to be(-10) } 75 | it { expect(RubyUnits::Unit.new("+1E+1").scalar).to be(10) } 76 | it { expect(RubyUnits::Unit.new("1E-1").scalar).to be(0.1) } 77 | it { expect(RubyUnits::Unit.new("-1E-1").scalar).to be(-0.1) } 78 | it { expect(RubyUnits::Unit.new("+1E-1").scalar).to be(0.1) } 79 | it { expect(RubyUnits::Unit.new("1.0e2").scalar).to be(100) } 80 | it { expect(RubyUnits::Unit.new(".1e2").scalar).to be(10) } 81 | it { expect(RubyUnits::Unit.new("0.1e2").scalar).to be(10) } 82 | it { expect { RubyUnits::Unit.new("0.1e2.5") }.to raise_error(ArgumentError) } 83 | end 84 | 85 | context "with Complex numbers" do 86 | it { expect(RubyUnits::Unit.new("1+1i").scalar).to eql(Complex(1, 1)) } 87 | it { expect(RubyUnits::Unit.new("1i").scalar).to eql(Complex(0, 1)) } 88 | it { expect(RubyUnits::Unit.new("-1i").scalar).to eql(Complex(0, -1)) } 89 | it { expect(RubyUnits::Unit.new("-1+1i").scalar).to eql(Complex(-1, 1)) } 90 | it { expect(RubyUnits::Unit.new("+1+1i").scalar).to eql(Complex(1, 1)) } 91 | it { expect(RubyUnits::Unit.new("1-1i").scalar).to eql(Complex(1, -1)) } 92 | it { expect(RubyUnits::Unit.new("-1.23-4.5i").scalar).to eql(Complex(-1.23, -4.5)) } 93 | it { expect(RubyUnits::Unit.new("1+0i").scalar).to be(1) } 94 | end 95 | end 96 | 97 | describe "Unit parsing" do 98 | before do 99 | RubyUnits::Unit.define("m2") do |m2| 100 | m2.definition = RubyUnits::Unit.new("meter^2") 101 | m2.aliases = %w[m2 meter2 square_meter square-meter] 102 | end 103 | end 104 | 105 | it { 106 | expect(RubyUnits::Unit.new("m2")).to have_attributes(scalar: 1, 107 | numerator: %w[], 108 | denominator: ["<1>"], 109 | kind: :area) 110 | } 111 | 112 | # make sure that underscores in the unit name are handled correctly 113 | it { expect(RubyUnits::Unit.new("1_000 square_meter")).to eq(RubyUnits::Unit.new("1000 m^2")) } 114 | end 115 | end 116 | -------------------------------------------------------------------------------- /spec/ruby_units/range_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Range do 4 | describe "of integer units" do 5 | subject { (RubyUnits::Unit.new("1 mm")..RubyUnits::Unit.new("3 mm")) } 6 | 7 | it { is_expected.to include(RubyUnits::Unit.new("2 mm")) } 8 | 9 | describe "#to_a" do 10 | subject { super().to_a } 11 | 12 | it { 13 | expect(subject).to eq([RubyUnits::Unit.new("1 mm"), 14 | RubyUnits::Unit.new("2 mm"), 15 | RubyUnits::Unit.new("3 mm")]) 16 | } 17 | end 18 | end 19 | 20 | describe "of floating point units" do 21 | subject { (RubyUnits::Unit.new("1.5 mm")..RubyUnits::Unit.new("3.5 mm")) } 22 | 23 | it { is_expected.to include(RubyUnits::Unit.new("2.0 mm")) } 24 | specify { expect { subject.to_a }.to raise_exception(ArgumentError, "Non Integer Scalar") } 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /spec/ruby_units/string_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RubyUnits::String do 4 | describe "Unit creation from strings" do 5 | specify { expect("1 mm".to_unit).to have_attributes(scalar: 1, units: "mm", kind: :length) } 6 | specify { expect("1 mm".to_unit("ft")).to have_attributes(scalar: 5/1524r, units: "ft", kind: :length) } 7 | 8 | specify { expect("1 m".convert_to("ft")).to be_within(RubyUnits::Unit.new("0.01 ft")).of RubyUnits::Unit.new("3.28084 ft") } 9 | end 10 | 11 | describe "% (format)S" do 12 | subject(:unit) { RubyUnits::Unit.new("1.23456 m/s^2") } 13 | 14 | specify { expect("%0.2f" % 1.23).to eq("1.23") } 15 | specify { expect("" % unit).to eq("") } 16 | specify { expect("%0.2f" % unit).to eq("1.23 m/s^2") } 17 | specify { expect("%0.2f km/h^2" % unit).to eq("15999.90 km/h^2") } 18 | specify { expect("km/h^2" % unit).to eq("15999.9 km/h^2") } 19 | specify { expect("%H:%M:%S" % RubyUnits::Unit.new("1.5 h")).to eq("01:30:00") } 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/ruby_units/subclass_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "Subclass" do 4 | subject(:subclass) { Class.new(RubyUnits::Unit) } 5 | 6 | it "can be subclassed" do 7 | expect(subclass).to be < RubyUnits::Unit 8 | end 9 | 10 | it "can be instantiated" do 11 | expect(subclass.new("1 m")).to be_a(RubyUnits::Unit) 12 | end 13 | 14 | it "compares to the parent class" do 15 | expect(subclass.new("1 m")).to eq(RubyUnits::Unit.new("1 m")) 16 | end 17 | 18 | it "can be added to another subclass instance" do 19 | expect(subclass.new("1 m") + subclass.new("1 m")).to eq(RubyUnits::Unit.new("2 m")) 20 | end 21 | 22 | it "returns a subclass object when added to another instance of a subclass" do 23 | expect(subclass.new("1 m") + subclass.new("1 m")).to be_an_instance_of(subclass) 24 | end 25 | 26 | it "returns an instance of the parent class when added to another instance of a subclass" do 27 | expect(RubyUnits::Unit.new("1 m") + subclass.new("1 m")).to be_an_instance_of(RubyUnits::Unit) 28 | end 29 | 30 | it "returns an instance of the subclass when added to an instance of the parent class" do 31 | expect(subclass.new("1 m") + RubyUnits::Unit.new("1 m")).to be_an_instance_of(subclass) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /spec/ruby_units/temperature_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe "temperatures" do 4 | describe "redefine display name" do 5 | before(:all) do 6 | Unit.redefine!("tempC") do |c| 7 | c.aliases = %w[tC tempC] 8 | c.display_name = "tC" 9 | end 10 | 11 | Unit.redefine!("tempF") do |f| 12 | f.aliases = %w[tF tempF] 13 | f.display_name = "tF" 14 | end 15 | 16 | Unit.redefine!("tempR") do |f| 17 | f.aliases = %w[tR tempR] 18 | f.display_name = "tR" 19 | end 20 | 21 | Unit.redefine!("tempK") do |f| 22 | f.aliases = %w[tK tempK] 23 | f.display_name = "tK" 24 | end 25 | end 26 | 27 | after(:all) do 28 | # define the temp units back to normal 29 | Unit.define("tempK") do |unit| 30 | unit.scalar = 1 31 | unit.numerator = %w[] 32 | unit.aliases = %w[tempK] 33 | unit.kind = :temperature 34 | end 35 | 36 | Unit.define("tempC") do |tempc| 37 | tempc.definition = RubyUnits::Unit.new("1 tempK") 38 | end 39 | 40 | temp_convert_factor = Rational(2_501_999_792_983_609, 4_503_599_627_370_496) # approximates 1/1.8 41 | 42 | Unit.define("tempF") do |tempf| 43 | tempf.definition = RubyUnits::Unit.new(temp_convert_factor, "tempK") 44 | end 45 | 46 | Unit.define("tempR") do |tempr| 47 | tempr.definition = RubyUnits::Unit.new("1 tempF") 48 | end 49 | end 50 | 51 | describe "RubyUnits::Unit.new('100 tC')" do 52 | subject { RubyUnits::Unit.new("100 tC") } 53 | 54 | describe "#scalar" do 55 | subject { super().scalar } 56 | 57 | it { is_expected.to be_within(0.001).of 100 } 58 | end 59 | 60 | describe "#units" do 61 | subject { super().units } 62 | 63 | it { is_expected.to eq("tC") } 64 | end 65 | 66 | describe "#kind" do 67 | subject { super().kind } 68 | 69 | it { is_expected.to eq(:temperature) } 70 | end 71 | 72 | it { is_expected.to be_temperature } 73 | it { is_expected.to be_degree } 74 | it { is_expected.not_to be_base } 75 | it { is_expected.not_to be_unitless } 76 | it { is_expected.not_to be_zero } 77 | 78 | describe "#base" do 79 | subject { super().base } 80 | 81 | it { is_expected.to be_within(RubyUnits::Unit.new("0.01 degK")).of RubyUnits::Unit.new("373.15 tempK") } 82 | end 83 | 84 | describe "#temperature_scale" do 85 | subject { super().temperature_scale } 86 | 87 | it { is_expected.to eq("degC") } 88 | end 89 | end 90 | 91 | context "between temperature scales" do 92 | # NOTE: that 'temp' units are for temperature readings on a scale, while 'deg' units are used to represent 93 | # differences between temperatures, offsets, or other differential temperatures. 94 | 95 | specify { expect(RubyUnits::Unit.new("100 tC")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("373.15 tempK")) } 96 | specify { expect(RubyUnits::Unit.new("0 tC")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("273.15 tempK")) } 97 | specify { expect(RubyUnits::Unit.new("37 tC")).to be_within(RubyUnits::Unit.new("0.01 degK")).of(RubyUnits::Unit.new("310.15 tempK")) } 98 | specify { expect(RubyUnits::Unit.new("-273.15 tC")).to eq(RubyUnits::Unit.new("0 tempK")) } 99 | 100 | specify { expect(RubyUnits::Unit.new("212 tF")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("373.15 tempK")) } 101 | specify { expect(RubyUnits::Unit.new("32 tF")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("273.15 tempK")) } 102 | specify { expect(RubyUnits::Unit.new("98.6 tF")).to be_within(RubyUnits::Unit.new("0.01 degK")).of(RubyUnits::Unit.new("310.15 tempK")) } 103 | specify { expect(RubyUnits::Unit.new("-459.67 tF")).to eq(RubyUnits::Unit.new("0 tempK")) } 104 | 105 | specify { expect(RubyUnits::Unit.new("671.67 tR")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("373.15 tempK")) } 106 | specify { expect(RubyUnits::Unit.new("491.67 tR")).to be_within(RubyUnits::Unit.new("0.001 degK")).of(RubyUnits::Unit.new("273.15 tempK")) } 107 | specify { expect(RubyUnits::Unit.new("558.27 tR")).to be_within(RubyUnits::Unit.new("0.01 degK")).of(RubyUnits::Unit.new("310.15 tempK")) } 108 | specify { expect(RubyUnits::Unit.new("0 tR")).to eq(RubyUnits::Unit.new("0 tempK")) } 109 | 110 | specify { expect(RubyUnits::Unit.new("100 tK").convert_to("tempC")).to be_within(RubyUnits::Unit.new("0.01 degC")).of(RubyUnits::Unit.new("-173.15 tempC")) } 111 | specify { expect(RubyUnits::Unit.new("100 tK").convert_to("tempF")).to be_within(RubyUnits::Unit.new("0.01 degF")).of(RubyUnits::Unit.new("-279.67 tempF")) } 112 | specify { expect(RubyUnits::Unit.new("100 tK").convert_to("tempR")).to be_within(RubyUnits::Unit.new("0.01 degR")).of(RubyUnits::Unit.new("180 tempR")) } 113 | 114 | specify { expect(RubyUnits::Unit.new("32 tF").convert_to("tempC")).to eq(RubyUnits::Unit.new("0 tC")) } 115 | 116 | # See https://github.com/olbrich/ruby-units/issues/251 117 | specify { expect(RubyUnits::Unit.new("32 tF").convert_to("tC")).to eq(RubyUnits::Unit.new("0 tC")) } 118 | end 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/ruby_units/time_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe RubyUnits::Time do 4 | let(:now) { Time.at(1_303_656_390, in: "-04:00") } 5 | 6 | before do 7 | allow(Time).to receive(:now).and_return(now) 8 | end 9 | 10 | # We need to make sure this works will all the variations of the way that ruby 11 | # allows `at` to be called. 12 | describe ".at" do 13 | subject(:date_unit) { Date.new(2011, 4, 1).to_unit - Date.new(1970, 1, 1) } 14 | 15 | specify { expect(Time.at(Time.at(0)).utc.strftime("%D %T")).to eq("01/01/70 00:00:00") } 16 | specify { expect(Time.at(date_unit).getutc.strftime("%D %T")).to eq("04/01/11 00:00:00") } 17 | 18 | specify { expect(Time.at(date_unit, 500).usec).to eq(500) } # at(seconds, microseconds_with_fraction) 19 | specify { expect(Time.at(date_unit, 5, :millisecond).usec).to eq(5000) } # at(seconds, milliseconds, :millisecond) 20 | specify { expect(Time.at(date_unit, 500, :usec).usec).to eq(500) } # at(seconds, microseconds, :usec) 21 | specify { expect(Time.at(date_unit, 500, :microsecond).usec).to eq(500) } # at(seconds, microseconds, :microsecond) 22 | specify { expect(Time.at(date_unit, 500, :nsec).nsec).to eq(500) } # at(seconds, nanoseconds, :nsec) 23 | specify { expect(Time.at(date_unit, 500, :nanosecond).nsec).to eq(500) } # at(seconds, nanoseconds, :nanosecond) 24 | specify { expect(Time.at(date_unit, in: "-05:00").utc_offset).to eq(-18_000) } # at(seconds, in: timezone) 25 | specify { expect(Time.at(date_unit, 500, in: "-05:00")).to have_attributes(usec: 500, utc_offset: -18_000) } # at(seconds, microseconds, in: timezone) 26 | specify { expect(Time.at(date_unit, 5, :millisecond, in: "-05:00")).to have_attributes(usec: 5000, utc_offset: -18_000) } # at(seconds, milliseconds, :millisecond, in: timezone) 27 | specify { expect(Time.at(date_unit, 500, :usec, in: "-05:00")).to have_attributes(usec: 500, utc_offset: -18_000) } # at(seconds, microseconds, :usec, in: timezone) 28 | specify { expect(Time.at(date_unit, 500, :microsecond, in: "-05:00")).to have_attributes(usec: 500, utc_offset: -18_000) } # at(seconds, microseconds, :microsecond, in: timezone) 29 | specify { expect(Time.at(date_unit, 500, :nsec, in: "-05:00")).to have_attributes(nsec: 500, utc_offset: -18_000) } # at(seconds, nanoseconds, :nsec, in: timezone) 30 | specify { expect(Time.at(date_unit, 500, :nanosecond, in: "-05:00")).to have_attributes(nsec: 500, utc_offset: -18_000) } # at(seconds, nanoseconds, :nanosecond, in: timezone) 31 | 32 | specify { expect(Time.at(now.to_i, 500).usec).to eq(500) } # at(seconds, microseconds_with_fraction) 33 | specify { expect(Time.at(now.to_i, 5, :millisecond).usec).to eq(5000) } # at(seconds, milliseconds, :millisecond) 34 | specify { expect(Time.at(now.to_i, 500, :usec).usec).to eq(500) } # at(seconds, microseconds, :usec) 35 | specify { expect(Time.at(now.to_i, 500, :microsecond).usec).to eq(500) } # at(seconds, microseconds, :microsecond) 36 | specify { expect(Time.at(now.to_i, 500, :nsec).nsec).to eq(500) } # at(seconds, nanoseconds, :nsec) 37 | specify { expect(Time.at(now.to_i, 500, :nanosecond).nsec).to eq(500) } # at(seconds, nanoseconds, :nanosecond) 38 | specify { expect(Time.at(now.to_i, in: "-05:00").utc_offset).to eq(-18_000) } # at(seconds, in: timezone) 39 | specify { expect(Time.at(now.to_i, 500, in: "-05:00")).to have_attributes(usec: 500, utc_offset: -18_000) } # at(seconds, microseconds, in: timezone) 40 | specify { expect(Time.at(now.to_i, 5, :millisecond, in: "-05:00")).to have_attributes(usec: 5000, utc_offset: -18_000) } # at(seconds, milliseconds, :millisecond, in: timezone) 41 | specify { expect(Time.at(now.to_i, 500, :usec, in: "-05:00")).to have_attributes(usec: 500, utc_offset: -18_000) } # at(seconds, microseconds, :usec, in: timezone) 42 | specify { expect(Time.at(now.to_i, 500, :microsecond, in: "-05:00")).to have_attributes(usec: 500, utc_offset: -18_000) } # at(seconds, microseconds, :microsecond, in: timezone) 43 | specify { expect(Time.at(now.to_i, 500, :nsec, in: "-05:00")).to have_attributes(nsec: 500, utc_offset: -18_000) } # at(seconds, nanoseconds, :nsec, in: timezone) 44 | specify { expect(Time.at(now.to_i, 500, :nanosecond, in: "-05:00")).to have_attributes(nsec: 500, utc_offset: -18_000) } # at(seconds, nanoseconds, :nanosecond, in: timezone) 45 | end 46 | 47 | describe ".in" do 48 | specify { expect(Time.in("5 min")).to have_attributes(to_s: "2011-04-24 10:51:30 -0400") } 49 | specify { expect(Time.in([5, "min"])).to have_attributes(to_s: "2011-04-24 10:51:30 -0400") } 50 | specify { expect { Time.in(300) }.to raise_error(ArgumentError, "Incompatible Units ('300' not compatible with 's')") } 51 | end 52 | 53 | describe "#to_unit" do 54 | subject { now.to_unit } 55 | 56 | it { is_expected.to have_attributes(units: "s", scalar: now.to_f, kind: :time) } 57 | 58 | context "when an argument is passed" do 59 | subject { now.to_unit("min") } 60 | 61 | it { is_expected.to have_attributes(units: "min", scalar: now.to_f / 60.0, kind: :time) } 62 | end 63 | end 64 | 65 | describe "addition (+)" do 66 | specify { expect(Time.now + 1).to eq(now + 1) } 67 | specify { expect(Time.now + RubyUnits::Unit.new("10 min")).to eq(now + 600) } 68 | end 69 | 70 | describe "subtraction (-)" do 71 | specify { expect(Time.now - 1).to eq(now - 1) } 72 | specify { expect(Time.now - RubyUnits::Unit.new("10 min")).to eq(now - 600) } 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/ruby_units/utf-8/unit_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require File.dirname(__FILE__) + "/../../spec_helper" 4 | 5 | describe Unit, "Degrees" do 6 | context "when the UTF-8 symbol is used" do 7 | context "Angles" do 8 | it "is a degree" do 9 | expect(RubyUnits::Unit.new("180\u00B0").units).to eq("deg") 10 | end 11 | end 12 | 13 | context "Temperature" do 14 | it "is a degree Celcius" do 15 | expect(RubyUnits::Unit.new("180\u00B0C").units).to eq("degC") 16 | end 17 | 18 | it "is a degree Fahrenheit" do 19 | expect(RubyUnits::Unit.new("180\u00B0F").units).to eq("degF") 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require "simplecov" 4 | require "bundler/setup" 5 | Bundler.require(:development, :test) 6 | require "rspec/core" 7 | 8 | SimpleCov.start do 9 | add_filter "/spec/" 10 | add_filter "/test/" 11 | skip_token "nocov_19" 12 | end 13 | 14 | RSpec.configure do |config| 15 | config.order = :random 16 | config.filter_run_including focus: true 17 | config.run_all_when_everything_filtered = true 18 | end 19 | 20 | require_relative "../lib/ruby-units" 21 | --------------------------------------------------------------------------------