diff --git a/.autotest b/.autotest deleted file mode 100644 index b32fba98..00000000 --- a/.autotest +++ /dev/null @@ -1,41 +0,0 @@ -# -*- ruby -*- - -require 'autotest/growl' -require 'autotest/fsevent' -require 'autotest/bundler' - -module AutoGrowl - def self.growl title, msg, pri=0 - system "growlnotify -n autotest --image /Applications/Mail.app/Contents/Resources/Caution.tiff -p #{pri} -m #{msg.inspect} #{title}" - end - - Autotest.add_hook :run do |at| - growl "Run", "Run" unless $TESTING - end - - Autotest.add_hook :red do |at| - growl "Tests Failed", "#{at.files_to_test.size} tests failed", 2 - end - - Autotest.add_hook :green do |at| - growl "Tests Passed", "All tests passed", -2 if at.tainted - end - - Autotest.add_hook :init do |at| - growl "autotest", "autotest was started" unless $TESTING - end - - Autotest.add_hook :interrupt do |at| - growl "autotest", "autotest was reset" unless $TESTING - nil - end - - Autotest.add_hook :quit do |at| - growl "autotest", "autotest is exiting" unless $TESTING - end - - Autotest.add_hook :all do |at|_hook - growl "autotest", "Tests have fully passed", -2 unless $TESTING - end -end - diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 00000000..2c12d30e --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,25 @@ +--- +engines: + bundler-audit: + enabled: false + csslint: + enabled: false + duplication: + enabled: true + config: + languages: + - ruby + eslint: + enabled: false + fixme: + enabled: true + rubocop: + enabled: true + channel: rubocop-0-63 +ratings: + paths: + - "**.rb" +exclude_paths: +- ruby-units.gemspec +- bin/ +- spec/ diff --git a/.csslintrc b/.csslintrc new file mode 100644 index 00000000..aacba956 --- /dev/null +++ b/.csslintrc @@ -0,0 +1,2 @@ +--exclude-exts=.min.css +--ignore=adjoining-classes,box-model,ids,order-alphabetical,unqualified-attributes diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..96212a35 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +**/*{.,-}min.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..9faa3750 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,213 @@ +ecmaFeatures: + modules: true + jsx: true + +env: + amd: true + browser: true + es6: true + jquery: true + node: true + +# http://eslint.org/docs/rules/ +rules: + # Possible Errors + comma-dangle: [2, never] + no-cond-assign: 2 + no-console: 0 + no-constant-condition: 2 + no-control-regex: 2 + no-debugger: 2 + no-dupe-args: 2 + no-dupe-keys: 2 + no-duplicate-case: 2 + no-empty: 2 + no-empty-character-class: 2 + no-ex-assign: 2 + no-extra-boolean-cast: 2 + no-extra-parens: 0 + no-extra-semi: 2 + no-func-assign: 2 + no-inner-declarations: [2, functions] + no-invalid-regexp: 2 + no-irregular-whitespace: 2 + no-negated-in-lhs: 2 + no-obj-calls: 2 + no-regex-spaces: 2 + no-sparse-arrays: 2 + no-unexpected-multiline: 2 + no-unreachable: 2 + use-isnan: 2 + valid-jsdoc: 0 + valid-typeof: 2 + + # Best Practices + accessor-pairs: 2 + block-scoped-var: 0 + complexity: [2, 6] + consistent-return: 0 + curly: 0 + default-case: 0 + dot-location: 0 + dot-notation: 0 + eqeqeq: 2 + guard-for-in: 2 + no-alert: 2 + no-caller: 2 + no-case-declarations: 2 + no-div-regex: 2 + no-else-return: 0 + no-empty-label: 2 + no-empty-pattern: 2 + no-eq-null: 2 + no-eval: 2 + no-extend-native: 2 + no-extra-bind: 2 + no-fallthrough: 2 + no-floating-decimal: 0 + no-implicit-coercion: 0 + no-implied-eval: 2 + no-invalid-this: 0 + no-iterator: 2 + no-labels: 0 + no-lone-blocks: 2 + no-loop-func: 2 + no-magic-number: 0 + no-multi-spaces: 0 + no-multi-str: 0 + no-native-reassign: 2 + no-new-func: 2 + no-new-wrappers: 2 + no-new: 2 + no-octal-escape: 2 + no-octal: 2 + no-proto: 2 + no-redeclare: 2 + no-return-assign: 2 + no-script-url: 2 + no-self-compare: 2 + no-sequences: 0 + no-throw-literal: 0 + no-unused-expressions: 2 + no-useless-call: 2 + no-useless-concat: 2 + no-void: 2 + no-warning-comments: 0 + no-with: 2 + radix: 2 + vars-on-top: 0 + wrap-iife: 2 + yoda: 0 + + # Strict + strict: 0 + + # Variables + init-declarations: 0 + no-catch-shadow: 2 + no-delete-var: 2 + no-label-var: 2 + no-shadow-restricted-names: 2 + no-shadow: 0 + no-undef-init: 2 + no-undef: 0 + no-undefined: 0 + no-unused-vars: 0 + no-use-before-define: 0 + + # Node.js and CommonJS + callback-return: 2 + global-require: 2 + handle-callback-err: 2 + no-mixed-requires: 0 + no-new-require: 0 + no-path-concat: 2 + no-process-exit: 2 + no-restricted-modules: 0 + no-sync: 0 + + # Stylistic Issues + array-bracket-spacing: 0 + block-spacing: 0 + brace-style: 0 + camelcase: 0 + comma-spacing: 0 + comma-style: 0 + computed-property-spacing: 0 + consistent-this: 0 + eol-last: 0 + func-names: 0 + func-style: 0 + id-length: 0 + id-match: 0 + indent: 0 + jsx-quotes: 0 + key-spacing: 0 + linebreak-style: 0 + lines-around-comment: 0 + max-depth: 0 + max-len: 0 + max-nested-callbacks: 0 + max-params: 0 + max-statements: [2, 30] + new-cap: 0 + new-parens: 0 + newline-after-var: 0 + no-array-constructor: 0 + no-bitwise: 0 + no-continue: 0 + no-inline-comments: 0 + no-lonely-if: 0 + no-mixed-spaces-and-tabs: 0 + no-multiple-empty-lines: 0 + no-negated-condition: 0 + no-nested-ternary: 0 + no-new-object: 0 + no-plusplus: 0 + no-restricted-syntax: 0 + no-spaced-func: 0 + no-ternary: 0 + no-trailing-spaces: 0 + no-underscore-dangle: 0 + no-unneeded-ternary: 0 + object-curly-spacing: 0 + one-var: 0 + operator-assignment: 0 + operator-linebreak: 0 + padded-blocks: 0 + quote-props: 0 + quotes: 0 + require-jsdoc: 0 + semi-spacing: 0 + semi: 0 + sort-vars: 0 + space-after-keywords: 0 + space-before-blocks: 0 + space-before-function-paren: 0 + space-before-keywords: 0 + space-in-parens: 0 + space-infix-ops: 0 + space-return-throw-case: 0 + space-unary-ops: 0 + spaced-comment: 0 + wrap-regex: 0 + + # ECMAScript 6 + arrow-body-style: 0 + arrow-parens: 0 + arrow-spacing: 0 + constructor-super: 0 + generator-star-spacing: 0 + no-arrow-condition: 0 + no-class-assign: 0 + no-const-assign: 0 + no-dupe-class-members: 0 + no-this-before-super: 0 + no-var: 0 + object-shorthand: 0 + prefer-arrow-callback: 0 + prefer-const: 0 + prefer-reflect: 0 + prefer-spread: 0 + prefer-template: 0 + require-yield: 0 diff --git a/.gitignore b/.gitignore index 73dc5afb..9878e20a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,15 @@ -pkg -.yardoc -coverage +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ +/bin/ +# rspec failure tracking +.rspec_status +.byebug_history + +# don't check in Gemfile.lock for a gem Gemfile.lock -*.rbc -.rspec -.rvmrc -.rbx -.idea -doc diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..83e16f80 --- /dev/null +++ b/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 00000000..a3dac22a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,20 @@ +AllCops: + TargetRubyVersion: 2.3 + Exclude: + - ruby-units.gemspec + - bin/* +Style/CaseEquality: + Enabled: false +Style/FrozenStringLiteralComment: + Enabled: false +Metrics/LineLength: + Enabled: false +Style/SingleLineBlockParams: + Enabled: false +Naming/FileName: + Exclude: + - 'lib/ruby-units.rb' +Style/FormatString: + Enabled: false +Style/DateTime: + Enabled: false diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 00000000..bc4abe86 --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.3.8 diff --git a/.travis.yml b/.travis.yml index 4b6f75d1..0e6f5a7a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,27 @@ +sudo: false language: ruby +cache: bundler +dist: xenial rvm: - - 1.9.3 - - jruby-19mode # JRuby in 1.9 mode -jdk: - - openjdk6 - - openjdk7 + - 2.6 + - 2.5 + - 2.4 + - 2.3 + - jruby-9.1 + - jruby-9.2 + - jruby-head + - ruby-head +before_install: + - gem update --system +before_script: + - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter + - chmod +x ./cc-test-reporter + - ./cc-test-reporter before-build +script: bundle exec rake +after_script: + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT +bundler_args: --without development matrix: - exclude: - - rvm: 1.9.3 - jdk: openjdk7 -script: bundle exec rake --trace \ No newline at end of file + allow_failures: + - rvm: ruby-head + - rvm: jruby-head diff --git a/CHANGELOG.txt b/CHANGELOG.txt index e868ebc7..89b8c95a 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,5 +1,37 @@ Change Log for Ruby-units ========================= +2018-03-06 2.3.0 * Fix add (+) and subtract (-) for BigDecimal scalars. (see #167) + * Update ruby versions (#163) + * fix: temperature converting rational issue (#164) +2017-12-07 2.2.1 * fix an issue with formatting of rational scalars (see #159) +2017-08-07 2.2.0 * add support for ruby 2.4.1 + * drop support for ruby 2.1.0 + * remove dependency on mathn (see #157) +2016-12-28 2.1.0 * add support for ruby 2.4.0 + * allow configuration for optional separator on output + * Fix issue #105 -- change 'grad' to 'gon' +2015-11-07 2.0.0 * remove support for ruby versions less than 2.0 + * remove `.unit` and `.u` from String + * remove `.unit` from Date + * Fix an issue with redefining units that have already been cached + * remove 'U()' and 'u()' constructors + * Fix issue #123 -- Fixes for singular unit parsing +2015-07-16 * Fix issue #129 -- doesn't handle number in the denominator +2015-05-09 * update test harness to use rspec 3 +2014-02-21 1.4.5 * Fix issue #98 -- add mcg as a valid unit +2013-07-19 1.4.4 * Fix issue #4 -- .best_prefix method + * Fix issue #60 -- Consider placing Unit in a module + * Fix issue #75 -- Siemens is kind of conductance not resistance + * Fix issue #36 -- Don't require spaces in units + * Fix issue #68 -- ditto + * Fix issue #16 -- ditto +2013-06-11 1.4.3 * Fix issue #70 -- Support passing Time objects to Time.at + * Fix issue #72 -- Remove non-existent RakeFile from gemspec + * Fix issue #71 -- Fix YAML test failure + * Fix issue #49 -- Unit instances constructed using other Unit instances are incompatible within the framework + * Fix issue #61 -- Fix failing case of subtraction losing Units class + * Fix issue #63 -- fixes an issue with to_date on 1.8.7 + * Fix issue #64 -- Aliases aren't considered in Unit.defined? method 2012-09-16 1.4.2 * Fix issue #54 -- pluralization of fluid-ounces * Fix issue #53, 51 -- incorrect definition for gee * Fix issue #52 -- add support for degree symbol @@ -11,7 +43,7 @@ Change Log for Ruby-units 2012-01-02 1.4.0 * Fix some definitions that were just wrong (amu, dalton) * Definition uses name of unit if no aliases provided * Refactor definition process. New units are immediately available -2011-12-31 * Define standard units in terms of base and other standard units -- more internally consistent +2011-12-31 * Define standard units in terms of base and other standard units -- more internally consistent and less prone to round-off errors. * add 'poundal' * remove 'wtpercent' @@ -19,7 +51,7 @@ Change Log for Ruby-units * Define compound units with base units for consistency * distinguish between a league and a nautical league * NOTE: the new unit definition DSL is not backwardly compatible with the old method - (which is now deprecated). + (which is now deprecated). * Fix issue #27 2011-12-18 * Can define a display_name for units (fixes #26) 2011-12-04 * Documentation improvements @@ -47,17 +79,17 @@ Change Log for Ruby-units 2007-12-13 1.1.3 * fixed a minor bug with string % 2007-12-12 1.1.2 * fixed a bug with format strings * detect if ruby 1.8.6 is installed and use its' to_date function - + 2007-07-14 1.1.1 * fixed bug that would prevent creating '' units, which prevented rounding from working * tests do not fail if Uncertain gem is not installed, you just get an annoying warning message - + 2007-01-28 1.1.0 * completely revamped the temperature handling system (see README) * fixed some spelling errors in some units * fixed to_datetime and to_date to convert durations to datetimes and dates' -2007-01-24 1.0.2 * Minor changes in the way powers are calculated to support Uncertain +2007-01-24 1.0.2 * Minor changes in the way powers are calculated to support Uncertain numbers better. * Fixed parsing bug with Uncertain Numbers * added resolution / typography units (pixels, points, pica) @@ -73,17 +105,17 @@ Change Log for Ruby-units '1+1i mm'.unit to get a complex unit. * Taking the root of a negative unit will give you a complex unit * fixed unary minus to work again - * Math.hypot now takes units. Both parameters must be the compatible - units or it will assert. Units will be converted to a common base + * Math.hypot now takes units. Both parameters must be the compatible + units or it will assert. Units will be converted to a common base before use. - * Can now specify units in rational numbers, i.e., '1/4 cup'.unit + * Can now specify units in rational numbers, i.e., '1/4 cup'.unit * Seems like a good time to move to 1.0 status - + 2006-12-15 0.3.9 * forgot to increment the version in the gem file..ooops. -2006-12-15 0.3.8 * Any object that supports a 'to_unit' method will now be +2006-12-15 0.3.8 * Any object that supports a 'to_unit' method will now be automatically coerced to a unit during math operations. - + 2006-12-14 0.3.7 * improved handling of percents and added a 'wt%' unit equivalent to 1 g/dl. * Improved handling for units with non-alphanumeric names @@ -97,21 +129,21 @@ Change Log for Ruby-units * to_int now coerces the result to an actual Integer, but only works properly for unitless Units. -2006-10-27 0.3.4 * Fixed a few more parsing bugs so that it will properly +2006-10-27 0.3.4 * Fixed a few more parsing bugs so that it will properly complain about malformed units. * Fixed a bug that prevents proper use of percents - * several minor tweaks + * several minor tweaks * some improved Date and DateTime handling * can convert between Date, DateTime, and Time objects * Time math will now return a DateTime if it goes out of range. -2006-10-03 0.3.3 * Apparently I can't do math late at night. +2006-10-03 0.3.3 * Apparently I can't do math late at night. Fixed a bug that would cause problems when adding - or subtracting units to a unit with a zero scalar. + or subtracting units to a unit with a zero scalar. * Date and DateTime objects can be converted to 'units' -2006-10-03 0.3.2 * More minor bug fixes +2006-10-03 0.3.2 * More minor bug fixes (now fixes a minor name collision with rails) 2006-10-02 0.3.1 * minor bug fixes @@ -119,10 +151,10 @@ Change Log for Ruby-units 2006-10-02 0.3.0 * Performance enhanced by caching results of many functions (Thanks to Kurt Stephens for pushing this.) * Throws an exception if the unit is not recognized - * units can now identify what 'kind' they are + * units can now identify what 'kind' they are (:length, :mass, etc..) - * New constructors: - Unit(1,"mm") + * New constructors: + Unit(1,"mm") Unit(1,"mm/s") Unit(1,"mm","s") @@ -135,8 +167,8 @@ Change Log for Ruby-units * 'string'.time returns a Time object or a DateTime if the Time object fails * 'string'.datetime returns a DateTime or a Time if the - DateTime fails - + DateTime fails + 2006-09-19 0.2.2 * tweaked temperature handling a bit. Now enter temperatures like this: '0 tempC'.unit #=> 273.15 degK @@ -144,7 +176,7 @@ Change Log for Ruby-units problems when temperatures are used in equations. * added Time.in("5 min") * added Unit.to_unit to simplify some calls - + 2006-09-18 0.2.1 * Trig math functions (sin, cos, tan, sinh, cosh, tanh) accept units that can be converted to radians Math.sin("90 deg".unit) => 1.0 @@ -156,14 +188,14 @@ Change Log for Ruby-units Time.now + "1 hr".unit => Mon Sep 18 11:51:29 EDT 2006 * can output time in 'hh:mm:ss' format by using 'unit.to_s(:time)' - * added time helper methods - ago, - since(Time/DateTime), - until(Time/DateTime), - from(Time/DateTime), + * added time helper methods + ago, + since(Time/DateTime), + until(Time/DateTime), + from(Time/DateTime), before(Time/DateTime), and after(Time/DateTime) - * Time helpers also work on strings. In this case they + * Time helpers also work on strings. In this case they are first converted to units '5 min'.from_now '1 week'.ago @@ -177,19 +209,19 @@ Change Log for Ruby-units output * can use U'1 mm' or '1 mm'.u to specify units now -2006-09-17 * can now use the '%' format specifier like +2006-09-17 * can now use the '%' format specifier like '%0.2f' % '1 mm'.unit #=> '1.00 mm' * works nicely with time now. '1 week'.unit + Time.now => 1.159e+09 s - Time.at('1.159e+09 s'.unit) + Time.at('1.159e+09 s'.unit) => Sat Sep 23 04:26:40 EDT 2006 - "1.159e9 s".unit.time + "1.159e9 s".unit.time => Sat Sep 23 04:26:40 EDT 2006 * Time.now.unit => 1.159e9 s * works well with 'Uncertain' numerics (www.rubyforge.org/projects/uncertain) * Improved parsing - + 2006-08-28 0.2.0 * Added 'ruby_unit.rb' file so that requires will still work if the wrong name is used * Added 'to' as an alias to '>>' so conversions can be @@ -212,13 +244,13 @@ Change Log for Ruby-units from forcing the conversion. To get the scalar, just use 'unit.scalar' * 'inspect' returns string representation - * better edge-case detection with math functions. + * better edge-case detection with math functions. "0 mm".unit**-1 now throws a ZeroDivisionError exception - * Ranges can make a series of units, so long as the end + * Ranges can make a series of units, so long as the end points have integer scalars. * Fixed a parsing bug with feet/pounds and scientific numbers - + 2006-08-22 0.1.1 * Added new format option "1 mm".to_unit("in") now converts the result to the indicated units * Fixed some naming issues so that the gem name matches @@ -235,12 +267,5 @@ Change Log for Ruby-units [1,'mm','s'].unit === "1 mm/s".unit 2.5.unit === "2.5".unit * Added instructions on how to add custom units - -2006-08-22 0.1.0 * Initial Release - - - - - - +2006-08-22 0.1.0 * Initial Release diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..ba00be10 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at kevin.olbrich@mckesson.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Gemfile b/Gemfile index a5f30db9..d682a7aa 100644 --- a/Gemfile +++ b/Gemfile @@ -1,15 +1,18 @@ -source "http://rubygems.org" +source 'https://rubygems.org' group :development do - gem 'bundler', '~> 1.0' - gem 'jeweler' + gem 'pry', '0.12.2' # Pry fails with error on load with version 0.13.1 - `NameError: uninitialized constant Pry::Command::ExitAll` + gem 'pry-byebug', platform: :mri + gem 'ruby-maven', platform: :jruby + gem 'ruby-prof', platform: :mri end +# This is a minimal set of Gems required to build on travis group :test do + gem 'bundler' gem 'rake' - gem 'rcov', :platforms => :mri_18 - gem 'simplecov', :require => false, :platforms => :mri_19 - gem 'simplecov-html', :platforms => :mri_19 - gem 'rspec', '~>2.5' + gem 'rspec' + gem 'simplecov' end +gemspec diff --git a/Guardfile b/Guardfile new file mode 100644 index 00000000..b1d0e6cb --- /dev/null +++ b/Guardfile @@ -0,0 +1,31 @@ +# A sample Guardfile +# More info at https://github.com/guard/guard#readme + +## Uncomment and set this to only include directories you want to watch +directories %w[lib spec] + +## Uncomment to clear the screen before every task +clearing :on + +# Note: The cmd option is now required due to the increasing number of ways +# rspec may be run, below are examples of the most common uses. +# * bundler: 'bundle exec rspec' +# * bundler binstubs: 'bin/rspec' +# * spring: 'bin/rspec' (This will use spring if running and you have +# installed the spring binstubs per the docs) +# * zeus: 'zeus rspec' (requires the server to be started separately) +# * 'just' rspec: 'rspec' + +guard :rspec, cmd: 'bundle exec rspec' do + require 'ostruct' + + # Generic Ruby apps + rspec = OpenStruct.new + rspec.spec = ->(m) { "spec/#{m}_spec.rb" } + rspec.spec_dir = 'spec' + rspec.spec_helper = 'spec/spec_helper.rb' + + watch(%r{^spec/.+_spec\.rb$}) + watch(%r{^lib/(.+)\.rb$}) { |m| rspec.spec.call(m[1]) } + watch(rspec.spec_helper) { rspec.spec_dir } +end diff --git a/LICENSE.txt b/LICENSE.txt index 4ccbfebf..8f6b4010 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,20 +1,21 @@ -Copyright (c) 2006-2012 Kevin C. Olbrich, Ph.D. +The MIT License (MIT) -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: +Copyright (c) 2019 Kevin Olbrich -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md index 7d61e19f..a1cb5dd5 100644 --- a/README.md +++ b/README.md @@ -1,154 +1,217 @@ -# Ruby Units +Ruby Units +========== [![Build Status](https://secure.travis-ci.org/olbrich/ruby-units.png)](http://travis-ci.org/olbrich/ruby-units) +[![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) -Kevin C. Olbrich, Ph.D. - -[Sciwerks.com](http://www.sciwerks.com) +Kevin C. Olbrich, Ph.D. Project page: [http://github.com/olbrich/ruby-units](http://github.com/olbrich/ruby-units) -## Introduction -Many technical applications make use of specialized calculations at some point. Frequently, these calculations require unit conversions to ensure accurate results. Needless to say, this is a pain to properly keep track of, and is prone to numerous errors. - -## Solution -The 'Ruby units' gem is designed so simplify the handling of units for scientific calculations. The units of each quantity are specified when a Unit object is created and the Unit class will handle all subsequent conversions and manipulations to ensure an accurate result. - -## Installation: -This package may be installed using: `gem install ruby-units` - -## Usage: - unit = Unit.new("1") # constant only - unit = Unit("mm") # unit only (defaults to a scalar of 1) - unit = Unit("1 mm") # create a simple unit - unit = Unit("1 mm/s") # a compound unit - unit = Unit("1 mm s^-1") # in exponent notation - unit = Unit("1 kg*m^2/s^2") # complex unit - unit = Unit("1 kg m^2 s^-2") # complex unit - unit = Unit("1 mm") # shorthand - unit = "1 mm".to_unit # convert string object - unit = object.to_unit # convert any object using object.to_s - unit = Unit('1/4 cup') # Rational number - unit = Unit('1+1i mm') # Complex Number - -## Rules: -1. only 1 quantity per unit (with 2 exceptions... 6'5" and '8 lbs 8 oz') -2. use SI notation when possible -3. avoid using spaces in unit names - -## Unit compatability: -Many methods require that the units of two operands are compatible. Compatible units are those that can be easily converted into each other, such as 'meters' and 'feet'. - - unit1 =~ unit2 #=> true if units are compatible - unit1.compatible?(unit2) #=> true if units are compatible - -## Unit Math: - Unit#+() # Add. only works if units are compatible - Unit#-() # Subtract. only works if units are compatible - Unit#*() # Multiply. - Unit#/() # Divide. - Unit#**() # Exponentiate. Exponent must be an integer, can be positive, negative, or zero - Unit#inverse # Returns 1/unit - Unit#abs # Returns absolute value of the unit quantity. Strips off the units - Unit#ceil # rounds quantity to next highest integer - Unit#floor # rounds quantity down to next lower integer - Unit#round # rounds quantity to nearest integer - Unit#to_int # returns the quantity as an integer - -Unit will coerce other objects into a Unit if used in a formula. This means that .. - - Unit("1 mm") + "2 mm" == Unit("3 mm") - -This will work as expected so long as you start the formula with a Unit object. - -## Conversions & comparisons +Notes +----- + +This version removes 'mathn' as a dependency. Mathn alters the behavior of some mathematical operators, which +frequently causes unexpected behavior and can be a source of difficult to diagnose bugs. Mathn is also scheduled to be removed from +the Ruby standard library. + +Introduction +------------ + +Many technical applications make use of specialized calculations at some point. Frequently, these calculations require unit conversions to ensure accurate results. Needless to say, this is a pain to properly keep track of, and is prone to numerous errors. + +Solution +-------- + +The 'Ruby units' gem is designed to simplify the handling of units for scientific calculations. The units of each quantity are specified when a Unit object is created and the Unit class will handle all subsequent conversions and manipulations to ensure an accurate result. + +Installation: +------------- + +This package may be installed using: `gem install ruby-units` + +Usage: +------ + +``` +unit = Unit.new("1") # constant only +unit = Unit.new("mm") # unit only (defaults to a scalar of 1) +unit = Unit.new("1 mm") # create a simple unit +unit = Unit.new("1 mm/s") # a compound unit +unit = Unit.new("1 mm s^-1") # in exponent notation +unit = Unit.new("1 kg*m^2/s^2") # complex unit +unit = Unit.new("1 kg m^2 s^-2") # complex unit +unit = Unit.new("1 mm") # shorthand +unit = "1 mm".to_unit # convert string object +unit = object.to_unit # convert any object using object.to_s +unit = Unit.new('1/4 cup') # Rational number +unit = Unit.new('1+1i mm') # Complex Number +``` + +Rules: +------ + +1. only 1 quantity per unit (with 2 exceptions... 6'5" and '8 lbs 8 oz') +2. use SI notation when possible +3. spaces in units are allowed, but ones like '11/m' will be recognized as '11 1/m'. + +Unit compatibility: +------------------- + +Many methods require that the units of two operands are compatible. Compatible units are those that can be easily converted into each other, such as 'meters' and 'feet'. + +``` +unit1 =~ unit2 #=> true if units are compatible +unit1.compatible?(unit2) #=> true if units are compatible +``` + +Unit Math: +---------- + +``` +Unit#+() # Add. only works if units are compatible +Unit#-() # Subtract. only works if units are compatible +Unit#*() # Multiply. +Unit#/() # Divide. +Unit#**() # Exponentiate. Exponent must be an integer, can be positive, negative, or zero +Unit#inverse # Returns 1/unit +Unit#abs # Returns absolute value of the unit quantity. Strips off the units +Unit#ceil # rounds quantity to next highest integer +Unit#floor # rounds quantity down to next lower integer +Unit#round # rounds quantity to nearest integer +Unit#to_int # returns the quantity as an integer +``` + +Unit will coerce other objects into a Unit if used in a formula. This means that .. + +``` +Unit.new("1 mm") + "2 mm" == Unit.new("3 mm") +``` + +This will work as expected so long as you start the formula with a Unit object. + +Conversions & comparisons +------------------------- + Units can be converted to other units in a couple of ways. - unit1 = unit >> "ft" # convert to 'feet' - unit >>= "ft" # convert and overwrite original object - unit3 = unit1 + unit2 # resulting object will have the units of unit1 - unit3 = unit1 - unit2 # resulting object will have the units of unit1 - unit1 <=> unit2 # does comparison on quantities in base units, throws an exception if not compatible - unit1 === unit2 # true if units and quantity are the same, even if 'equivalent' by <=> - unit.convert_to('ft') # convert - unit1 + unit2 >> "ft" # converts result of math to 'ft' - (unit1 + unit2).convert_to('ft') # converts result to 'ft' - +``` +unit.convert_to('ft') # convert +unit1 = unit >> "ft" # convert to 'feet' +unit >>= "ft" # convert and overwrite original object +unit3 = unit1 + unit2 # resulting object will have the units of unit1 +unit3 = unit1 - unit2 # resulting object will have the units of unit1 +unit1 <=> unit2 # does comparison on quantities in base units, throws an exception if not compatible +unit1 === unit2 # true if units and quantity are the same, even if 'equivalent' by <=> +unit1 + unit2 >> "ft" # converts result of math to 'ft' +(unit1 + unit2).convert_to('ft') # converts result to 'ft' +``` + Any object that defines a 'to_unit' method will be automatically coerced to a unit during calculations. - -## Text Output -Units will display themselves nicely based on the display_name for the units and prefixes. -Since Unit implements a Unit#to_s, all that is needed in most cases is: - - "#{Unit('1 mm')}" #=> "1 mm" - + +Text Output +----------- + +Units will display themselves nicely based on the display_name for the units and prefixes. Since Unit implements a Unit#to_s, all that is needed in most cases is: + +``` +"#{Unit.new('1 mm')}" #=> "1 mm" +``` + The to_s also accepts some options. - Unit('1.5 mm').to_s("%0.2f") # "1.50 mm". Enter any valid format - string. Also accepts strftime format - Unit('1.5 mm').to_s("in") # converts to inches before printing - Unit("2 m").to_s(:ft) # returns 6'7" - Unit("100 kg").to_s(:lbs) # returns 220 lbs, 7 oz +``` +Unit.new('1.5 mm').to_s("%0.2f") # "1.50 mm". Enter any valid format + string. Also accepts strftime format +Unit.new('1.5 mm').to_s("in") # converts to inches before printing +Unit.new("2 m").to_s(:ft) # returns 6'7" +Unit.new("100 kg").to_s(:lbs) # returns 220 lbs, 7 oz +Unit.new("100 kg").to_s(:stone) # returns 15 stone, 10 lb +``` + +Time Helpers +------------ - -## Time Helpers Time, Date, and DateTime objects can have time units added or subtracted. - Time.now + Unit("10 min") +``` +Time.now + Unit.new("10 min") +``` -Several helpers have also been defined. -Note: If you include the 'Chronic' gem, you can specify times in natural - language. +Several helpers have also been defined. Note: If you include the 'Chronic' gem, you can specify times in natural language. - Unit('min').since(DateTime.parse('9/18/06 3:00pm')) +``` + Unit.new('min').since(DateTime.parse('9/18/06 3:00pm')) +``` Durations may be entered as 'HH:MM:SS, usec' and will be returned in 'hours'. - Unit('1:00') #=> 1 h - Unit('0:30') #=> 0.5 h - Unit('0:30:30') #=> 0.5 h + 30 sec +``` +Unit.new('1:00') #=> 1 h +Unit.new('0:30') #=> 0.5 h +Unit.new('0:30:30') #=> 0.5 h + 30 sec +``` If only one ":" is present, it is interpreted as the separator between hours and minutes. -## Ranges - [Unit('0 h')..Unit('10 h')].each {|x| p x} +Ranges +------ + +``` +[Unit.new('0 h')..Unit.new('10 h')].each {|x| p x} +``` + works so long as the starting point has an integer scalar -## Math functions -All Trig math functions (sin, cos, sinh, hypot...) can take a unit as their parameter. It will be converted to radians and then used if possible. +Math functions +-------------- + +All Trig math functions (sin, cos, sinh, hypot...) can take a unit as their parameter. It will be converted to radians and then used if possible. + +Temperatures +------------ -## Temperatures Ruby-units makes a distinction between a temperature (which technically is a property) and degrees of temperature (which temperatures are measured in). -Temperature units (i.e., 'tempK') can be converted back and forth, and will take into account the differences in the zero points of the various scales. Differential temperature (e.g., '100 degC'.unit) units behave like most other units. +Temperature units (i.e., 'tempK') can be converted back and forth, and will take into account the differences in the zero points of the various scales. Differential temperature (e.g., Unit.new('100 degC')) units behave like most other units. - Unit('37 tempC').convert_to('tempF') #=> 98.6 tempF +``` +Unit.new('37 tempC').convert_to('tempF') #=> 98.6 tempF +``` Ruby-units will raise an exception if you attempt to create a temperature unit that would fall below absolute zero. -Unit math on temperatures is fairly limited. +Unit math on temperatures is fairly limited. + +``` +Unit.new('100 tempC') + Unit.new('10 degC') # '110 tempC'.to_unit +Unit.new('100 tempC') - Unit.new('10 degC') # '90 tempC'.to_unit +Unit.new('100 tempC') + Unit.new('50 tempC') # exception (can't add two temperatures) +Unit.new('100 tempC') - Unit.new('50 tempC') # '50 degC'.to_unit (get the difference between two temperatures) +Unit.new('50 tempC') - Unit.new('100 tempC') # '-50 degC'.to_unit +Unit.new('100 tempC') * scalar # '100*scalar tempC'.to_unit +Unit.new('100 tempC') / scalar # '100/scalar tempC'.to_unit +Unit.new('100 tempC') * unit # exception +Unit.new('100 tempC') / unit # exception +Unit.new('100 tempC') ** N # exception - Unit('100 tempC') + Unit('10 degC') # '110 tempC'.unit - Unit('100 tempC') - Unit('10 degC') # '90 tempC'.unit - Unit('100 tempC') + Unit('50 tempC') # exception - Unit('100 tempC') - Unit('50 tempC') # '50 degC'.unit - Unit('50 tempC') - Unit('100 tempC') # '-50 degC'.unit - Unit('100 tempC') * [scalar] # '100*scalar tempC'.unit - Unit('100 tempC') / [scalar] # '100/scalar tempC'.unit - Unit('100 tempC') * [unit] # exception - Unit('100 tempC') / [unit] # exception - Unit('100 tempC') ** N # exception +Unit.new('100 tempC').convert_to('degC') #=> Unit.new('100 degC') +``` - Unit('100 tempC').convert_to('degC') #=> Unit('100 degC') -This conversion references the 0 point on the scale of the temperature unit +This conversion references the 0 point on the scale of the temperature unit - Unit('100 degC').convert_to('tempC') #=> '-173 tempC'.unit -These conversions are always interpreted as being relative to absolute zero. -Conversions are probably better done like this... - - Unit('0 tempC') + Unit('100 degC') #=> Unit('100 tempC') +``` +Unit.new('100 degC').convert_to('tempC') #=> '-173 tempC'.to_unit +``` -## Defining Units +These conversions are always interpreted as being relative to absolute zero. Conversions are probably better done like this... + +``` +Unit.new('0 tempC') + Unit.new('100 degC') #=> Unit.new('100 tempC') +``` + +Defining Units +-------------- It is possible to define new units or redefine existing ones. @@ -156,18 +219,78 @@ It is possible to define new units or redefine existing ones. The easiest approach is to define a unit in terms of other units. - Unit.define("foobar") do |foobar| - foobar.definition = Unit("1 foo") * Unit("1 bar") # anything that results in a Unit object - foobar.aliases = %w{foobar fb} # array of synonyms for the unit - foobar.display_name = "Foobar" # How unit is displayed when output - end - +``` +Unit.define("foobar") do |foobar| + foobar.definition = Unit.new("1 foo") * Unit.new("1 bar") # anything that results in a Unit object + foobar.aliases = %w{foobar fb} # array of synonyms for the unit + foobar.display_name = "Foobar" # How unit is displayed when output +end +``` + ### Redefine Existing Unit -Redefining a unit allows the user to change a single aspect of a definition without having to re-create the entire definition. -This is useful for changing display names, adding aliases, etc. +Redefining a unit allows the user to change a single aspect of a definition without having to re-create the entire definition. This is useful for changing display names, adding aliases, etc. + +``` +Unit.redefine!("cup") do |cup| + cup.display_name = "cup" +end +``` + +### Useful methods + +1. `scalar` will return the numeric portion of the unit without the attached units +2. `base_scalar` will return the scalar in base units (SI) +3. `units` will return the name of the units (without the scalar) +4. `base` will return the unit converted to base units (SI) + +### Storing in a database + +Units can be stored in a database as either the string representation or in two separate columns defining the scalar and the units. +Note that if sorting by units is desired you will want to ensure that you are storing the scalars in a consistent unit (i.e, the base units). + +### Namespaced Class + +Sometimes the default class 'Unit' may conflict with other gems or applications. Internally ruby-units defines itself using the RubyUnits namespace. The actual class of a unit is the RubyUnits::Unit. For simplicity and backwards compatibility, the '::Unit' class is defined as an alias to '::RubyUnits::Unit'. + +To load ruby-units without this alias... + +``` +require 'ruby_units/namespaced' +``` + +When using bundler... + +``` +gem 'ruby-units', require: 'ruby_units/namespaced' +``` + +Note: when using the namespaced version, the Unit.new('unit string') helper will not be defined. + +### Configuration + +Configuration options can be set like: + +``` +RubyUnits.configure do |config| + config.separator = false +end +``` + +Currently there is only one configuration you can set: + +1. separator (true/false): should a space be used to separate the scalar from the unit part during output. + + +### NOTES + +#### Performance vs. Accuracy + +Ruby units was originally intended to provide a robust and accurate way to do arbitrary unit conversions. +In some cases, these conversions can result in the creation and garbage collection of a lot of intermediate objects during +calculations. This in turn can have a negative impact on performance. The design of ruby-units has emphasized accuracy +over speed. YMMV if you are doing a lot of math involving units. + - Unit.redefine!("cup") do |cup| - cup.display_name = "cup" - end - +## License +[![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) \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 00000000..4c774a2b --- /dev/null +++ b/Rakefile @@ -0,0 +1,6 @@ +require 'bundler/gem_tasks' +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new(:spec) + +task default: :spec diff --git a/Rakefile.rb b/Rakefile.rb deleted file mode 100644 index d41e377a..00000000 --- a/Rakefile.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'rubygems' -require 'rake' -require 'rake/testtask' -require './lib/ruby-units' - -begin - require 'jeweler' - Jeweler::Tasks.new do |gem| - gem.name = "ruby-units" - gem.summary = %Q{A class that performs unit conversions and unit math} - gem.description = %Q{Provides classes and methods to perform unit math and conversions} - gem.authors = ["Kevin Olbrich, Ph.D."] - gem.email = ["kevin.olbrich+ruby_units@gmail.com"] - gem.homepage = "https://github.com/olbrich/ruby-units" - gem.files.exclude(".*","test/**/*","spec/**/*","autotest/**/*", "Gemfile") - gem.license = "MIT" - gem.post_install_message = <<-EOS -==================== -Deprecation Warning -==================== - -Several convenience methods that ruby-units added to the string class have -been deprecated in this release. These methods include String#to, String#from, String#ago, String#before and others. -If your code relies on these functions, they can be added back by adding this line to your code. - -require 'ruby-units/string/extras' -# note that these methods do not play well with Rails, which is one of the reasons they are being removed. - -The extra functions mostly work the same, but will no longer properly handle cases when they are called with strings.. - -'min'.from("4-1-2011") # => Exception - -Pass in a Date, Time, or DateTime object to get the expected result. - -They will go away completely in the next release, so it would be a good idea to refactor your code -to avoid using them. They will also throw deprecation warnings when they are used. -EOS - end - Jeweler::GemcutterTasks.new -rescue LoadError - puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler" -end - -begin - require 'simplecov' - desc "code coverage report using simplecov (ruby 1.9+)" - task :simplecov do - ENV['COVERAGE']='true' - Rake::Task['spec'].invoke - end -rescue LoadError -end - -begin - require 'rspec/core/rake_task' - - desc "Run specs" - RSpec::Core::RakeTask.new do |spec| - puts - # puts %x{rvm current} - puts - end - - desc "Run all specs with rcov" - RSpec::Core::RakeTask.new("spec:rcov") do |t| - t.rcov = true - t.rcov_opts = %w{--exclude osx\/objc,gems\/,spec\/,features\/} - end -rescue LoadError -end - -task :specs => :spec - -rubies = { - "ruby-1.8.7" => %w{ruby-1.8.7-p357@ruby-units ruby-1.8.7-p357@ruby-units-with-chronic}, - "ruby-1.9.2" => %w{ruby-1.9.2-p290@ruby-units ruby-1.9.2-p290@ruby-units-with-chronic}, - "rbx" => %w{rbx-head@ruby-units}, - "jruby" => %w{jruby-1.6.7@ruby-units} - } - -rubies.each do |name, ruby| - desc "Run Spec against #{name}" - task "spec:#{name}" do - sh "rvm #{ruby.join(',')} do rake spec" - end -end - -desc "Run specs against several ruby versions, requires rvm" -task "spec:all" => rubies.keys.map {|name| "spec:#{name}"} - -task :default => :spec diff --git a/TODO b/TODO deleted file mode 100644 index b8edfa86..00000000 --- a/TODO +++ /dev/null @@ -1,3 +0,0 @@ -2006-10-02 Currency handling remains to be implemented well -2011-04-23 Refactor caching -2011-11-23 Perform all internal calculations in rational numbers to avoid round off problems \ No newline at end of file diff --git a/VERSION b/VERSION deleted file mode 100644 index c9929e36..00000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.4.2 \ No newline at end of file diff --git a/autotest/discover.rb b/autotest/discover.rb deleted file mode 100644 index 24dd9b7b..00000000 --- a/autotest/discover.rb +++ /dev/null @@ -1 +0,0 @@ -Autotest.add_discovery { "rspec2" } \ No newline at end of file diff --git a/lib/ruby-units.rb b/lib/ruby-units.rb index b6e8b020..20794030 100755 --- a/lib/ruby-units.rb +++ b/lib/ruby-units.rb @@ -1,14 +1,3 @@ $LOAD_PATH << File.dirname(__FILE__) -require "ruby_units/version" -require "ruby_units/definition" -require "ruby_units/cache" -require 'ruby_units/array' -require 'ruby_units/date' -require 'ruby_units/time' -require 'ruby_units/math' -require 'ruby_units/numeric' -require 'ruby_units/object' -require 'ruby_units/string' -require 'ruby_units/unit' -require 'ruby_units/fixnum' -require 'ruby_units/unit_definitions' +require_relative 'ruby_units/namespaced' +Unit = RubyUnits::Unit diff --git a/lib/ruby_units.rb b/lib/ruby_units.rb deleted file mode 120000 index ef999766..00000000 --- a/lib/ruby_units.rb +++ /dev/null @@ -1 +0,0 @@ -ruby-units.rb \ No newline at end of file diff --git a/lib/ruby_units/array.rb b/lib/ruby_units/array.rb index a3a7ee6c..1f95a634 100644 --- a/lib/ruby_units/array.rb +++ b/lib/ruby_units/array.rb @@ -1,11 +1,9 @@ class Array # Construct a unit from an array - # @example [1, 'mm'].to_unit => Unit("1 mm") - # @return (see Unit#initialize) + # @example [1, 'mm'].to_unit => RubyUnits::Unit.new("1 mm") + # @return (see RubyUnits::Unit#initialize) # @param [Object] other convert to same units as passed def to_unit(other = nil) - other ? Unit.new(self).convert_to(other) : Unit.new(self) + other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) end - alias :unit :to_unit - alias :u :to_unit -end \ No newline at end of file +end diff --git a/lib/ruby_units/cache.rb b/lib/ruby_units/cache.rb index 6a65379c..669cdd02 100644 --- a/lib/ruby_units/cache.rb +++ b/lib/ruby_units/cache.rb @@ -1,20 +1,21 @@ -class Unit < Numeric - @@cached_units = {} - - class Cache - def self.get(key = nil) - key.nil? ? @@cached_units : @@cached_units[key] - end - - def self.set(key, value) - @@cached_units[key] = value - end +module RubyUnits + class Unit < Numeric + @@cached_units = {} + + class Cache + def self.get(key = nil) + key.nil? ? @@cached_units : @@cached_units[key] + end + + def self.set(key, value) + @@cached_units[key] = value + end - def self.clear - @@cached_units = {} - @@base_unit_cache = {} - Unit.new(1) + def self.clear + @@cached_units = {} + @@base_unit_cache = {} + Unit.new(1) + end end - end -end \ No newline at end of file +end diff --git a/lib/ruby_units/configuration.rb b/lib/ruby_units/configuration.rb new file mode 100644 index 00000000..43ec328d --- /dev/null +++ b/lib/ruby_units/configuration.rb @@ -0,0 +1,41 @@ +# allow for optional configuration of RubyUnits +# +# Usage: +# +# RubyUnits.configure do |config| +# config.separator = false +# end +module RubyUnits + class << self + attr_writer :configuration + end + + def self.configuration + @configuration ||= Configuration.new + end + + def self.reset + @configuration = Configuration.new + end + + def self.configure + yield configuration + end + + # holds actual configuration values for RubyUnits + class Configuration + # used to separate the scalar from the unit when generating output. + # set to nil to prevent adding a space to the string representation of a unit + # separators other than ' ' and '' may work, but you may encounter problems + attr_reader :separator + + def initialize + self.separator = true + end + + def separator=(value) + raise ArgumentError, "configuration 'separator' may only be true or false" unless value.class == TrueClass || value.class == FalseClass + @separator = value ? ' ' : nil + end + end +end diff --git a/lib/ruby_units/date.rb b/lib/ruby_units/date.rb index 70b75770..5a8d23d3 100644 --- a/lib/ruby_units/date.rb +++ b/lib/ruby_units/date.rb @@ -1,66 +1,64 @@ require 'date' # Allow date objects to do offsets by a time unit -# Date.today + U"1 week" => gives today+1 week +# Date.today + Unit.new("1 week") => gives today+1 week class Date - alias :unit_date_add :+ - # @param [Object] unit + alias unit_date_add + + # @param [Object] other # @return [Unit] - def +(unit) - case unit - when Unit - unit = unit.convert_to('d').round if ['y', 'decade', 'century'].include? unit.units - unit_date_add(unit.convert_to('day').scalar) + def +(other) + case other + when RubyUnits::Unit + other = other.convert_to('d').round if %w[y decade century].include? other.units + unit_date_add(other.convert_to('day').scalar) else - unit_date_add(unit) + unit_date_add(other) end end - - alias :unit_date_sub :- - # @param [Object] unit + + alias unit_date_sub - + # @param [Object] other # @return [Unit] - def -(unit) - case unit - when Unit - unit = unit.convert_to('d').round if ['y', 'decade', 'century'].include? unit.units - unit_date_sub(unit.convert_to('day').scalar) + def -(other) + case other + when RubyUnits::Unit + other = other.convert_to('d').round if %w[y decade century].include? other.units + unit_date_sub(other.convert_to('day').scalar) else - unit_date_sub(unit) + unit_date_sub(other) end end - + # Construct a unit from a Date # @example Date.today.to_unit => Unit # @return (see Unit#initialize) # @param [Object] other convert to same units as passed def to_unit(other = nil) - other ? Unit.new(self).convert_to(other) : Unit.new(self) + other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) end - alias :unit :to_unit - + # :nocov_19: unless Date.instance_methods.include?(:to_time) # @return [Time] def to_time - Time.local(*ParseDate.parsedate(self.to_s)) + Time.local(*ParseDate.parsedate(to_s)) end end # :nocov_19: - - alias :units_datetime_inspect :inspect + + alias units_datetime_inspect inspect # @deprecated - def inspect(raw = false) - return self.units_datetime_inspect if raw - self.to_s + def inspect(dump = false) + return units_datetime_inspect if dump + to_s end - + unless Date.instance_methods.include?(:to_date) # :nocov_19: # @return [Date] def to_date - Date.civil(self.year, self.month, self.day) + Date.civil(year, month, day) end # :nocov_19: end - -end \ No newline at end of file +end diff --git a/lib/ruby_units/definition.rb b/lib/ruby_units/definition.rb index 7fd0b406..8dd483e1 100644 --- a/lib/ruby_units/definition.rb +++ b/lib/ruby_units/definition.rb @@ -1,61 +1,59 @@ -class Unit < Numeric - +class RubyUnits::Unit < Numeric # Handle the definition of units class Definition - # @return [Array] attr_writer :aliases - + # @return [Symbol] attr_accessor :kind - + # @return [Numeric] attr_accessor :scalar - + # @return [Array] attr_accessor :numerator # @return [Array] attr_accessor :denominator - + # @return [String] attr_accessor :display_name - + # @example Raw definition from a hash # Unit::Definition.new("rack-unit",[%w{U rack-U}, (6405920109971793/144115188075855872), :length, %w{} ]) - # + # # @example Block form # Unit::Definition.new("rack-unit") do |unit| # unit.aliases = %w{U rack-U} - # unit.definition = Unit("7/4 inches") + # unit.definition = RubyUnits::Unit.new("7/4 inches") # end # - def initialize(_name, _definition = [], &block) + def initialize(name, definition = []) yield self if block_given? - self.name ||= _name.gsub(/[<>]/,'') - @aliases ||= (_definition[0] || [_name]) - @scalar ||= _definition[1] - @kind ||= _definition[2] - @numerator ||= _definition[3] || Unit::UNITY_ARRAY - @denominator ||= _definition[4] || Unit::UNITY_ARRAY + self.name ||= name.gsub(/[<>]/, '') + @aliases ||= (definition[0] || [name]) + @scalar ||= definition[1] + @kind ||= definition[2] + @numerator ||= definition[3] || RubyUnits::Unit::UNITY_ARRAY + @denominator ||= definition[4] || RubyUnits::Unit::UNITY_ARRAY @display_name ||= @aliases.first end - + # name of the unit # nil if name is not set, adds '<' and '>' around the name # @return [String, nil] # @todo refactor Unit and Unit::Definition so we don't need to wrap units with angle brackets def name - "<#{@name}>" if (defined?(@name) && @name) + "<#{@name}>" if defined?(@name) && @name end - + # set the name, strip off '<' and '>' # @param [String] # @return [String] - def name=(_name) - @name = _name.gsub(/[<>]/,'') + def name=(name_value) + @name = name_value.gsub(/[<>]/, '') end - + # alias array must contain the name of the unit and entries must be unique # @return [Array] def aliases @@ -66,35 +64,35 @@ def aliases # @param [Unit] unit # @return [Unit::Definition] def definition=(unit) - _base = unit.to_base - @scalar = _base.scalar - @kind = _base.kind - @numerator = _base.numerator - @denominator = _base.denominator + base = unit.to_base + @scalar = base.scalar + @kind = base.kind + @numerator = base.numerator + @denominator = base.denominator self end # is this definition for a prefix? # @return [Boolean] def prefix? - self.kind == :prefix + kind == :prefix end - + # Is this definition the unity definition? # @return [Boolean] def unity? - self.prefix? && self.scalar == 1 + prefix? && scalar == 1 end - + # is this a base unit? # units are base units if the scalar is one, and the unit is defined in terms of itself. # @return [Boolean] def base? - (self.denominator == Unit::UNITY_ARRAY) && - (self.numerator != Unit::UNITY_ARRAY) && - (self.numerator.size == 1) && - (self.scalar == 1) && - (self.numerator.first == self.name) + (denominator == RubyUnits::Unit::UNITY_ARRAY) && + (numerator != RubyUnits::Unit::UNITY_ARRAY) && + (numerator.size == 1) && + (scalar == 1) && + (numerator.first == self.name) end end -end \ No newline at end of file +end diff --git a/lib/ruby_units/fixnum.rb b/lib/ruby_units/fixnum.rb deleted file mode 100644 index 3101bca9..00000000 --- a/lib/ruby_units/fixnum.rb +++ /dev/null @@ -1,22 +0,0 @@ -if RUBY_VERSION < "1.9" - # :nocov_19: - class Fixnum - alias quo_without_units quo - - # @note this patch is necessary for ruby 1.8 because cases where Integers are divided by Units don't work quite right - # @param [Numeric] - # @return [Unit, Integer] - def quo_with_units(other) - case other - when Unit - self * other.inverse - else - quo_without_units(other) - end - end - - alias quo quo_with_units - alias / quo_with_units - end - # :nocov_19: -end \ No newline at end of file diff --git a/lib/ruby_units/math.rb b/lib/ruby_units/math.rb index 5b2bf1b5..baff5786 100644 --- a/lib/ruby_units/math.rb +++ b/lib/ruby_units/math.rb @@ -1,14 +1,11 @@ -require 'mathn' - -# Math will convert unit objects to radians and then attempt to use the value for -# trigonometric functions. +# Math will convert unit objects to radians and then attempt to use the value for +# trigonometric functions. module Math - - alias :unit_sqrt :sqrt + alias unit_sqrt sqrt # @return [Numeric] def sqrt(n) - if Unit === n - (n**(Rational(1,2))).to_unit + if n.is_a?(RubyUnits::Unit) + (n**Rational(1, 2)).to_unit else unit_sqrt(n) end @@ -19,12 +16,12 @@ def sqrt(n) module_function :sqrt #:nocov: - if self.respond_to?(:cbrt) - alias :unit_cbrt :cbrt + if respond_to?(:cbrt) + alias unit_cbrt cbrt # @return [Numeric] def cbrt(n) - if Unit === n - (n**(Rational(1,3))).to_unit + if RubyUnits::Unit === n + (n**Rational(1, 3)).to_unit else unit_cbrt(n) end @@ -35,97 +32,94 @@ def cbrt(n) module_function :cbrt end #:nocov: - - alias :unit_sin :sin + + alias unit_sin sin # @return [Numeric] def sin(n) - Unit === n ? unit_sin(n.convert_to('radian').scalar) : unit_sin(n) + RubyUnits::Unit === n ? unit_sin(n.convert_to('radian').scalar) : unit_sin(n) end # @return [Numeric] module_function :unit_sin # @return [Numeric] module_function :sin - alias :unit_cos :cos + alias unit_cos cos # @return [Numeric] def cos(n) - Unit === n ? unit_cos(n.convert_to('radian').scalar) : unit_cos(n) + RubyUnits::Unit === n ? unit_cos(n.convert_to('radian').scalar) : unit_cos(n) end # @return [Numeric] module_function :unit_cos # @return [Numeric] module_function :cos - - alias :unit_sinh :sinh + + alias unit_sinh sinh # @return [Numeric] def sinh(n) - Unit === n ? unit_sinh(n.convert_to('radian').scalar) : unit_sinh(n) + RubyUnits::Unit === n ? unit_sinh(n.convert_to('radian').scalar) : unit_sinh(n) end # @return [Numeric] module_function :unit_sinh # @return [Numeric] module_function :sinh - alias :unit_cosh :cosh + alias unit_cosh cosh # @return [Numeric] def cosh(n) - Unit === n ? unit_cosh(n.convert_to('radian').scalar) : unit_cosh(n) + RubyUnits::Unit === n ? unit_cosh(n.convert_to('radian').scalar) : unit_cosh(n) end # @return [Numeric] module_function :unit_cosh # @return [Numeric] module_function :cosh - alias :unit_tan :tan + alias unit_tan tan # @return [Numeric] def tan(n) - Unit === n ? unit_tan(n.convert_to('radian').scalar) : unit_tan(n) + RubyUnits::Unit === n ? unit_tan(n.convert_to('radian').scalar) : unit_tan(n) end # @return [Numeric] module_function :tan # @return [Numeric] module_function :unit_tan - alias :unit_tanh :tanh + alias unit_tanh tanh # @return [Numeric] def tanh(n) - Unit === n ? unit_tanh(n.convert_to('radian').scalar) : unit_tanh(n) + RubyUnits::Unit === n ? unit_tanh(n.convert_to('radian').scalar) : unit_tanh(n) end # @return [Numeric] module_function :unit_tanh # @return [Numeric] module_function :tanh - alias :unit_hypot :hypot + alias unit_hypot hypot # Convert parameters to consistent units and perform the function # @return [Numeric] - def hypot(x,y) - if Unit === x && Unit === y - (x**2 + y**2)**(1/2) + def hypot(x, y) + if RubyUnits::Unit === x && RubyUnits::Unit === y + (x**2 + y**2)**Rational(1, 2) else - unit_hypot(x,y) + unit_hypot(x, y) end end # @return [Numeric] module_function :unit_hypot # @return [Numeric] module_function :hypot - - alias :unit_atan2 :atan2 - # @return [Numeric] - def atan2(x,y) - case - when (x.is_a?(Unit) && y.is_a?(Unit)) && (x !~ y) - raise ArgumentError, "Incompatible Units" - when (x.is_a?(Unit) && y.is_a?(Unit)) && (x =~ y) - Math::unit_atan2(x.base_scalar, y.base_scalar) + + alias unit_atan2 atan2 + # @return [Numeric] + def atan2(x, y) + raise ArgumentError, 'Incompatible RubyUnits::Units' if (x.is_a?(RubyUnits::Unit) && y.is_a?(RubyUnits::Unit)) && !x.compatible?(y) + if (x.is_a?(RubyUnits::Unit) && y.is_a?(RubyUnits::Unit)) && x.compatible?(y) + Math.unit_atan2(x.base_scalar, y.base_scalar) else - Math::unit_atan2(x,y) + Math.unit_atan2(x, y) end end # @return [Numeric] module_function :unit_atan2 # @return [Numeric] module_function :atan2 - end diff --git a/lib/ruby_units/namespaced.rb b/lib/ruby_units/namespaced.rb new file mode 100644 index 00000000..0509d845 --- /dev/null +++ b/lib/ruby_units/namespaced.rb @@ -0,0 +1,16 @@ + +$LOAD_PATH << File.dirname(__FILE__) + +# require_relative this file to avoid creating an class alias from Unit to RubyUnits::Unit +require_relative 'version' +require_relative 'configuration' +require_relative 'definition' +require_relative 'cache' +require_relative 'array' +require_relative 'date' +require_relative 'time' +require_relative 'math' +require_relative 'numeric' +require_relative 'string' +require_relative 'unit' +require_relative 'unit_definitions' diff --git a/lib/ruby_units/numeric.rb b/lib/ruby_units/numeric.rb index 136787cd..1b0879ac 100644 --- a/lib/ruby_units/numeric.rb +++ b/lib/ruby_units/numeric.rb @@ -1,9 +1,7 @@ class Numeric # make a unitless unit with a given scalar - # @return (see Unit#initialize) + # @return (see RubyUnits::Unit#initialize) def to_unit(other = nil) - other ? Unit.new(self, other) : Unit.new(self) + other ? RubyUnits::Unit.new(self, other) : RubyUnits::Unit.new(self) end - alias :unit :to_unit - alias :u :to_unit -end \ No newline at end of file +end diff --git a/lib/ruby_units/object.rb b/lib/ruby_units/object.rb deleted file mode 100644 index c82d880a..00000000 --- a/lib/ruby_units/object.rb +++ /dev/null @@ -1,15 +0,0 @@ -class Object - - # Shortcut for creating Unit object - # @example - # Unit("1 mm") - # U("1 mm") - # @return [Unit] - def Unit(*other) - other.to_unit - end - alias :U :Unit - - # @deprecated - alias :u :Unit -end diff --git a/lib/ruby_units/string.rb b/lib/ruby_units/string.rb index fd820f34..24d0fd4f 100644 --- a/lib/ruby_units/string.rb +++ b/lib/ruby_units/string.rb @@ -1,35 +1,27 @@ require 'time' class String # make a string into a unit - # @return (see Unit#initialize) + # @return (see RubyUnits::Unit#initialize) def to_unit(other = nil) - other ? Unit.new(self).convert_to(other) : Unit.new(self) + other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) end - alias :unit :to_unit - alias :u :to_unit - - alias :unit_format :% - # format unit output using formating codes - # @example '%0.2f' % '1 mm'.unit => '1.00 mm' + alias original_format % + # format unit output using formating codes + # @example '%0.2f' % '1 mm'.to_unit => '1.00 mm' # @return [String] - def %(*args) - return "" if self.empty? - case - when args.first.is_a?(Unit) - args.first.to_s(self) - when (!defined?(Uncertain).nil? && args.first.is_a?(Uncertain)) - args.first.to_s(self) - when args.first.is_a?(Complex) - args.first.to_s + def format_with_unit(*other) + if other.first.is_a?(RubyUnits::Unit) + other.first.to_s(self) else - unit_format(*args) + original_format(*other) end end - - # @param (see Unit#convert_to) - # @return (see Unit#convert_to) + alias % format_with_unit + + # @param (see RubyUnits::Unit#convert_to) + # @return (see RubyUnits::Unit#convert_to) def convert_to(other) - self.unit.convert_to(other) + to_unit.convert_to(other) end end diff --git a/lib/ruby_units/time.rb b/lib/ruby_units/time.rb index 9ac59f5d..717f0c60 100644 --- a/lib/ruby_units/time.rb +++ b/lib/ruby_units/time.rb @@ -1,75 +1,79 @@ +require 'time' # # Time math is handled slightly differently. The difference is considered to be an exact duration if # the subtracted value is in hours, minutes, or seconds. It is rounded to the nearest day if the offset -# is in years, decades, or centuries. This leads to less precise values, but ones that match the +# is in years, decades, or centuries. This leads to less precise values, but ones that match the # calendar better. class Time - class << self alias unit_time_at at end - - # Convert a duration to a Time value by considering the duration to be the number of seconds since the + + # Convert a duration to a Time value by considering the duration to be the number of seconds since the # epoch # @param [Time] arg # @param [Integer] ms - # @return [Unit, Time] - def self.at(arg,ms=0) + # @return [RubyUnits::Unit, Time] + def self.at(arg, ms = nil) case arg - when Unit - unit_time_at(arg.convert_to("s").scalar, ms) + when Time + unit_time_at(arg) + when RubyUnits::Unit + if ms + unit_time_at(arg.convert_to('s').scalar, ms) + else + unit_time_at(arg.convert_to('s').scalar) + end else - unit_time_at(arg, ms) + ms.nil? ? unit_time_at(arg) : unit_time_at(arg, ms) end end - - # @return (see Unit#initialize) + + # @return (see RubyUnits::Unit#initialize) def to_unit(other = nil) - other ? Unit.new(self).convert_to(other) : Unit.new(self) + other ? RubyUnits::Unit.new(self).convert_to(other) : RubyUnits::Unit.new(self) end - alias :unit :to_unit - alias :u :to_unit unless Time.public_method_defined?(:to_date) # :nocov_19: public :to_date # :nocov_19: end - - alias :unit_add :+ - # @return [Unit, Time] + + alias unit_add + + # @return [RubyUnits::Unit, Time] def +(other) case other - when Unit - other = other.convert_to('d').round.convert_to('s') if ['y', 'decade', 'century'].include? other.units + when RubyUnits::Unit + other = other.convert_to('d').round.convert_to('s') if %w[y decade century].include? other.units begin unit_add(other.convert_to('s').scalar) rescue RangeError - self.to_datetime + other + to_datetime + other end else unit_add(other) end end - + # @example # Time.in '5 min' # @return (see Time#+) def self.in(duration) Time.now + duration.to_unit end - - alias :unit_sub :- - - # @return [Unit, Time] + + alias unit_sub - + + # @return [RubyUnits::Unit, Time] def -(other) case other - when Unit - other = other.convert_to('d').round.convert_to('s') if ['y', 'decade', 'century'].include? other.units + when RubyUnits::Unit + other = other.convert_to('d').round.convert_to('s') if %w[y decade century].include? other.units begin unit_sub(other.convert_to('s').scalar) rescue RangeError - self.send(:to_datetime) - other + send(:to_datetime) - other end else unit_sub(other) diff --git a/lib/ruby_units/unit.rb b/lib/ruby_units/unit.rb index 2bbac52e..307771a4 100644 --- a/lib/ruby_units/unit.rb +++ b/lib/ruby_units/unit.rb @@ -1,13 +1,5 @@ -# encoding: utf-8 require 'date' -if RUBY_VERSION < "1.9" - # :nocov_19: - require 'parsedate' - require 'rational' - # :nocov_19: -end -# Copyright 2006-2012 -# +# Copyright 2006-2015 # @author Kevin C. Olbrich, Ph.D. # @see https://github.com/olbrich/ruby-units # @@ -23,327 +15,493 @@ # # To add or override a unit definition, add a code block like this.. # @example Define a new unit -# Unit.define("foobar") do |unit| +# RubyUnits::Unit.define("foobar") do |unit| # unit.aliases = %w{foo fb foo-bar} -# unit.definition = Unit("1 baz") +# unit.definition = RubyUnits::Unit.new("1 baz") # end # -# @todo fix class variables so they conform to standard naming conventions and refactor away as many of them as possible -# @todo pull caching out into its own class -# @todo refactor internal representation of units -# @todo method to determine best natural prefix -class Unit < Numeric - VERSION = Unit::Version::STRING - @@definitions = {} - @@PREFIX_VALUES = {} - @@PREFIX_MAP = {} - @@UNIT_MAP = {} - @@UNIT_VALUES = {} - @@UNIT_REGEX = nil - @@UNIT_MATCH_REGEX = nil - UNITY = '<1>' - UNITY_ARRAY = [UNITY] - FEET_INCH_REGEX = /(\d+)\s*(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inches)/ - TIME_REGEX = /(\d+)*:(\d+)*:*(\d+)*[:,]*(\d+)*/ - LBS_OZ_REGEX = /(\d+)\s*(?:#|lbs|pounds|pound-mass)+[\s,]*(\d+)\s*(?:oz|ounces)/ - SCI_NUMBER = %r{([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)} - RATIONAL_NUMBER = /\(?([+-]?\d+)\/(\d+)\)?/ - COMPLEX_NUMBER = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/ - NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/ - UNIT_STRING_REGEX = /#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*/ - TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/ - BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/ - UNCERTAIN_REGEX = /#{SCI_NUMBER}\s*\+\/-\s*#{SCI_NUMBER}\s(.+)/ - COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/ - RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/ - KELVIN = [''] - FAHRENHEIT = [''] - RANKINE = [''] - CELSIUS = [''] - @@TEMP_REGEX = nil - SIGNATURE_VECTOR = [ - :length, - :time, - :temperature, - :mass, - :current, - :substance, - :luminosity, - :currency, - :memory, - :angle - ] - @@KINDS = { - -312078 => :elastance, - -312058 => :resistance, - -312038 => :inductance, - -152040 => :magnetism, - -152038 => :magnetism, - -152058 => :potential, - -7997 => :specific_volume, - -79 => :snap, - -59 => :jolt, - -39 => :acceleration, - -38 => :radiation, - -20 => :frequency, - -19 => :speed, - -18 => :viscosity, - -17 => :volumetric_flow, - -1 => :wavenumber, - 0 => :unitless, - 1 => :length, - 2 => :area, - 3 => :volume, - 20 => :time, - 400 => :temperature, - 7941 => :yank, - 7942 => :power, - 7959 => :pressure, - 7962 => :energy, - 7979 => :viscosity, - 7961 => :force, - 7981 => :momentum, - 7982 => :angular_momentum, - 7997 => :density, - 7998 => :area_density, - 8000 => :mass, - 152020 => :radiation_exposure, - 159999 => :magnetism, - 160000 => :current, - 160020 => :charge, - 312058 => :resistance, - 312078 => :capacitance, - 3199980 => :activity, - 3199997 => :molar_concentration, - 3200000 => :substance, - 63999998 => :illuminance, - 64000000 => :luminous_power, - 1280000000 => :currency, - 25600000000 => :memory, - 511999999980 => :angular_velocity, - 512000000000 => :angle - } - @@cached_units = {} - @@base_unit_cache = {} - - # setup internal arrays and hashes - # @return [true] - def self.setup - self.clear_cache - @@PREFIX_VALUES = {} - @@PREFIX_MAP = {} - @@UNIT_VALUES = {} - @@UNIT_MAP = {} - @@UNIT_REGEX = nil - @@UNIT_MATCH_REGEX = nil - @@PREFIX_REGEX = nil - - @@definitions.each do |name, definition| - self.use_definition(definition) - end - - Unit.new(1) - return true - end +module RubyUnits + class Unit < Numeric + @@definitions = {} + @@prefix_values = {} + @@prefix_map = {} + @@unit_map = {} + @@unit_values = {} + @@unit_regex = nil + @@unit_match_regex = nil + UNITY = '<1>'.freeze + UNITY_ARRAY = [UNITY].freeze + # ideally we would like to generate this regex from the alias for a 'feet' and 'inches', but they aren't + # defined at the point in the code where we need this regex. + FEET_INCH_UNITS_REGEX = /(?:'|ft|feet)\s*(\d+)\s*(?:"|in|inch(?:es)?)/ + FEET_INCH_REGEX = /(\d+)\s*#{FEET_INCH_UNITS_REGEX}/ + # ideally we would like to generate this regex from the alias for a 'pound' and 'ounce', but they aren't + # defined at the point in the code where we need this regex. + LBS_OZ_UNIT_REGEX = /(?:#|lbs?|pounds?|pound-mass)+[\s,]*(\d+)\s*(?:ozs?|ounces?)/ + LBS_OZ_REGEX = /(\d+)\s*#{LBS_OZ_UNIT_REGEX}/ + # ideally we would like to generate this regex from the alias for a 'stone' and 'pound', but they aren't + # defined at the point in the code where we need this regex. + # also note that the plural of 'stone' is still 'stone', but we accept 'stones' anyway. + STONE_LB_UNIT_REGEX = /(?:sts?|stones?)+[\s,]*(\d+)\s*(?:#|lbs?|pounds?|pound-mass)*/ + STONE_LB_REGEX = /(\d+)\s*#{STONE_LB_UNIT_REGEX}/ + TIME_REGEX = /(\d+)*:(\d+)*:*(\d+)*[:,]*(\d+)*/ + SCI_NUMBER = /([+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*)/ + RATIONAL_NUMBER = %r{\(?([+-])?(\d+[ -])?(\d+)\/(\d+)\)?} + COMPLEX_NUMBER = /#{SCI_NUMBER}?#{SCI_NUMBER}i\b/ + NUMBER_REGEX = /#{SCI_NUMBER}*\s*(.+)?/ + UNIT_STRING_REGEX = %r{#{SCI_NUMBER}*\s*([^\/]*)\/*(.+)*} + TOP_REGEX = /([^ \*]+)(?:\^|\*\*)([\d-]+)/ + BOTTOM_REGEX = /([^* ]+)(?:\^|\*\*)(\d+)/ + NUMBER_UNIT_REGEX = /#{SCI_NUMBER}?(.*)/ + COMPLEX_REGEX = /#{COMPLEX_NUMBER}\s?(.+)?/ + RATIONAL_REGEX = /#{RATIONAL_NUMBER}\s?(.+)?/ + KELVIN = [''].freeze + FAHRENHEIT = [''].freeze + RANKINE = [''].freeze + CELSIUS = [''].freeze + @@temp_regex = nil + SIGNATURE_VECTOR = %i[ + length + time + temperature + mass + current + substance + luminosity + currency + information + angle + ].freeze + @@kinds = { + -312_078 => :elastance, + -312_058 => :resistance, + -312_038 => :inductance, + -152_040 => :magnetism, + -152_038 => :magnetism, + -152_058 => :potential, + -7997 => :specific_volume, + -79 => :snap, + -59 => :jolt, + -39 => :acceleration, + -38 => :radiation, + -20 => :frequency, + -19 => :speed, + -18 => :viscosity, + -17 => :volumetric_flow, + -1 => :wavenumber, + 0 => :unitless, + 1 => :length, + 2 => :area, + 3 => :volume, + 20 => :time, + 400 => :temperature, + 7941 => :yank, + 7942 => :power, + 7959 => :pressure, + 7962 => :energy, + 7979 => :viscosity, + 7961 => :force, + 7981 => :momentum, + 7982 => :angular_momentum, + 7997 => :density, + 7998 => :area_density, + 8000 => :mass, + 152_020 => :radiation_exposure, + 159_999 => :magnetism, + 160_000 => :current, + 160_020 => :charge, + 312_058 => :conductance, + 312_078 => :capacitance, + 3_199_980 => :activity, + 3_199_997 => :molar_concentration, + 3_200_000 => :substance, + 63_999_998 => :illuminance, + 64_000_000 => :luminous_power, + 1_280_000_000 => :currency, + 25_600_000_000 => :information, + 511_999_999_980 => :angular_velocity, + 512_000_000_000 => :angle + }.freeze + @@cached_units = {} + @@base_unit_cache = {} + + # Class Methods + + # setup internal arrays and hashes + # @return [true] + def self.setup + clear_cache + @@prefix_values = {} + @@prefix_map = {} + @@unit_values = {} + @@unit_map = {} + @@unit_regex = nil + @@unit_match_regex = nil + @@prefix_regex = nil + + @@definitions.each do |_name, definition| + use_definition(definition) + end + RubyUnits::Unit.new(1) + true + end - # determine if a unit is already defined - # @param [String] unit - # @return [Boolean] - def self.defined?(unit) - self.definitions.values.any? {|d| d.aliases.include?(unit)} - end + # determine if a unit is already defined + # @param [String] unit + # @return [Boolean] + def self.defined?(unit) + definitions.values.any? { |d| d.aliases.include?(unit) } + end - # return the unit definition for a unit - # @param [String] unit - # @return [Unit::Definition, nil] - def self.definition(_unit) - unit = (_unit =~ /^<.+>$/) ? _unit : "<#{_unit}>" - return @@definitions[unit] - end + # return the unit definition for a unit + # @param [String] unit + # @return [RubyUnits::Unit::Definition, nil] + def self.definition(unit_name) + unit = unit_name =~ /^<.+>$/ ? unit_name : "<#{unit_name}>" + @@definitions[unit] + end - # return a list of all defined units - # @return [Array] - def self.definitions - return @@definitions - end + # return a list of all defined units + # @return [Array] + def self.definitions + @@definitions + end - # @param [Unit::Definition|String] unit_definition - # @param [Block] block - # @return [Unit::Definition] - # @raise [ArgumentError] when passed a non-string if using the block form - # Unpack a unit definition and add it to the array of defined units - # - # @example Block form - # Unit.define('foobar') do |foobar| - # foobar.definition = Unit("1 baz") - # end - # - # @example Unit::Definition form - # unit_definition = Unit::Definition.new("foobar") {|foobar| foobar.definition = Unit("1 baz")} - # Unit.define(unit_definition) - def self.define(unit_definition, &block) - if block_given? - raise ArgumentError, "When using the block form of Unit.define, pass the name of the unit" unless unit_definition.instance_of?(String) - unit_definition = Unit::Definition.new(unit_definition, &block) - end - Unit.definitions[unit_definition.name] = unit_definition - Unit.use_definition(unit_definition) - return unit_definition - end + # @param [RubyUnits::Unit::Definition|String] unit_definition + # @param [Block] block + # @return [RubyUnits::Unit::Definition] + # @raise [ArgumentError] when passed a non-string if using the block form + # Unpack a unit definition and add it to the array of defined units + # + # @example Block form + # RubyUnits::Unit.define('foobar') do |foobar| + # foobar.definition = RubyUnits::Unit.new("1 baz") + # end + # + # @example RubyUnits::Unit::Definition form + # unit_definition = RubyUnits::Unit::Definition.new("foobar") {|foobar| foobar.definition = RubyUnits::Unit.new("1 baz")} + # RubyUnits::Unit.define(unit_definition) + def self.define(unit_definition, &block) + if block_given? + raise ArgumentError, 'When using the block form of RubyUnits::Unit.define, pass the name of the unit' unless unit_definition.instance_of?(String) + unit_definition = RubyUnits::Unit::Definition.new(unit_definition, &block) + end + RubyUnits::Unit.definitions[unit_definition.name] = unit_definition + RubyUnits::Unit.use_definition(unit_definition) + unit_definition + end - # @param [String] name Name of unit to redefine - # @param [Block] block - # @raise [ArgumentError] if a block is not given - # @yield [Unit::Definition] - # @return (see Unit.define) - # Get the definition for a unit and allow it to be redefined - def self.redefine!(name, &block) - raise ArgumentError, "A block is required to redefine a unit" unless block_given? - unit_definition = self.definition(name) - yield unit_definition - self.define(unit_definition) - end + # @param [String] name Name of unit to redefine + # @param [Block] block + # @raise [ArgumentError] if a block is not given + # @yield [RubyUnits::Unit::Definition] + # @return (see RubyUnits::Unit.define) + # Get the definition for a unit and allow it to be redefined + def self.redefine!(name) + raise ArgumentError, 'A block is required to redefine a unit' unless block_given? + unit_definition = definition(name) + raise(ArgumentError, "'#{name}' Unit not recognized") unless unit_definition + yield unit_definition + @@definitions.delete("<#{name}>") + define(unit_definition) + RubyUnits::Unit.setup + end - # @param [String] name of unit to undefine - # @return (see Unit.setup) - # Undefine a unit. Will not raise an exception for unknown units. - def self.undefine!(unit) - @@definitions.delete("<#{unit}>") - Unit.setup - end + # @param [String] name of unit to undefine + # @return (see RubyUnits::Unit.setup) + # Undefine a unit. Will not raise an exception for unknown units. + def self.undefine!(unit) + @@definitions.delete("<#{unit}>") + RubyUnits::Unit.setup + end - include Comparable + # @return [Hash] + def self.cached + @@cached_units + end - # @return [Numeric] - attr_accessor :scalar + # @return [true] + def self.clear_cache + @@cached_units = {} + @@base_unit_cache = {} + RubyUnits::Unit.new(1) + true + end - # @return [Array] - attr_accessor :numerator + # @return [Hash] + def self.base_unit_cache + @@base_unit_cache + end - # @return [Array] - attr_accessor :denominator + # @example parse strings + # "1 minute in seconds" + # @param [String] input + # @return [Unit] + def self.parse(input) + first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first + second.nil? ? RubyUnits::Unit.new(first) : RubyUnits::Unit.new(first).convert_to(second) + end - # @return [Integer] - attr_accessor :signature + # @param [Numeric] q quantity + # @param [Array] n numerator + # @param [Array] d denominator + # @return [Hash] + def self.eliminate_terms(q, n, d) + num = n.dup + den = d.dup + + num.delete_if { |v| v == UNITY } + den.delete_if { |v| v == UNITY } + combined = Hash.new(0) + + i = 0 + loop do + break if i > num.size + if @@prefix_values.key? num[i] + k = [num[i], num[i + 1]] + i += 2 + else + k = num[i] + i += 1 + end + combined[k] += 1 unless k.nil? || k == UNITY + end - # @return [Numeric] - attr_accessor :base_scalar + j = 0 + loop do + break if j > den.size + if @@prefix_values.key? den[j] + k = [den[j], den[j + 1]] + j += 2 + else + k = den[j] + j += 1 + end + combined[k] -= 1 unless k.nil? || k == UNITY + end - # @return [Array] - attr_accessor :base_numerator + num = [] + den = [] + combined.each do |key, value| + if value >= 0 + value.times { num << key } + elsif value < 0 + value.abs.times { den << key } + end + end + num = UNITY_ARRAY if num.empty? + den = UNITY_ARRAY if den.empty? + { scalar: q, numerator: num.flatten.compact, denominator: den.flatten.compact } + end - # @return [Array] - attr_accessor :base_denominator + # return an array of base units + # @return [Array] + def self.base_units + @@base_units ||= @@definitions.dup.delete_if { |_, defn| !defn.base? }.keys.map { |u| RubyUnits::Unit.new(u) } + end - # @return [String] - attr_accessor :output + # parse a string consisting of a number and a unit string + # NOTE: This does not properly handle units formatted like '12mg/6ml' + # @param [String] string + # @return [Array] consisting of [Numeric, "unit"] + def self.parse_into_numbers_and_units(string) + # scientific notation.... 123.234E22, -123.456e-10 + sci = /[+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*/ + # rational numbers.... -1/3, 1/5, 20/100, -6 1/2, -6-1/2 + rational = %r{\(?[+-]?(?:\d+[ -])?\d+\/\d+\)?} + # complex numbers... -1.2+3i, +1.2-3.3i + complex = /#{sci}{2,2}i/ + anynumber = /(?:(#{complex}|#{rational}|#{sci}))?\s?([^-\d\.].*)?/ + + num, unit = string.scan(anynumber).first + + [ + case num + when NilClass + 1 + when complex + if num.respond_to?(:to_c) + num.to_c + else + #:nocov_19: + Complex(*num.scan(/(#{sci})(#{sci})i/).flatten.map(&:to_i)) + #:nocov_19: + end + when rational + # if it has whitespace, it will be of the form '6 1/2' + if num =~ RATIONAL_NUMBER + sign = Regexp.last_match(1) == '-' ? -1 : 1 + n = Regexp.last_match(2).to_i + f = Rational(Regexp.last_match(3).to_i, Regexp.last_match(4).to_i) + sign * (n + f) + else + Rational(*num.split('/').map(&:to_i)) + end + else + num.to_f + end, + unit.to_s.strip + ] + end - # @return [String] - attr_accessor :unit_name + # return a fragment of a regex to be used for matching units or reconstruct it if hasn't been used yet. + # Unit names are reverse sorted by length so the regexp matcher will prefer longer and more specific names + # @return [String] + def self.unit_regex + @@unit_regex ||= @@unit_map.keys.sort_by { |unit_name| [unit_name.length, unit_name] }.reverse.join('|') + end - # needed to make complex units play nice -- otherwise not detected as a complex_generic - # @param [Class] - # @return [Boolean] - def kind_of?(klass) - self.scalar.kind_of?(klass) - end + # return a regex used to match units + # @return [RegExp] + def self.unit_match_regex + @@unit_match_regex ||= /(#{RubyUnits::Unit.prefix_regex})??(#{RubyUnits::Unit.unit_regex})\b/ + end - # Used to copy one unit to another - # @param [Unit] from Unit to copy definition from - # @return [Unit] - def copy(from) - @scalar = from.scalar - @numerator = from.numerator - @denominator = from.denominator - @is_base = from.is_base? - @signature = from.signature - @base_scalar = from.base_scalar - @unit_name = from.unit_name rescue nil - return self - end + # return a regexp fragment used to match prefixes + # @return [String] + # @private + def self.prefix_regex + @@prefix_regex ||= @@prefix_map.keys.sort_by { |prefix| [prefix.length, prefix] }.reverse.join('|') + end + + def self.temp_regex + @@temp_regex ||= begin + temp_units = %w[tempK tempC tempF tempR degK degC degF degR] + aliases = temp_units.map do |unit| + d = RubyUnits::Unit.definition(unit) + d && d.aliases + end.flatten.compact + regex_str = aliases.empty? ? '(?!x)x' : aliases.join('|') + Regexp.new "(?:#{regex_str})" + end + end + + # inject a definition into the internal array and set it up for use + def self.use_definition(definition) + @@unit_match_regex = nil # invalidate the unit match regex + @@temp_regex = nil # invalidate the temp regex + if definition.prefix? + @@prefix_values[definition.name] = definition.scalar + definition.aliases.each { |alias_name| @@prefix_map[alias_name] = definition.name } + @@prefix_regex = nil # invalidate the prefix regex + else + @@unit_values[definition.name] = {} + @@unit_values[definition.name][:scalar] = definition.scalar + @@unit_values[definition.name][:numerator] = definition.numerator if definition.numerator + @@unit_values[definition.name][:denominator] = definition.denominator if definition.denominator + definition.aliases.each { |alias_name| @@unit_map[alias_name] = definition.name } + @@unit_regex = nil # invalidate the unit regex + end + end + + include Comparable - if RUBY_VERSION < "1.9" - # :nocov_19: + # @return [Numeric] + attr_accessor :scalar - # a list of properties to emit to yaml # @return [Array] - def to_yaml_properties - %w{@scalar @numerator @denominator @signature @base_scalar} - end + attr_accessor :numerator + + # @return [Array] + attr_accessor :denominator + + # @return [Integer] + attr_accessor :signature + + # @return [Numeric] + attr_accessor :base_scalar + + # @return [Array] + attr_accessor :base_numerator + + # @return [Array] + attr_accessor :base_denominator - # basically a copy of the basic to_yaml. Needed because otherwise it ends up coercing the object to a string - # before YAML'izing it. - # @param [Hash] opts # @return [String] - def to_yaml( opts = {} ) - YAML::quick_emit( object_id, opts ) do |out| - out.map( taguri, to_yaml_style ) do |map| - for m in to_yaml_properties do - map.add( m[1..-1], instance_variable_get( m ) ) - end - end - end + attr_accessor :output + + # @return [String] + attr_accessor :unit_name + + # Used to copy one unit to another + # @param [Unit] from Unit to copy definition from + # @return [Unit] + def copy(from) + @scalar = from.scalar + @numerator = from.numerator + @denominator = from.denominator + @base = from.base? + @signature = from.signature + @base_scalar = from.base_scalar + @unit_name = begin + from.unit_name + rescue + nil + end + self end - # :nocov_19: - end - # Create a new Unit object. Can be initialized using a String, a Hash, an Array, Time, DateTime - # - # @example Valid options include: - # "5.6 kg*m/s^2" - # "5.6 kg*m*s^-2" - # "5.6 kilogram*meter*second^-2" - # "2.2 kPa" - # "37 degC" - # "1" -- creates a unitless constant with value 1 - # "GPa" -- creates a unit with scalar 1 with units 'GPa' - # "6'4\""" -- recognized as 6 feet + 4 inches - # "8 lbs 8 oz" -- recognized as 8 lbs + 8 ounces - # [1, 'kg'] - # {:scalar => 1, :numerator=>'kg'} - # - # @param [Unit,String,Hash,Array,Date,Time,DateTime] options - # @return [Unit] - # @raise [ArgumentError] if absolute value of a temperature is less than absolute zero - # @raise [ArgumentError] if no unit is specified - # @raise [ArgumentError] if an invalid unit is specified - def initialize(*options) - @scalar = nil - @base_scalar = nil - @unit_name = nil - @signature = nil - @output = { } - raise ArgumentError, "Invalid Unit Format" if options[0].nil? - if options.size == 2 - # options[0] is the scalar - # options[1] is a unit string - begin - cached = @@cached_units[options[1]] * options[0] - copy(cached) - rescue - initialize("#{options[0]} #{(options[1].units rescue options[1])}") + # Create a new Unit object. Can be initialized using a String, a Hash, an Array, Time, DateTime + # + # @example Valid options include: + # "5.6 kg*m/s^2" + # "5.6 kg*m*s^-2" + # "5.6 kilogram*meter*second^-2" + # "2.2 kPa" + # "37 degC" + # "1" -- creates a unitless constant with value 1 + # "GPa" -- creates a unit with scalar 1 with units 'GPa' + # "6'4\""" -- recognized as 6 feet + 4 inches + # "8 lbs 8 oz" -- recognized as 8 lbs + 8 ounces + # [1, 'kg'] + # {scalar: 1, numerator: 'kg'} + # + # @param [Unit,String,Hash,Array,Date,Time,DateTime] options + # @return [Unit] + # @raise [ArgumentError] if absolute value of a temperature is less than absolute zero + # @raise [ArgumentError] if no unit is specified + # @raise [ArgumentError] if an invalid unit is specified + def initialize(*options) + @scalar = nil + @base_scalar = nil + @unit_name = nil + @signature = nil + @output = {} + raise ArgumentError, 'Invalid Unit Format' if options[0].nil? + if options.size == 2 + # options[0] is the scalar + # options[1] is a unit string + begin + cached = @@cached_units[options[1]] * options[0] + copy(cached) + rescue + initialize("#{options[0]} #{(begin + options[1].units + rescue + options[1] + end)}") + end + return end - return - end - if options.size == 3 - options[1] = options[1].join if options[1].kind_of?(Array) - options[2] = options[2].join if options[2].kind_of?(Array) - begin - cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0] - copy(cached) - rescue - initialize("#{options[0]} #{options[1]}/#{options[2]}") + if options.size == 3 + options[1] = options[1].join if options[1].is_a?(Array) + options[2] = options[2].join if options[2].is_a?(Array) + begin + cached = @@cached_units["#{options[1]}/#{options[2]}"] * options[0] + copy(cached) + rescue + initialize("#{options[0]} #{options[1]}/#{options[2]}") + end + return end - return - end - case options[0] + case options[0] when Unit copy(options[0]) return when Hash - @scalar = options[0][:scalar] || 1 + @scalar = (options[0][:scalar] || 1) @numerator = options[0][:numerator] || UNITY_ARRAY @denominator = options[0][:denominator] || UNITY_ARRAY @signature = options[0][:signature] @@ -362,1230 +520,1118 @@ def initialize(*options) @numerator = [''] @denominator = UNITY_ARRAY when /^\s*$/ - raise ArgumentError, "No Unit Specified" + raise ArgumentError, 'No Unit Specified' when String parse(options[0]) else - raise ArgumentError, "Invalid Unit Format" - end - self.update_base_scalar - raise ArgumentError, "Temperatures must not be less than absolute zero" if self.is_temperature? && self.base_scalar < 0 - unary_unit = self.units || "" - if options.first.instance_of?(String) - opt_scalar, opt_units = Unit.parse_into_numbers_and_units(options[0]) - unless @@cached_units.keys.include?(opt_units) || (opt_units =~ /(#{Unit.temp_regex})|(pounds|lbs[ ,]\d+ ounces|oz)|('\d+")|(ft|feet[ ,]\d+ in|inch|inches)|%|(#{TIME_REGEX})|i\s?(.+)?|±|\+\/-/) - @@cached_units[opt_units] = (self.scalar == 1 ? self : opt_units.unit) if opt_units && !opt_units.empty? + raise ArgumentError, 'Invalid Unit Format' end + update_base_scalar + raise ArgumentError, 'Temperatures must not be less than absolute zero' if temperature? && base_scalar < 0 + unary_unit = units || '' + if options.first.instance_of?(String) + _opt_scalar, opt_units = RubyUnits::Unit.parse_into_numbers_and_units(options[0]) + unless @@cached_units.keys.include?(opt_units) || + (opt_units =~ %r{\D/[\d+\.]+}) || + (opt_units =~ %r{(#{RubyUnits::Unit.temp_regex})|(#{STONE_LB_UNIT_REGEX})|(#{LBS_OZ_UNIT_REGEX})|(#{FEET_INCH_UNITS_REGEX})|%|(#{TIME_REGEX})|i\s?(.+)?|±|\+\/-}) + @@cached_units[opt_units] = (scalar == 1 ? self : opt_units.to_unit) if opt_units && !opt_units.empty? + end + end + unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{RubyUnits::Unit.temp_regex}/) + @@cached_units[unary_unit] = (scalar == 1 ? self : unary_unit.to_unit) + end + [@scalar, @numerator, @denominator, @base_scalar, @signature, @base].each(&:freeze) + self end - unless @@cached_units.keys.include?(unary_unit) || (unary_unit =~ /#{Unit.temp_regex}/) then - @@cached_units[unary_unit] = (self.scalar == 1 ? self : unary_unit.unit) - end - [@scalar, @numerator, @denominator, @base_scalar, @signature, @is_base].each {|x| x.freeze} - return self - end - - # @todo: figure out how to handle :counting units. This method should probably return :counting instead of :unitless for 'each' - # return the kind of the unit (:mass, :length, etc...) - # @return [Symbol] - def kind - return @@KINDS[self.signature] - end - - # @private - # @return [Hash] - def self.cached - return @@cached_units - end - - # @private - # @return [true] - def self.clear_cache - @@cached_units = {} - @@base_unit_cache = {} - Unit.new(1) - return true - end - # @private - # @return [Hash] - def self.base_unit_cache - return @@base_unit_cache - end + # @todo: figure out how to handle :counting units. This method should probably return :counting instead of :unitless for 'each' + # return the kind of the unit (:mass, :length, etc...) + # @return [Symbol] + def kind + @@kinds[signature] + end - # @example parse strings - # "1 minute in seconds" - # @param [String] input - # @return [Unit] - def self.parse(input) - first, second = input.scan(/(.+)\s(?:in|to|as)\s(.+)/i).first - return second.nil? ? first.unit : first.unit.convert_to(second) - end + # @return [Unit] + def to_unit + self + end - # @return [Unit] - def to_unit - self - end - alias :unit :to_unit - - # Is this unit in base form? - # @return [Boolean] - def is_base? - return @is_base if defined? @is_base - @is_base = (@numerator + @denominator).compact.uniq. - map {|unit| Unit.definition(unit)}. - all? {|element| element.unity? || element.base? } - return @is_base - end - alias :base? :is_base? - - # convert to base SI units - # results of the conversion are cached so subsequent calls to this will be fast - # @return [Unit] - # @todo this is brittle as it depends on the display_name of a unit, which can be changed - def to_base - return self if self.is_base? - if @@UNIT_MAP[self.units] =~ /\A<(?:temp|deg)[CRF]>\Z/ - if RUBY_VERSION < "1.9" - # :nocov_19: - @signature = @@KINDS.index(:temperature) - # :nocov_19: - else - #:nocov: - @signature = @@KINDS.key(:temperature) - #:nocov: - end - base = case - when self.is_temperature? - self.convert_to('tempK') - when self.is_degree? - self.convert_to('degK') - end - return base + alias unit to_unit + + # Is this unit in base form? + # @return [Boolean] + def base? + return @base if defined? @base + @base = (@numerator + @denominator) + .compact + .uniq + .map { |unit| RubyUnits::Unit.definition(unit) } + .all? { |element| element.unity? || element.base? } + @base end - cached = ((@@base_unit_cache[self.units] * self.scalar) rescue nil) - return cached if cached + alias is_base? base? + + # convert to base SI units + # results of the conversion are cached so subsequent calls to this will be fast + # @return [Unit] + # @todo this is brittle as it depends on the display_name of a unit, which can be changed + def to_base + return self if base? + if @@unit_map[units] =~ /\A<(?:temp|deg)[CRF]>\Z/ + @signature = @@kinds.key(:temperature) + base = if temperature? + convert_to('tempK') + elsif degree? + convert_to('degK') + end + return base + end - num = [] - den = [] - q = 1 - for unit in @numerator.compact do - if @@PREFIX_VALUES[unit] - q *= @@PREFIX_VALUES[unit] - else - q *= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit] - num << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator] - den << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator] + cached = (begin + (@@base_unit_cache[units] * scalar) + rescue + nil + end) + return cached if cached + + num = [] + den = [] + q = Rational(1) + @numerator.compact.each do |num_unit| + if @@prefix_values[num_unit] + q *= @@prefix_values[num_unit] + else + q *= @@unit_values[num_unit][:scalar] if @@unit_values[num_unit] + num << @@unit_values[num_unit][:numerator] if @@unit_values[num_unit] && @@unit_values[num_unit][:numerator] + den << @@unit_values[num_unit][:denominator] if @@unit_values[num_unit] && @@unit_values[num_unit][:denominator] + end end - end - for unit in @denominator.compact do - if @@PREFIX_VALUES[unit] - q /= @@PREFIX_VALUES[unit] - else - q /= @@UNIT_VALUES[unit][:scalar] if @@UNIT_VALUES[unit] - den << @@UNIT_VALUES[unit][:numerator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:numerator] - num << @@UNIT_VALUES[unit][:denominator] if @@UNIT_VALUES[unit] && @@UNIT_VALUES[unit][:denominator] + @denominator.compact.each do |num_unit| + if @@prefix_values[num_unit] + q /= @@prefix_values[num_unit] + else + q /= @@unit_values[num_unit][:scalar] if @@unit_values[num_unit] + den << @@unit_values[num_unit][:numerator] if @@unit_values[num_unit] && @@unit_values[num_unit][:numerator] + num << @@unit_values[num_unit][:denominator] if @@unit_values[num_unit] && @@unit_values[num_unit][:denominator] + end end + + num = num.flatten.compact + den = den.flatten.compact + num = UNITY_ARRAY if num.empty? + base = RubyUnits::Unit.new(RubyUnits::Unit.eliminate_terms(q, num, den)) + @@base_unit_cache[units] = base + base * @scalar end - num = num.flatten.compact - den = den.flatten.compact - num = UNITY_ARRAY if num.empty? - base= Unit.new(Unit.eliminate_terms(q,num,den)) - @@base_unit_cache[self.units]=base - return base * @scalar - end - alias :base :to_base - - # Generate human readable output. - # If the name of a unit is passed, the unit will first be converted to the target unit before output. - # some named conversions are available - # - # @example - # unit.to_s(:ft) - outputs in feet and inches (e.g., 6'4") - # unit.to_s(:lbs) - outputs in pounds and ounces (e.g, 8 lbs, 8 oz) - # - # You can also pass a standard format string (i.e., '%0.2f') - # or a strftime format string. - # - # output is cached so subsequent calls for the same format will be fast - # - # @param [Symbol] target_units - # @return [String] - def to_s(target_units=nil) - out = @output[target_units] - if out - return out - else + alias base to_base + + # Generate human readable output. + # If the name of a unit is passed, the unit will first be converted to the target unit before output. + # some named conversions are available + # + # @example + # unit.to_s(:ft) - outputs in feet and inches (e.g., 6'4") + # unit.to_s(:lbs) - outputs in pounds and ounces (e.g, 8 lbs, 8 oz) + # + # You can also pass a standard format string (i.e., '%0.2f') + # or a strftime format string. + # + # output is cached so subsequent calls for the same format will be fast + # + # @note Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..) + # @param [Symbol] target_units + # @return [String] + def to_s(target_units = nil) + out = @output[target_units] + return out if out + separator = RubyUnits.configuration.separator case target_units when :ft - inches = self.convert_to("in").scalar.to_int - out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" + inches = convert_to('in').scalar.to_int + out = "#{(inches / 12).truncate}\'#{(inches % 12).round}\"" when :lbs - ounces = self.convert_to("oz").scalar.to_int - out = "#{(ounces / 16).truncate} lbs, #{(ounces % 16).round} oz" + ounces = convert_to('oz').scalar.to_int + out = "#{(ounces / 16).truncate}#{separator}lbs, #{(ounces % 16).round}#{separator}oz" + when :stone + pounds = convert_to('lbs').scalar.to_int + out = "#{(pounds / 14).truncate}#{separator}stone, #{(pounds % 14).round}#{separator}lb" when String - out = case target_units - when /(%[\-+\.\w#]+)\s*(.+)*/ #format string like '%0.2f in' - begin - if $2 #unit specified, need to convert - self.convert_to($2).to_s($1) - else - "#{$1 % @scalar} #{$2 || self.units}".strip - end - rescue # parse it like a strftime format string - (DateTime.new(0) + self).strftime(target_units) - end - when /(\S+)/ #unit only 'mm' or '1/mm' - self.convert_to($1).to_s - else - raise "unhandled case" - end + out = case target_units.strip + when /\A\s*\Z/ # whitespace only + '' + when /(%[\-+\.\w#]+)\s*(.+)*/ # format string like '%0.2f in' + begin + if Regexp.last_match(2) # unit specified, need to convert + convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1)) + else + "#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units}".strip + end + rescue # parse it like a strftime format string + (DateTime.new(0) + self).strftime(target_units) + end + when /(\S+)/ # unit only 'mm' or '1/mm' + convert_to(Regexp.last_match(1)).to_s + else + raise 'unhandled case' + end else out = case @scalar - when Rational - "#{@scalar} #{self.units}" - else - "#{'%g' % @scalar} #{self.units}" - end.strip + when Complex + "#{@scalar}#{separator}#{units}" + when Rational + "#{@scalar == @scalar.to_i ? @scalar.to_i : @scalar}#{separator}#{units}" + else + "#{'%g' % @scalar}#{separator}#{units}" + end.strip end @output[target_units] = out - return out + out end - end - # Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump' - # @deprecated - # @return [String] - def inspect(option=nil) - return super() if option == :dump - return self.to_s - end + # Normally pretty prints the unit, but if you really want to see the guts of it, pass ':dump' + # @deprecated + # @return [String] + def inspect(dump = nil) + return super() if dump + to_s + end - # true if unit is a 'temperature', false if a 'degree' or anything else - # @return [Boolean] - # @todo use unit definition to determine if it's a temperature instead of a regex - def is_temperature? - return self.is_degree? && (!(@@UNIT_MAP[self.units] =~ /temp[CFRK]/).nil?) - end - alias :temperature? :is_temperature? + # true if unit is a 'temperature', false if a 'degree' or anything else + # @return [Boolean] + # @todo use unit definition to determine if it's a temperature instead of a regex + def temperature? + degree? && !(@@unit_map[units] =~ /temp[CFRK]/).nil? + end - # true if a degree unit or equivalent. - # @return [Boolean] - def is_degree? - return self.kind == :temperature - end - alias :degree? :is_degree? - - # returns the 'degree' unit associated with a temperature unit - # @example '100 tempC'.unit.temperature_scale #=> 'degC' - # @return [String] possible values: degC, degF, degR, or degK - def temperature_scale - return nil unless self.is_temperature? - return "deg#{@@UNIT_MAP[self.units][/temp([CFRK])/,1]}" - end + alias is_temperature? temperature? - # returns true if no associated units - # false, even if the units are "unitless" like 'radians, each, etc' - # @return [Boolean] - def unitless? - return(@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY) - end + # true if a degree unit or equivalent. + # @return [Boolean] + def degree? + kind == :temperature + end + + alias is_degree? degree? - # Compare two Unit objects. Throws an exception if they are not of compatible types. - # Comparisons are done based on the value of the unit in base SI units. - # @param [Object] other - # @return [-1|0|1|nil] - # @raise [NoMethodError] when other does not define <=> - # @raise [ArgumentError] when units are not compatible - def <=>(other) - case - when !self.base_scalar.respond_to?(:<=>) - raise NoMethodError, "undefined method `<=>' for #{self.base_scalar.inspect}" - when other.nil? - return self.base_scalar <=> nil - when !self.is_temperature? && other.zero? - return self.base_scalar <=> 0 - when other.instance_of?(Unit) - raise ArgumentError, "Incompatible Units (#{self.units} !~ #{other.units})" unless self =~ other - return self.base_scalar <=> other.base_scalar - else - x,y = coerce(other) - return x <=> y + # returns the 'degree' unit associated with a temperature unit + # @example '100 tempC'.to_unit.temperature_scale #=> 'degC' + # @return [String] possible values: degC, degF, degR, or degK + def temperature_scale + return nil unless temperature? + "deg#{@@unit_map[units][/temp([CFRK])/, 1]}" + end + + # returns true if no associated units + # false, even if the units are "unitless" like 'radians, each, etc' + # @return [Boolean] + def unitless? + (@numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY) end - end - # Compare Units for equality - # this is necessary mostly for Complex units. Complex units do not have a <=> operator - # so we define this one here so that we can properly check complex units for equality. - # Units of incompatible types are not equal, except when they are both zero and neither is a temperature - # Equality checks can be tricky since round off errors may make essentially equivalent units - # appear to be different. - # @param [Object] other - # @return [Boolean] - def ==(other) - case - when other.respond_to?(:zero?) && other.zero? - return self.zero? - when other.instance_of?(Unit) - return false unless self =~ other - return self.base_scalar == other.base_scalar - else - begin - x,y = coerce(other) - return x == y - rescue ArgumentError # return false when object cannot be coerced - return false + # Compare two Unit objects. Throws an exception if they are not of compatible types. + # Comparisons are done based on the value of the unit in base SI units. + # @param [Object] other + # @return [-1|0|1|nil] + # @raise [NoMethodError] when other does not define <=> + # @raise [ArgumentError] when units are not compatible + def <=>(other) + raise NoMethodError, "undefined method `<=>' for #{base_scalar.inspect}" unless base_scalar.respond_to?(:<=>) + if other.nil? + base_scalar <=> nil + elsif !temperature? && other.respond_to?(:zero?) && other.zero? + base_scalar <=> 0 + elsif other.instance_of?(Unit) + raise ArgumentError, "Incompatible Units ('#{units}' not compatible with '#{other.units}')" unless self =~ other + base_scalar <=> other.base_scalar + else + x, y = coerce(other) + y <=> x end end - end - # check to see if units are compatible, but not the scalar part - # this check is done by comparing signatures for performance reasons - # if passed a string, it will create a unit object with the string and then do the comparison - # @example this permits a syntax like: - # unit =~ "mm" - # @note if you want to do a regexp comparison of the unit string do this ... - # unit.units =~ /regexp/ - # @param [Object] other - # @return [Boolean] - def =~(other) - case other - when Unit - self.signature == other.signature - else - begin - x,y = coerce(other) - return x =~ y - rescue ArgumentError - return false + # Compare Units for equality + # this is necessary mostly for Complex units. Complex units do not have a <=> operator + # so we define this one here so that we can properly check complex units for equality. + # Units of incompatible types are not equal, except when they are both zero and neither is a temperature + # Equality checks can be tricky since round off errors may make essentially equivalent units + # appear to be different. + # @param [Object] other + # @return [Boolean] + def ==(other) + if other.respond_to?(:zero?) && other.zero? + zero? + elsif other.instance_of?(Unit) + return false unless self =~ other + base_scalar == other.base_scalar + else + begin + x, y = coerce(other) + x == y + rescue ArgumentError # return false when object cannot be coerced + false + end end end - end - alias :compatible? :=~ - alias :compatible_with? :=~ - - # Compare two units. Returns true if quantities and units match - # @example - # Unit("100 cm") === Unit("100 cm") # => true - # Unit("100 cm") === Unit("1 m") # => false - # @param [Object] other - # @return [Boolean] - def ===(other) - case other - when Unit - (self.scalar == other.scalar) && (self.units == other.units) - else - begin - x,y = coerce(other) - return x === y - rescue ArgumentError - return false + # check to see if units are compatible, but not the scalar part + # this check is done by comparing signatures for performance reasons + # if passed a string, it will create a unit object with the string and then do the comparison + # @example this permits a syntax like: + # unit =~ "mm" + # @note if you want to do a regexp comparison of the unit string do this ... + # unit.units =~ /regexp/ + # @param [Object] other + # @return [Boolean] + def =~(other) + case other + when Unit + signature == other.signature + else + begin + x, y = coerce(other) + return x =~ y + rescue ArgumentError + return false + end + end + end + + alias compatible? =~ + alias compatible_with? =~ + + # Compare two units. Returns true if quantities and units match + # @example + # RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("100 cm") # => true + # RubyUnits::Unit.new("100 cm") === RubyUnits::Unit.new("1 m") # => false + # @param [Object] other + # @return [Boolean] + def ===(other) + case other + when Unit + (scalar == other.scalar) && (units == other.units) + else + begin + x, y = coerce(other) + return x === y + rescue ArgumentError + return false + end end end - end - alias :same? :=== - alias :same_as? :=== - - # Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately - # throws an exception if the units are not compatible. - # It is possible to add Time objects to units of time - # @param [Object] other - # @return [Unit] - # @raise [ArgumentError] when two temperatures are added - # @raise [ArgumentError] when units are not compatible - # @raise [ArgumentError] when adding a fixed time or date to a time span - def +(other) - case other - when Unit - case - when self.zero? - other.dup - when self =~ other - raise ArgumentError, "Cannot add two temperatures" if ([self, other].all? {|x| x.is_temperature?}) - if [self, other].any? {|x| x.is_temperature?} - if self.is_temperature? - Unit.new(:scalar => (self.scalar + other.convert_to(self.temperature_scale).scalar), :numerator => @numerator, :denominator=>@denominator, :signature => @signature) + alias same? === + alias same_as? === + + # Add two units together. Result is same units as receiver and scalar and base_scalar are updated appropriately + # throws an exception if the units are not compatible. + # It is possible to add Time objects to units of time + # @param [Object] other + # @return [Unit] + # @raise [ArgumentError] when two temperatures are added + # @raise [ArgumentError] when units are not compatible + # @raise [ArgumentError] when adding a fixed time or date to a time span + def +(other) + case other + when Unit + if zero? + other.dup + elsif self =~ other + raise ArgumentError, 'Cannot add two temperatures' if [self, other].all?(&:temperature?) + if [self, other].any?(&:temperature?) + if temperature? + RubyUnits::Unit.new(scalar: (scalar + other.convert_to(temperature_scale).scalar), numerator: @numerator, denominator: @denominator, signature: @signature) + else + RubyUnits::Unit.new(scalar: (other.scalar + convert_to(other.temperature_scale).scalar), numerator: other.numerator, denominator: other.denominator, signature: other.signature) + end else - Unit.new(:scalar => (other.scalar + self.convert_to(other.temperature_scale).scalar), :numerator => other.numerator, :denominator=>other.denominator, :signature => other.signature) + RubyUnits::Unit.new(scalar: (base_scalar + other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self) end else - @q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.to_base.scalar)) - Unit.new(:scalar=>(self.base_scalar + other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature) + raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" end + when Date, Time + raise ArgumentError, 'Date and Time objects represent fixed points in time and cannot be added to a Unit' else - raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" + x, y = coerce(other) + y + x end - when Date, Time - raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be added to a Unit" - else - x,y = coerce(other) - y + x end - end - # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately - # @param [Numeric] other - # @return [Unit] - # @raise [ArgumentError] when subtracting a temperature from a degree - # @raise [ArgumentError] when units are not compatible - # @raise [ArgumentError] when subtracting a fixed time from a time span - def -(other) - case other + # Subtract two units. Result is same units as receiver and scalar and base_scalar are updated appropriately + # @param [Numeric] other + # @return [Unit] + # @raise [ArgumentError] when subtracting a temperature from a degree + # @raise [ArgumentError] when units are not compatible + # @raise [ArgumentError] when subtracting a fixed time from a time span + def -(other) + case other when Unit - case - when self.zero? + if zero? if other.zero? other.dup * -1 # preserve Units class else -other.dup end - when self =~ other - case - when [self, other].all? {|x| x.is_temperature?} - Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => KELVIN, :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self.temperature_scale) - when self.is_temperature? - Unit.new(:scalar => (self.base_scalar - other.base_scalar), :numerator => [''], :denominator => UNITY_ARRAY, :signature => @signature).convert_to(self) - when other.is_temperature? - raise ArgumentError, "Cannot subtract a temperature from a differential degree unit" - else - @q ||= ((@@cached_units[self.units].scalar / @@cached_units[self.units].base_scalar) rescue (self.units.unit.scalar/self.units.unit.to_base.scalar)) - Unit.new(:scalar=>(self.base_scalar - other.base_scalar)*@q, :numerator=>@numerator, :denominator=>@denominator, :signature=>@signature) + elsif self =~ other + if [self, other].all?(&:temperature?) + RubyUnits::Unit.new(scalar: (base_scalar - other.base_scalar), numerator: KELVIN, denominator: UNITY_ARRAY, signature: @signature).convert_to(temperature_scale) + elsif temperature? + RubyUnits::Unit.new(scalar: (base_scalar - other.base_scalar), numerator: [''], denominator: UNITY_ARRAY, signature: @signature).convert_to(self) + elsif other.temperature? + raise ArgumentError, 'Cannot subtract a temperature from a differential degree unit' + else + RubyUnits::Unit.new(scalar: (base_scalar - other.base_scalar), numerator: base.numerator, denominator: base.denominator, signature: @signature).convert_to(self) end else - raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" + raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" end - when Time - raise ArgumentError, "Date and Time objects represent fixed points in time and cannot be subtracted from to a Unit, which can only represent time spans" - else - x,y = coerce(other) - return y-x - end - end - - # Multiply two units. - # @param [Numeric] other - # @return [Unit] - # @raise [ArgumentError] when attempting to multiply two temperatures - def *(other) - case other - when Unit - raise ArgumentError, "Cannot multiply by temperatures" if [other,self].any? {|x| x.is_temperature?} - opts = Unit.eliminate_terms(@scalar*other.scalar, @numerator + other.numerator ,@denominator + other.denominator) - opts.merge!(:signature => @signature + other.signature) - return Unit.new(opts) - when Numeric - return Unit.new(:scalar=>@scalar*other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature) - else - x,y = coerce(other) - return x * y + when Time + raise ArgumentError, 'Date and Time objects represent fixed points in time and cannot be subtracted from to a Unit, which can only represent time spans' + else + x, y = coerce(other) + y - x + end end - end - # Divide two units. - # Throws an exception if divisor is 0 - # @param [Numeric] other - # @return [Unit] - # @raise [ZeroDivisionError] if divisor is zero - # @raise [ArgumentError] if attempting to divide a temperature by another temperature - def /(other) - case other - when Unit - raise ZeroDivisionError if other.zero? - raise ArgumentError, "Cannot divide with temperatures" if [other,self].any? {|x| x.is_temperature?} - opts = Unit.eliminate_terms(@scalar/other.scalar, @numerator + other.denominator ,@denominator + other.numerator) - opts.merge!(:signature=> @signature - other.signature) - return Unit.new(opts) - when Numeric - raise ZeroDivisionError if other.zero? - return Unit.new(:scalar=>@scalar/other, :numerator=>@numerator, :denominator=>@denominator, :signature => @signature) - else - x,y = coerce(other) - return y / x + # Multiply two units. + # @param [Numeric] other + # @return [Unit] + # @raise [ArgumentError] when attempting to multiply two temperatures + def *(other) + case other + when Unit + raise ArgumentError, 'Cannot multiply by temperatures' if [other, self].any?(&:temperature?) + opts = RubyUnits::Unit.eliminate_terms(@scalar * other.scalar, @numerator + other.numerator, @denominator + other.denominator) + opts[:signature] = @signature + other.signature + RubyUnits::Unit.new(opts) + when Numeric + RubyUnits::Unit.new(scalar: @scalar * other, numerator: @numerator, denominator: @denominator, signature: @signature) + else + x, y = coerce(other) + x * y + end end - end - # divide two units and return quotient and remainder - # when both units are in the same units we just use divmod on the raw scalars - # otherwise we use the scalar of the base unit which will be a float - # @param [Object] other - # @return [Array] - def divmod(other) - raise ArgumentError, "Incompatible Units" unless self =~ other - if self.units == other.units - return self.scalar.divmod(other.scalar) - else - return self.to_base.scalar.divmod(other.to_base.scalar) + # Divide two units. + # Throws an exception if divisor is 0 + # @param [Numeric] other + # @return [Unit] + # @raise [ZeroDivisionError] if divisor is zero + # @raise [ArgumentError] if attempting to divide a temperature by another temperature + def /(other) + case other + when Unit + raise ZeroDivisionError if other.zero? + raise ArgumentError, 'Cannot divide with temperatures' if [other, self].any?(&:temperature?) + sc = Rational(@scalar, other.scalar) + sc = sc.numerator if sc.denominator == 1 + opts = RubyUnits::Unit.eliminate_terms(sc, @numerator + other.denominator, @denominator + other.numerator) + opts[:signature] = @signature - other.signature + RubyUnits::Unit.new(opts) + when Numeric + raise ZeroDivisionError if other.zero? + sc = Rational(@scalar, other) + sc = sc.numerator if sc.denominator == 1 + RubyUnits::Unit.new(scalar: sc, numerator: @numerator, denominator: @denominator, signature: @signature) + else + x, y = coerce(other) + y / x + end end - end - # perform a modulo on a unit, will raise an exception if the units are not compatible - # @param [Object] other - # @return [Integer] - def %(other) - return self.divmod(other).last - end - - # Exponentiate. Only takes integer powers. - # Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units. - # Throws an exception if exponent is not an integer. - # Ideally this routine should accept a float for the exponent - # It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator - # but, sadly, floats can't be converted to rationals. - # - # For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1 - # @param [Numeric] other - # @return [Unit] - # @raise [ArgumentError] when raising a temperature to a power - # @raise [ArgumentError] when n not in the set integers from (1..9) - # @raise [ArgumentError] when attempting to raise to a complex number - # @raise [ArgumentError] when an invalid exponent is passed - def **(other) - raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature? - if other.kind_of?(Numeric) - return self.inverse if other == -1 - return self if other == 1 - return 1 if other.zero? - end - case other - when Rational - return self.power(other.numerator).root(other.denominator) - when Integer - return self.power(other) - when Float - return self**(other.to_i) if other == other.to_i - valid = (1..9).map {|x| 1/x} - raise ArgumentError, "Not a n-th root (1..9), use 1/n" unless valid.include? other.abs - return self.root((1/other).to_int) - when Complex - raise ArgumentError, "exponentiation of complex numbers is not yet supported." - else - raise ArgumentError, "Invalid Exponent" + # divide two units and return quotient and remainder + # when both units are in the same units we just use divmod on the raw scalars + # otherwise we use the scalar of the base unit which will be a float + # @param [Object] other + # @return [Array] + def divmod(other) + raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ other + return scalar.divmod(other.scalar) if units == other.units + to_base.scalar.divmod(other.to_base.scalar) end - end - # returns the unit raised to the n-th power - # @param [Integer] n - # @return [Unit] - # @raise [ArgumentError] when attempting to raise a temperature to a power - # @raise [ArgumentError] when n is not an integer - def power(n) - raise ArgumentError, "Cannot raise a temperature to a power" if self.is_temperature? - raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer) - return self.inverse if n == -1 - return 1 if n.zero? - return self if n == 1 - if n > 0 then - return (1..(n-1).to_i).inject(self) {|product, x| product * self} - else - return (1..-(n-1).to_i).inject(self) {|product, x| product / self} + # perform a modulo on a unit, will raise an exception if the units are not compatible + # @param [Object] other + # @return [Integer] + def %(other) + divmod(other).last end - end - # Calculates the n-th root of a unit - # if n < 0, returns 1/unit^(1/n) - # @param [Integer] n - # @return [Unit] - # @raise [ArgumentError] when attemptint to take the root of a temperature - # @raise [ArgumentError] when n is not an integer - # @raise [ArgumentError] when n is 0 - def root(n) - raise ArgumentError, "Cannot take the root of a temperature" if self.is_temperature? - raise ArgumentError, "Exponent must an Integer" unless n.kind_of?(Integer) - raise ArgumentError, "0th root undefined" if n.zero? - return self if n == 1 - return self.root(n.abs).inverse if n < 0 - - vec = self.unit_signature_vector - vec=vec.map {|x| x % n} - raise ArgumentError, "Illegal root" unless vec.max == 0 - num = @numerator.dup - den = @denominator.dup - - for item in @numerator.uniq do - x = num.find_all {|i| i==item}.size - r = ((x/n)*(n-1)).to_int - r.times {|y| num.delete_at(num.index(item))} - end - - for item in @denominator.uniq do - x = den.find_all {|i| i==item}.size - r = ((x/n)*(n-1)).to_int - r.times {|y| den.delete_at(den.index(item))} - end - q = @scalar < 0 ? (-1)**Rational(1,n) * (@scalar.abs)**Rational(1,n) : @scalar**Rational(1,n) - return Unit.new(:scalar=>q,:numerator=>num,:denominator=>den) - end - - # returns inverse of Unit (1/unit) - # @return [Unit] - def inverse - return Unit("1") / self - end - - # convert to a specified unit string or to the same units as another Unit - # - # unit.convert_to "kg" will covert to kilograms - # unit1.convert_to unit2 converts to same units as unit2 object - # - # To convert a Unit object to match another Unit object, use: - # unit1 >>= unit2 - # - # Special handling for temperature conversions is supported. If the Unit object is converted - # from one temperature unit to another, the proper temperature offsets will be used. - # Supports Kelvin, Celsius, Fahrenheit, and Rankine scales. - # - # @note If temperature is part of a compound unit, the temperature will be treated as a differential - # and the units will be scaled appropriately. - # @param [Object] other - # @return [Unit] - # @raise [ArgumentError] when attempting to convert a degree to a temperature - # @raise [ArgumentError] when target unit is unknown - # @raise [ArgumentError] when target unit is incompatible - def convert_to(other) - return self if other.nil? - return self if TrueClass === other - return self if FalseClass === other - if (Unit === other && other.is_temperature?) || (String === other && other =~ /temp[CFRK]/) - raise ArgumentError, "Receiver is not a temperature unit" unless self.degree? - start_unit = self.units - target_unit = other.units rescue other - unless @base_scalar - @base_scalar = case @@UNIT_MAP[start_unit] - when '' - @scalar + 273.15 - when '' - @scalar - when '' - (@scalar+459.67)*Rational(5,9) - when '' - @scalar*Rational(5,9) - end - end - q= case @@UNIT_MAP[target_unit] - when '' - @base_scalar - 273.15 - when '' - @base_scalar - when '' - @base_scalar * Rational(9,5) - 459.67 - when '' - @base_scalar * Rational(9,5) + # Exponentiate. Only takes integer powers. + # Note that anything raised to the power of 0 results in a Unit object with a scalar of 1, and no units. + # Throws an exception if exponent is not an integer. + # Ideally this routine should accept a float for the exponent + # It should then convert the float to a rational and raise the unit by the numerator and root it by the denominator + # but, sadly, floats can't be converted to rationals. + # + # For now, if a rational is passed in, it will be used, otherwise we are stuck with integers and certain floats < 1 + # @param [Numeric] other + # @return [Unit] + # @raise [ArgumentError] when raising a temperature to a power + # @raise [ArgumentError] when n not in the set integers from (1..9) + # @raise [ArgumentError] when attempting to raise to a complex number + # @raise [ArgumentError] when an invalid exponent is passed + def **(other) + raise ArgumentError, 'Cannot raise a temperature to a power' if temperature? + if other.is_a?(Numeric) + return inverse if other == -1 + return self if other == 1 + return 1 if other.zero? end - return Unit.new("#{q} #{target_unit}") - else case other - when Unit - return self if other.units == self.units - target = other - when String - target = Unit.new(other) + when Rational + return power(other.numerator).root(other.denominator) + when Integer + return power(other) + when Float + return self**other.to_i if other == other.to_i + valid = (1..9).map { |n| Rational(1, n) } + raise ArgumentError, 'Not a n-th root (1..9), use 1/n' unless valid.include? other.abs + return root(Rational(1, other).to_int) + when Complex + raise ArgumentError, 'exponentiation of complex numbers is not supported.' else - raise ArgumentError, "Unknown target units" + raise ArgumentError, 'Invalid Exponent' end - raise ArgumentError, "Incompatible Units" unless self =~ target - _numerator1 = @numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact - _denominator1 = @denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|i| i.kind_of?(Numeric) ? i : @@UNIT_VALUES[i][:scalar] }.compact - _numerator2 = target.numerator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact - _denominator2 = target.denominator.map {|x| @@PREFIX_VALUES[x] ? @@PREFIX_VALUES[x] : x}.map {|x| x.kind_of?(Numeric) ? x : @@UNIT_VALUES[x][:scalar] }.compact + end - q = @scalar * ( (_numerator1 + _denominator2).inject(1) {|product,n| product*n} ) / - ( (_numerator2 + _denominator1).inject(1) {|product,n| product*n} ) - return Unit.new(:scalar=>q, :numerator=>target.numerator, :denominator=>target.denominator, :signature => target.signature) + # returns the unit raised to the n-th power + # @param [Integer] n + # @return [Unit] + # @raise [ArgumentError] when attempting to raise a temperature to a power + # @raise [ArgumentError] when n is not an integer + def power(n) + raise ArgumentError, 'Cannot raise a temperature to a power' if temperature? + raise ArgumentError, 'Exponent must an Integer' unless n.is_a?(Integer) + return inverse if n == -1 + return 1 if n.zero? + return self if n == 1 + return (1..(n - 1).to_i).inject(self) { |acc, _elem| acc * self } if n >= 0 + (1..-(n - 1).to_i).inject(self) { |acc, _elem| acc / self } end - end - alias :>> :convert_to - alias :to :convert_to - - # converts the unit back to a float if it is unitless. Otherwise raises an exception - # @return [Float] - # @raise [RuntimeError] when not unitless - def to_f - return @scalar.to_f if self.unitless? - raise RuntimeError, "Cannot convert '#{self.to_s}' to Float unless unitless. Use Unit#scalar" - end - # converts the unit back to a complex if it is unitless. Otherwise raises an exception - # @return [Complex] - # @raise [RuntimeError] when not unitless - def to_c - return Complex(@scalar) if self.unitless? - raise RuntimeError, "Cannot convert '#{self.to_s}' to Complex unless unitless. Use Unit#scalar" - end + # Calculates the n-th root of a unit + # if n < 0, returns 1/unit^(1/n) + # @param [Integer] n + # @return [Unit] + # @raise [ArgumentError] when attemptint to take the root of a temperature + # @raise [ArgumentError] when n is not an integer + # @raise [ArgumentError] when n is 0 + def root(n) + raise ArgumentError, 'Cannot take the root of a temperature' if temperature? + raise ArgumentError, 'Exponent must an Integer' unless n.is_a?(Integer) + raise ArgumentError, '0th root undefined' if n.zero? + return self if n == 1 + return root(n.abs).inverse if n < 0 + + vec = unit_signature_vector + vec = vec.map { |x| x % n } + raise ArgumentError, 'Illegal root' unless vec.max.zero? + num = @numerator.dup + den = @denominator.dup + + @numerator.uniq.each do |item| + x = num.find_all { |i| i == item }.size + r = ((x / n) * (n - 1)).to_int + r.times { num.delete_at(num.index(item)) } + end - # if unitless, returns an int, otherwise raises an error - # @return [Integer] - # @raise [RuntimeError] when not unitless - def to_i - return @scalar.to_int if self.unitless? - raise RuntimeError, "Cannot convert '#{self.to_s}' to Integer unless unitless. Use Unit#scalar" - end - alias :to_int :to_i - - # if unitless, returns a Rational, otherwise raises an error - # @return [Rational] - # @raise [RuntimeError] when not unitless - def to_r - return @scalar.to_r if self.unitless? - raise RuntimeError, "Cannot convert '#{self.to_s}' to Rational unless unitless. Use Unit#scalar" - end + @denominator.uniq.each do |item| + x = den.find_all { |i| i == item }.size + r = ((x / n) * (n - 1)).to_int + r.times { den.delete_at(den.index(item)) } + end + RubyUnits::Unit.new(scalar: @scalar**Rational(1, n), numerator: num, denominator: den) + end - # Returns string formatted for json - # @return [String] - def as_json(*args) - to_s - end + # returns inverse of Unit (1/unit) + # @return [Unit] + def inverse + RubyUnits::Unit.new('1') / self + end - # returns the 'unit' part of the Unit object without the scalar - # @return [String] - def units - return "" if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY - return @unit_name unless @unit_name.nil? - output_numerator = [] - output_denominator = [] - num = @numerator.clone.compact - den = @denominator.clone.compact - - if @numerator == UNITY_ARRAY - output_numerator << "1" - else - while defn = Unit.definition(num.shift) do - if defn && defn.prefix? - output_numerator << defn.display_name + Unit.definition(num.shift).display_name + # convert to a specified unit string or to the same units as another Unit + # + # unit.convert_to "kg" will covert to kilograms + # unit1.convert_to unit2 converts to same units as unit2 object + # + # To convert a Unit object to match another Unit object, use: + # unit1 >>= unit2 + # + # Special handling for temperature conversions is supported. If the Unit object is converted + # from one temperature unit to another, the proper temperature offsets will be used. + # Supports Kelvin, Celsius, Fahrenheit, and Rankine scales. + # + # @note If temperature is part of a compound unit, the temperature will be treated as a differential + # and the units will be scaled appropriately. + # @param [Object] other + # @return [Unit] + # @raise [ArgumentError] when attempting to convert a degree to a temperature + # @raise [ArgumentError] when target unit is unknown + # @raise [ArgumentError] when target unit is incompatible + def convert_to(other) + return self if other.nil? + return self if TrueClass === other + return self if FalseClass === other + if (Unit === other && other.temperature?) || (String === other && other =~ /temp[CFRK]/) + raise ArgumentError, 'Receiver is not a temperature unit' unless degree? + start_unit = units + target_unit = begin + other.units + rescue + other + end + @base_scalar ||= case @@unit_map[start_unit] + when '' + @scalar + 273.15 + when '' + @scalar + when '' + (@scalar + 459.67).to_r * Rational(5, 9) + when '' + @scalar.to_r * Rational(5, 9) + end + q = case @@unit_map[target_unit] + when '' + @base_scalar - 273.15r + when '' + @base_scalar + when '' + @base_scalar.to_r * Rational(9, 5) - 459.67r + when '' + @base_scalar.to_r * Rational(9, 5) + end + return RubyUnits::Unit.new("#{q} #{target_unit}") + else + case other + when Unit + return self if other.units == units + target = other + when String + target = RubyUnits::Unit.new(other) else - output_numerator << defn.display_name + raise ArgumentError, 'Unknown target units' end + raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ target + numerator1 = @numerator.map { |x| @@prefix_values[x] ? @@prefix_values[x] : x }.map { |i| i.is_a?(Numeric) ? i : @@unit_values[i][:scalar] }.compact + denominator1 = @denominator.map { |x| @@prefix_values[x] ? @@prefix_values[x] : x }.map { |i| i.is_a?(Numeric) ? i : @@unit_values[i][:scalar] }.compact + numerator2 = target.numerator.map { |x| @@prefix_values[x] ? @@prefix_values[x] : x }.map { |x| x.is_a?(Numeric) ? x : @@unit_values[x][:scalar] }.compact + denominator2 = target.denominator.map { |x| @@prefix_values[x] ? @@prefix_values[x] : x }.map { |x| x.is_a?(Numeric) ? x : @@unit_values[x][:scalar] }.compact + + q = @scalar * ((numerator1 + denominator2).inject(1) { |acc, elem| acc * elem }) / + ((numerator2 + denominator1).inject(1) { |acc, elem| acc * elem }) + return RubyUnits::Unit.new(scalar: q, numerator: target.numerator, denominator: target.denominator, signature: target.signature) end end - if @denominator == UNITY_ARRAY + alias >> convert_to + alias to convert_to + + # converts the unit back to a float if it is unitless. Otherwise raises an exception + # @return [Float] + # @raise [RuntimeError] when not unitless + def to_f + return @scalar.to_f if unitless? + raise "Cannot convert '#{self}' to Float unless unitless. Use Unit#scalar" + end + + # converts the unit back to a complex if it is unitless. Otherwise raises an exception + # @return [Complex] + # @raise [RuntimeError] when not unitless + def to_c + return Complex(@scalar) if unitless? + raise "Cannot convert '#{self}' to Complex unless unitless. Use Unit#scalar" + end + + # if unitless, returns an int, otherwise raises an error + # @return [Integer] + # @raise [RuntimeError] when not unitless + def to_i + return @scalar.to_int if unitless? + raise "Cannot convert '#{self}' to Integer unless unitless. Use Unit#scalar" + end + + alias to_int to_i + + # if unitless, returns a Rational, otherwise raises an error + # @return [Rational] + # @raise [RuntimeError] when not unitless + def to_r + return @scalar.to_r if unitless? + raise "Cannot convert '#{self}' to Rational unless unitless. Use Unit#scalar" + end + + # Returns string formatted for json + # @return [String] + def as_json(*) + to_s + end + + # returns the 'unit' part of the Unit object without the scalar + # @return [String] + def units(with_prefix: true) + return '' if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY + output_numerator = ['1'] output_denominator = [] - else - while defn = Unit.definition(den.shift) do - if defn && defn.prefix? - output_denominator << defn.display_name + Unit.definition(den.shift).display_name - else - output_denominator << defn.display_name - end + num = @numerator.clone.compact + den = @denominator.clone.compact + + unless num == UNITY_ARRAY + definitions = num.map { |element| RubyUnits::Unit.definition(element) } + definitions.reject!(&:prefix?) unless with_prefix + # there is a bug in jruby 9.1.6.0's implementation of chunk_while + # see https://github.com/jruby/jruby/issues/4410 + # TODO: fix this after jruby fixes their bug. + definitions = if definitions.respond_to?(:chunk_while) && RUBY_ENGINE != 'jruby' + definitions.chunk_while { |defn, _| defn.prefix? }.to_a + else # chunk_while is new to ruby 2.3+, so fallback to less efficient methods for older ruby + result = [] + enumerator = definitions.to_enum + loop do + first = enumerator.next + result << (first.prefix? ? [first, enumerator.next] : [first]) + end + result + end + output_numerator = definitions.map { |element| element.map(&:display_name).join } end - end - on = output_numerator.uniq. - map {|x| [x, output_numerator.count(x)]}. - map {|element, power| ("#{element}".strip + (power > 1 ? "^#{power}" : ''))} - od = output_denominator.uniq. - map {|x| [x, output_denominator.count(x)]}. - map {|element, power| ("#{element}".strip + (power > 1 ? "^#{power}" : ''))} - out = "#{on.join('*')}#{od.empty? ? '': '/' + od.join('*')}".strip - @unit_name = out unless self.kind == :temperature - return out - end + unless den == UNITY_ARRAY + definitions = den.map { |element| RubyUnits::Unit.definition(element) } + definitions.reject!(&:prefix?) unless with_prefix + # there is a bug in jruby 9.1.6.0's implementation of chunk_while + # see https://github.com/jruby/jruby/issues/4410 + # TODO: fix this after jruby fixes their bug. + definitions = if definitions.respond_to?(:chunk_while) && RUBY_ENGINE != 'jruby' + definitions.chunk_while { |defn, _| defn.prefix? }.to_a + else # chunk_while is new to ruby 2.3+, so fallback to less efficient methods for older ruby + result = [] + enumerator = definitions.to_enum + loop do + first = enumerator.next + result << (first.prefix? ? [first, enumerator.next] : [first]) + end + result + end + output_denominator = definitions.map { |element| element.map(&:display_name).join } + end - # negates the scalar of the Unit - # @return [Numeric,Unit] - def -@ - return -@scalar if self.unitless? - return (self.dup * -1) - end + on = output_numerator + .uniq + .map { |x| [x, output_numerator.count(x)] } + .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) } + od = output_denominator + .uniq + .map { |x| [x, output_denominator.count(x)] } + .map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) } + "#{on.join('*')}#{od.empty? ? '' : '/' + od.join('*')}".strip + end - # absolute value of a unit - # @return [Numeric,Unit] - def abs - return @scalar.abs if self.unitless? - return Unit.new(@scalar.abs, @numerator, @denominator) - end + # negates the scalar of the Unit + # @return [Numeric,Unit] + def -@ + return -@scalar if unitless? + dup * -1 + end - # ceil of a unit - # @return [Numeric,Unit] - def ceil - return @scalar.ceil if self.unitless? - return Unit.new(@scalar.ceil, @numerator, @denominator) - end + # absolute value of a unit + # @return [Numeric,Unit] + def abs + return @scalar.abs if unitless? + RubyUnits::Unit.new(@scalar.abs, @numerator, @denominator) + end - # @return [Numeric,Unit] - def floor - return @scalar.floor if self.unitless? - return Unit.new(@scalar.floor, @numerator, @denominator) - end + # ceil of a unit + # @return [Numeric,Unit] + def ceil + return @scalar.ceil if unitless? + RubyUnits::Unit.new(@scalar.ceil, @numerator, @denominator) + end - if RUBY_VERSION < '1.9' # @return [Numeric,Unit] - def round - return @scalar.round if self.unitless? - return Unit.new(@scalar.round, @numerator, @denominator) + def floor + return @scalar.floor if unitless? + RubyUnits::Unit.new(@scalar.floor, @numerator, @denominator) end - else + # @return [Numeric,Unit] def round(ndigits = 0) - return @scalar.round(ndigits) if self.unitless? - return Unit.new(@scalar.round(ndigits), @numerator, @denominator) + return @scalar.round(ndigits) if unitless? + RubyUnits::Unit.new(@scalar.round(ndigits), @numerator, @denominator) end - end - # @return [Numeric, Unit] - def truncate - return @scalar.truncate if self.unitless? - return Unit.new(@scalar.truncate, @numerator, @denominator) - end - - # returns next unit in a range. '1 mm'.unit.succ #=> '2 mm'.unit - # only works when the scalar is an integer - # @return [Unit] - # @raise [ArgumentError] when scalar is not equal to an integer - def succ - raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i - return Unit.new(@scalar.to_i.succ, @numerator, @denominator) - end - alias :next :succ - - # returns previous unit in a range. '2 mm'.unit.pred #=> '1 mm'.unit - # only works when the scalar is an integer - # @return [Unit] - # @raise [ArgumentError] when scalar is not equal to an integer - def pred - raise ArgumentError, "Non Integer Scalar" unless @scalar == @scalar.to_i - return Unit.new(@scalar.to_i.pred, @numerator, @denominator) - end + # @return [Numeric, Unit] + def truncate + return @scalar.truncate if unitless? + RubyUnits::Unit.new(@scalar.truncate, @numerator, @denominator) + end - # Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch. - # @return [Time] - def to_time - return Time.at(self) - end - alias :time :to_time + # returns next unit in a range. '1 mm'.to_unit.succ #=> '2 mm'.to_unit + # only works when the scalar is an integer + # @return [Unit] + # @raise [ArgumentError] when scalar is not equal to an integer + def succ + raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i + RubyUnits::Unit.new(@scalar.to_i.succ, @numerator, @denominator) + end - # convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date - # defined by DateTime - # @return [DateTime] - def to_datetime - return DateTime.new!(self.convert_to('d').scalar) - end + alias next succ - # @return [Date] - def to_date - return Date.new0(self.convert_to('d').scalar) - end + # returns previous unit in a range. '2 mm'.to_unit.pred #=> '1 mm'.to_unit + # only works when the scalar is an integer + # @return [Unit] + # @raise [ArgumentError] when scalar is not equal to an integer + def pred + raise ArgumentError, 'Non Integer Scalar' unless @scalar == @scalar.to_i + RubyUnits::Unit.new(@scalar.to_i.pred, @numerator, @denominator) + end - # true if scalar is zero - # @return [Boolean] - def zero? - return self.base_scalar.zero? - end + # Tries to make a Time object from current unit. Assumes the current unit hold the duration in seconds from the epoch. + # @return [Time] + def to_time + Time.at(self) + end - # @example '5 min'.unit.ago - # @return [Unit] - def ago - return self.before - end + alias time to_time - # @example '5 min'.before(time) - # @return [Unit] - def before(time_point = ::Time.now) - case time_point - when Time, Date, DateTime - return (time_point - self rescue time_point.to_datetime - self) - else - raise ArgumentError, "Must specify a Time, Date, or DateTime" + # convert a duration to a DateTime. This will work so long as the duration is the duration from the zero date + # defined by DateTime + # @return [DateTime] + def to_datetime + DateTime.new!(convert_to('d').scalar) end - end - alias :before_now :before - - # @example 'min'.since(time) - # @param [Time, Date, DateTime] time_point - # @return [Unit] - # @raise [ArgumentError] when time point is not a Time, Date, or DateTime - def since(time_point) - case time_point - when Time - return (Time.now - time_point).unit('s').convert_to(self) - when DateTime, Date - return (DateTime.now - time_point).unit('d').convert_to(self) - else - raise ArgumentError, "Must specify a Time, Date, or DateTime" + + # @return [Date] + def to_date + Date.new0(convert_to('d').scalar) end - end - # @example 'min'.until(time) - # @param [Time, Date, DateTime] time_point - # @return [Unit] - def until(time_point) - case time_point - when Time - return (time_point - Time.now).unit('s').convert_to(self) - when DateTime, Date - return (time_point - DateTime.now).unit('d').convert_to(self) - else - raise ArgumentError, "Must specify a Time, Date, or DateTime" + # true if scalar is zero + # @return [Boolean] + def zero? + base_scalar.zero? end - end - # @example '5 min'.from(time) - # @param [Time, Date, DateTime] time_point - # @return [Time, Date, DateTime] - # @raise [ArgumentError] when passed argument is not a Time, Date, or DateTime - def from(time_point) - case time_point - when Time, DateTime, Date - return (time_point + self rescue time_point.to_datetime + self) - else - raise ArgumentError, "Must specify a Time, Date, or DateTime" + # @example '5 min'.to_unit.ago + # @return [Unit] + def ago + before end - end - alias :after :from - alias :from_now :from - - # automatically coerce objects to units when possible - # if an object defines a 'to_unit' method, it will be coerced using that method - # @param [Object, #to_unit] - # @return [Array] - def coerce(other) - if other.respond_to? :to_unit - return [other.to_unit, self] - end - case other - when Unit - return [other, self] - else - return [Unit.new(other), self] + + # @example '5 min'.before(time) + # @return [Unit] + def before(time_point = ::Time.now) + case time_point + when Time, Date, DateTime + return (begin + time_point - self + rescue + time_point.to_datetime - self + end) + else + raise ArgumentError, 'Must specify a Time, Date, or DateTime' + end end - end - # Protected and Private Functions that should only be called from this class - protected + alias before_now before - # figure out what the scalar part of the base unit for this unit is - # @return [nil] - def update_base_scalar - if self.is_base? - @base_scalar = @scalar - @signature = unit_signature - else - base = self.to_base - @base_scalar = base.scalar - @signature = base.signature + # @example 'min'.since(time) + # @param [Time, Date, DateTime] time_point + # @return [Unit] + # @raise [ArgumentError] when time point is not a Time, Date, or DateTime + def since(time_point) + case time_point + when Time + (Time.now - time_point).to_unit('s').convert_to(self) + when DateTime, Date + (DateTime.now - time_point).to_unit('d').convert_to(self) + else + raise ArgumentError, 'Must specify a Time, Date, or DateTime' + end end - end - # calculates the unit signature vector used by unit_signature - # @return [Array] - # @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20 - def unit_signature_vector - return self.to_base.unit_signature_vector unless self.is_base? - vector = Array.new(SIGNATURE_VECTOR.size,0) - # it's possible to have a kind that misses the array... kinds like :counting - # are more like prefixes, so don't use them to calculate the vector - @numerator.map {|element| Unit.definition(element)}.each do |definition| - index = SIGNATURE_VECTOR.index(definition.kind) - vector[index] += 1 if index - end - @denominator.map {|element| Unit.definition(element)}.each do |definition| - index = SIGNATURE_VECTOR.index(definition.kind) - vector[index] -= 1 if index - end - raise ArgumentError, "Power out of range (-20 < net power of a unit < 20)" if vector.any? {|x| x.abs >=20} - return vector - end - - private + # @example 'min'.until(time) + # @param [Time, Date, DateTime] time_point + # @return [Unit] + def until(time_point) + case time_point + when Time + (time_point - Time.now).to_unit('s').convert_to(self) + when DateTime, Date + (time_point - DateTime.now).to_unit('d').convert_to(self) + else + raise ArgumentError, 'Must specify a Time, Date, or DateTime' + end + end - # used by #dup to duplicate a Unit - # @param [Unit] other - # @private - def initialize_copy(other) - @numerator = other.numerator.dup - @denominator = other.denominator.dup - end + # @example '5 min'.from(time) + # @param [Time, Date, DateTime] time_point + # @return [Time, Date, DateTime] + # @raise [ArgumentError] when passed argument is not a Time, Date, or DateTime + def from(time_point) + case time_point + when Time, DateTime, Date + (begin + time_point + self + rescue + time_point.to_datetime + self + end) + else + raise ArgumentError, 'Must specify a Time, Date, or DateTime' + end + end - # calculates the unit signature id for use in comparing compatible units and simplification - # the signature is based on a simple classification of units and is based on the following publication - # - # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering, 21(8), Aug 1995, pp.651-661 - # @see http://doi.ieeecomputersociety.org/10.1109/32.403789 - # @return [Array] - def unit_signature - return @signature unless @signature.nil? - vector = unit_signature_vector - vector.each_with_index {|item,index| vector[index] = item * 20**index} - @signature=vector.inject(0) {|sum,n| sum+n} - return @signature - end + alias after from + alias from_now from - # @param [Numeric] q quantity - # @param [Array] n numerator - # @param [Array] d denominator - # @return [Hash] - def self.eliminate_terms(q, n, d) - num = n.dup - den = d.dup - - num.delete_if {|v| v == UNITY} - den.delete_if {|v| v == UNITY} - combined = Hash.new(0) - - i = 0 - loop do - break if i > num.size - if @@PREFIX_VALUES.has_key? num[i] - k = [num[i],num[i+1]] - i += 2 + # automatically coerce objects to units when possible + # if an object defines a 'to_unit' method, it will be coerced using that method + # @param [Object, #to_unit] + # @return [Array] + def coerce(other) + return [other.to_unit, self] if other.respond_to? :to_unit + case other + when Unit + [other, self] else - k = num[i] - i += 1 + [RubyUnits::Unit.new(other), self] end - combined[k] += 1 unless k.nil? || k == UNITY end - j = 0 - loop do - break if j > den.size - if @@PREFIX_VALUES.has_key? den[j] - k = [den[j],den[j+1]] - j += 2 - else - k = den[j] - j += 1 - end - combined[k] -= 1 unless k.nil? || k == UNITY + # returns a new unit that has been scaled to be more in line with typical usage. + def best_prefix + return to_base if scalar.zero? + best_prefix = if kind == :information + @@prefix_values.key(2**((Math.log(base_scalar, 2) / 10.0).floor * 10)) + else + @@prefix_values.key(10**((Math.log10(base_scalar) / 3.0).floor * 3)) + end + to(RubyUnits::Unit.new(@@prefix_map.key(best_prefix) + units(with_prefix: false))) end - num = [] - den = [] - for key, value in combined do - case - when value > 0 - value.times {num << key} - when value < 0 - value.abs.times {den << key} + # override hash method so objects with same values are considered equal + def hash + [ + @scalar, + @numerator, + @denominator, + @base, + @signature, + @base_scalar, + @unit_name + ].hash + end + + # Protected and Private Functions that should only be called from this class + protected + + # figure out what the scalar part of the base unit for this unit is + # @return [nil] + def update_base_scalar + if base? + @base_scalar = @scalar + @signature = unit_signature + else + base = to_base + @base_scalar = base.scalar + @signature = base.signature end end - num = UNITY_ARRAY if num.empty? - den = UNITY_ARRAY if den.empty? - return {:scalar=>q, :numerator=>num.flatten.compact, :denominator=>den.flatten.compact} - end - # parse a string into a unit object. - # Typical formats like : - # "5.6 kg*m/s^2" - # "5.6 kg*m*s^-2" - # "5.6 kilogram*meter*second^-2" - # "2.2 kPa" - # "37 degC" - # "1" -- creates a unitless constant with value 1 - # "GPa" -- creates a unit with scalar 1 with units 'GPa' - # 6'4" -- recognized as 6 feet + 4 inches - # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces - # @return [nil | Unit] - # @todo This should either be a separate class or at least a class method - def parse(passed_unit_string="0") - unit_string = passed_unit_string.dup - if unit_string =~ /\$\s*(#{NUMBER_REGEX})/ - unit_string = "#{$1} USD" - end - unit_string.gsub!("\u00b0".force_encoding('utf-8'), 'deg') if RUBY_VERSION >= '1.9' && unit_string.encoding == Encoding::UTF_8 - - unit_string.gsub!(/%/,'percent') - unit_string.gsub!(/'/,'feet') - unit_string.gsub!(/"/,'inch') - unit_string.gsub!(/#/,'pound') - - #:nocov: - #:nocov_19: - if defined?(Uncertain) && unit_string =~ /(\+\/-|±)/ - value, uncertainty, unit_s = unit_string.scan(UNCERTAIN_REGEX)[0] - result = unit_s.unit * Uncertain.new(value.to_f,uncertainty.to_f) - copy(result) - return - end - #:nocov: - #:nocov_19: - - if defined?(Complex) && unit_string =~ COMPLEX_NUMBER - real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0] - result = Unit(unit_s || '1') * Complex(real.to_f,imaginary.to_f) - copy(result) - return - end - - if defined?(Rational) && unit_string =~ RATIONAL_NUMBER - numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0] - result = Unit(unit_s || '1') * Rational(numerator.to_i,denominator.to_i) - copy(result) - return - end - - unit_string =~ NUMBER_REGEX - unit = @@cached_units[$2] - mult = ($1.empty? ? 1.0 : $1.to_f) rescue 1.0 - mult = mult.to_int if (mult.to_int == mult) - if unit - copy(unit) - @scalar *= mult - @base_scalar *= mult - return self - end - unit_string.gsub!(/<(#{@@UNIT_REGEX})><(#{@@UNIT_REGEX})>/, '\1*\2') - unit_string.gsub!(/[<>]/,"") - - if unit_string =~ /:/ - hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0] - raise ArgumentError, "Invalid Duration" if [hours, minutes, seconds, microseconds].all? {|x| x.nil?} - result = "#{hours || 0} h".unit + - "#{minutes || 0} minutes".unit + - "#{seconds || 0} seconds".unit + - "#{microseconds || 0} usec".unit - copy(result) - return - end - - # Special processing for unusual unit strings - # feet -- 6'5" - feet, inches = unit_string.scan(FEET_INCH_REGEX)[0] - if (feet && inches) - result = Unit.new("#{feet} ft") + Unit.new("#{inches} inches") - copy(result) - return - end - - # weight -- 8 lbs 12 oz - pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0] - if (pounds && oz) - result = Unit.new("#{pounds} lbs") + Unit.new("#{oz} oz") - copy(result) - return - end - - # more than one per. I.e., "1 m/s/s" - raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1 - raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.scan(/\s[02-9]/).size > 0 - @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] #parse the string into parts - top.scan(TOP_REGEX).each do |item| - n = item[1].to_i - x = "#{item[0]} " - case - when n>=0 - top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) {|s| x * n} - when n<0 - bottom = "#{bottom} #{x * -n}"; top.gsub!(/#{item[0]}(\^|\*\*)#{n}/,"") + # calculates the unit signature vector used by unit_signature + # @return [Array] + # @raise [ArgumentError] when exponent associated with a unit is > 20 or < -20 + def unit_signature_vector + return to_base.unit_signature_vector unless base? + vector = Array.new(SIGNATURE_VECTOR.size, 0) + # it's possible to have a kind that misses the array... kinds like :counting + # are more like prefixes, so don't use them to calculate the vector + @numerator.map { |element| RubyUnits::Unit.definition(element) }.each do |definition| + index = SIGNATURE_VECTOR.index(definition.kind) + vector[index] += 1 if index end + @denominator.map { |element| RubyUnits::Unit.definition(element) }.each do |definition| + index = SIGNATURE_VECTOR.index(definition.kind) + vector[index] -= 1 if index + end + raise ArgumentError, 'Power out of range (-20 < net power of a unit < 20)' if vector.any? { |x| x.abs >= 20 } + vector end - bottom.gsub!(BOTTOM_REGEX) {|s| "#{$1} " * $2.to_i} if bottom - @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty? - @scalar = 1 unless @scalar.kind_of? Numeric - @scalar = @scalar.to_int if (@scalar.to_int == @scalar) - @numerator ||= UNITY_ARRAY - @denominator ||= UNITY_ARRAY - @numerator = top.scan(Unit.unit_match_regex).delete_if {|x| x.empty?}.compact if top - @denominator = bottom.scan(Unit.unit_match_regex).delete_if {|x| x.empty?}.compact if bottom + private - # eliminate all known terms from this string. This is a quick check to see if the passed unit - # contains terms that are not defined. - used = "#{top} #{bottom}".to_s.gsub(Unit.unit_match_regex,'').gsub(/[\d\*, "'_^\/\$]/,'') - raise( ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty? + # used by #dup to duplicate a Unit + # @param [Unit] other + # @private + def initialize_copy(other) + @numerator = other.numerator.dup + @denominator = other.denominator.dup + end - @numerator = @numerator.map do |item| - @@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]] - end.flatten.compact.delete_if {|x| x.empty?} + # calculates the unit signature id for use in comparing compatible units and simplification + # the signature is based on a simple classification of units and is based on the following publication + # + # Novak, G.S., Jr. "Conversion of units of measurement", IEEE Transactions on Software Engineering, 21(8), Aug 1995, pp.651-661 + # @see http://doi.ieeecomputersociety.org/10.1109/32.403789 + # @return [Array] + def unit_signature + return @signature unless @signature.nil? + vector = unit_signature_vector + vector.each_with_index { |item, index| vector[index] = item * 20**index } + @signature = vector.inject(0) { |acc, elem| acc + elem } + @signature + end - @denominator = @denominator.map do |item| - @@PREFIX_MAP[item[0]] ? [@@PREFIX_MAP[item[0]], @@UNIT_MAP[item[1]]] : [@@UNIT_MAP[item[1]]] - end.flatten.compact.delete_if {|x| x.empty?} + # parse a string into a unit object. + # Typical formats like : + # "5.6 kg*m/s^2" + # "5.6 kg*m*s^-2" + # "5.6 kilogram*meter*second^-2" + # "2.2 kPa" + # "37 degC" + # "1" -- creates a unitless constant with value 1 + # "GPa" -- creates a unit with scalar 1 with units 'GPa' + # 6'4" -- recognized as 6 feet + 4 inches + # 8 lbs 8 oz -- recognized as 8 lbs + 8 ounces + # @return [nil | Unit] + # @todo This should either be a separate class or at least a class method + def parse(passed_unit_string = '0') + unit_string = passed_unit_string.dup + unit_string = "#{Regexp.last_match(1)} USD" if unit_string =~ /\$\s*(#{NUMBER_REGEX})/ + unit_string.gsub!("\u00b0".force_encoding('utf-8'), 'deg') if unit_string.encoding == Encoding::UTF_8 + + unit_string.gsub!(/[%'"#]/, '%' => 'percent', "'" => 'feet', '"' => 'inch', '#' => 'pound') + + if defined?(Complex) && unit_string =~ COMPLEX_NUMBER + real, imaginary, unit_s = unit_string.scan(COMPLEX_REGEX)[0] + result = RubyUnits::Unit.new(unit_s || '1') * Complex(real.to_f, imaginary.to_f) + copy(result) + return + end - @numerator = UNITY_ARRAY if @numerator.empty? - @denominator = UNITY_ARRAY if @denominator.empty? - return self - end + power_match = %r{\^\d+/\d+} # This prevents thinking e.g. kg^2/100l is rational because of 2/100 + if defined?(Rational) && unit_string =~ RATIONAL_NUMBER && !unit_string.match(power_match) + sign, proper, numerator, denominator, unit_s = unit_string.scan(RATIONAL_REGEX)[0] + sign = sign == '-' ? -1 : 1 + rational = sign * (proper.to_i + Rational(numerator.to_i, denominator.to_i)) + result = RubyUnits::Unit.new(unit_s || '1') * rational + copy(result) + return + end - # return an array of base units - # @return [Array] - def self.base_units - return @@base_units ||= @@definitions.dup.delete_if {|_, defn| !defn.base?}.keys.map {|u| Unit.new(u)} - end + unit_string =~ NUMBER_REGEX + unit = @@cached_units[Regexp.last_match(2)] + mult = begin + (Regexp.last_match(1).empty? ? 1.0 : Regexp.last_match(1).to_f) + rescue + 1.0 + end + mult = mult.to_int if mult.to_int == mult + if unit + copy(unit) + @scalar *= mult + @base_scalar *= mult + return self + end - private - - # parse a string consisting of a number and a unit string - # @param [String] string - # @return [Array] consisting of [Numeric, "unit"] - # @private - def self.parse_into_numbers_and_units(string) - # scientific notation.... 123.234E22, -123.456e-10 - sci = %r{[+-]?\d*[.]?\d+(?:[Ee][+-]?)?\d*} - # rational numbers.... -1/3, 1/5, 20/100 - rational = %r{[+-]?\d+\/\d+} - # complex numbers... -1.2+3i, +1.2-3.3i - complex = %r{#{sci}{2,2}i} - anynumber = %r{(?:(#{complex}|#{rational}|#{sci})\b)?\s?([\D].*)?} - num, unit = string.scan(anynumber).first - - return [case num - when NilClass - 1 - when complex - if num.respond_to?(:to_c) - num.to_c - else - #:nocov_19: - Complex(*num.scan(/(#{sci})(#{sci})i/).flatten.map {|n| n.to_i}) - #:nocov_19: + while unit_string.gsub!(/(<#{@@unit_regex})><(#{@@unit_regex}>)/, '\1*\2') + # collapse into ... + end + # ... and then strip the remaining brackets for x*y*z + unit_string.gsub!(/[<>]/, '') + + if unit_string =~ /:/ + hours, minutes, seconds, microseconds = unit_string.scan(TIME_REGEX)[0] + raise ArgumentError, 'Invalid Duration' if [hours, minutes, seconds, microseconds].all?(&:nil?) + result = RubyUnits::Unit.new("#{hours || 0} h") + + RubyUnits::Unit.new("#{minutes || 0} minutes") + + RubyUnits::Unit.new("#{seconds || 0} seconds") + + RubyUnits::Unit.new("#{microseconds || 0} usec") + copy(result) + return + end + + # Special processing for unusual unit strings + # feet -- 6'5" + feet, inches = unit_string.scan(FEET_INCH_REGEX)[0] + if feet && inches + result = RubyUnits::Unit.new("#{feet} ft") + RubyUnits::Unit.new("#{inches} inches") + copy(result) + return + end + + # weight -- 8 lbs 12 oz + pounds, oz = unit_string.scan(LBS_OZ_REGEX)[0] + if pounds && oz + result = RubyUnits::Unit.new("#{pounds} lbs") + RubyUnits::Unit.new("#{oz} oz") + copy(result) + return + end + + # stone -- 3 stone 5, 2 stone, 14 stone 3 pounds, etc. + stone, pounds = unit_string.scan(STONE_LB_REGEX)[0] + if stone && pounds + result = RubyUnits::Unit.new("#{stone} stone") + RubyUnits::Unit.new("#{pounds} lbs") + copy(result) + return + end + + # more than one per. I.e., "1 m/s/s" + raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string.count('/') > 1 + raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") if unit_string =~ /\s[02-9]/ + @scalar, top, bottom = unit_string.scan(UNIT_STRING_REGEX)[0] # parse the string into parts + top.scan(TOP_REGEX).each do |item| + n = item[1].to_i + x = "#{item[0]} " + if n >= 0 + top.gsub!(/#{item[0]}(\^|\*\*)#{n}/) { x * n } + elsif n < 0 + bottom = "#{bottom} #{x * -n}" + top.gsub!(/#{item[0]}(\^|\*\*)#{n}/, '') end - when rational - Rational(*num.split("/").map {|x| x.to_i}) - else - num.to_f - end, unit.to_s.strip] - end + end + if bottom + bottom.gsub!(BOTTOM_REGEX) { "#{Regexp.last_match(1)} " * Regexp.last_match(2).to_i } + # Separate leading decimal from denominator, if any + bottom_scalar, bottom = bottom.scan(NUMBER_UNIT_REGEX)[0] unless self.class.defined?(bottom) # Maintain scalar if it's part of the unit definition + end - # return a fragment of a regex to be used for matching units or reconstruct it if hasn't been used yet. - # Unit names are reverse sorted by length so the regexp matcher will prefer longer and more specific names - # @return [String] - # @private - def self.unit_regex - @@UNIT_REGEX ||= @@UNIT_MAP.keys.sort_by {|unit_name| [unit_name.length, unit_name]}.reverse.join('|') - end + @scalar = @scalar.to_f unless @scalar.nil? || @scalar.empty? + @scalar = 1 unless @scalar.is_a? Numeric + @scalar = @scalar.to_int if @scalar.to_int == @scalar - # return a regex used to match units - # @return [RegExp] - # @private - def self.unit_match_regex - @@UNIT_MATCH_REGEX ||= /(#{Unit.prefix_regex})*?(#{Unit.unit_regex})\b/ - end + bottom_scalar = 1 if bottom_scalar.nil? || bottom_scalar.empty? + bottom_scalar = if bottom_scalar.to_i == bottom_scalar + bottom_scalar.to_i + else + bottom_scalar.to_f + end - # return a regexp fragment used to match prefixes - # @return [String] - # @private - def self.prefix_regex - return @@PREFIX_REGEX ||= @@PREFIX_MAP.keys.sort_by {|prefix| [prefix.length, prefix]}.reverse.join('|') - end + @scalar /= bottom_scalar - def self.temp_regex - @@TEMP_REGEX ||= Regexp.new "(?:#{ - temp_units=%w(tempK tempC tempF tempR degK degC degF degR) - aliases=temp_units.map{|unit| d=Unit.definition(unit); d && d.aliases}.flatten.compact - regex_str= aliases.empty? ? '(?!x)x' : aliases.join('|') - regex_str - })" - end + @numerator ||= UNITY_ARRAY + @denominator ||= UNITY_ARRAY + @numerator = top.scan(RubyUnits::Unit.unit_match_regex).delete_if(&:empty?).compact if top + @denominator = bottom.scan(RubyUnits::Unit.unit_match_regex).delete_if(&:empty?).compact if bottom + + # eliminate all known terms from this string. This is a quick check to see if the passed unit + # contains terms that are not defined. + used = "#{top} #{bottom}".to_s.gsub(RubyUnits::Unit.unit_match_regex, '').gsub(%r{[\d\*, "'_^\/\$]}, '') + raise(ArgumentError, "'#{passed_unit_string}' Unit not recognized") unless used.empty? + + @numerator = @numerator.map do |item| + @@prefix_map[item[0]] ? [@@prefix_map[item[0]], @@unit_map[item[1]]] : [@@unit_map[item[1]]] + end.flatten.compact.delete_if(&:empty?) - # inject a definition into the internal array and set it up for use - # @private - def self.use_definition(definition) - @@UNIT_MATCH_REGEX = nil #invalidate the unit match regex - @@TEMP_REGEX = nil #invalidate the temp regex - if definition.prefix? - @@PREFIX_VALUES[definition.name] = definition.scalar - definition.aliases.each {|_alias| @@PREFIX_MAP[_alias] = definition.name } - @@PREFIX_REGEX = nil #invalidate the prefix regex - else - @@UNIT_VALUES[definition.name] = {} - @@UNIT_VALUES[definition.name][:scalar] = definition.scalar - @@UNIT_VALUES[definition.name][:numerator] = definition.numerator if definition.numerator - @@UNIT_VALUES[definition.name][:denominator] = definition.denominator if definition.denominator - definition.aliases.each {|_alias| @@UNIT_MAP[_alias] = definition.name} - @@UNIT_REGEX = nil #invalidate the unit regex + @denominator = @denominator.map do |item| + @@prefix_map[item[0]] ? [@@prefix_map[item[0]], @@unit_map[item[1]]] : [@@unit_map[item[1]]] + end.flatten.compact.delete_if(&:empty?) + + @numerator = UNITY_ARRAY if @numerator.empty? + @denominator = UNITY_ARRAY if @denominator.empty? + self end end - end diff --git a/lib/ruby_units/unit_definitions.rb b/lib/ruby_units/unit_definitions.rb index 4f6b0ea6..0aa0331f 100644 --- a/lib/ruby_units/unit_definitions.rb +++ b/lib/ruby_units/unit_definitions.rb @@ -1,3 +1,3 @@ -require 'ruby_units/unit_definitions/prefix' -require 'ruby_units/unit_definitions/base' -require 'ruby_units/unit_definitions/standard' +require_relative 'unit_definitions/prefix' +require_relative 'unit_definitions/base' +require_relative 'unit_definitions/standard' diff --git a/lib/ruby_units/unit_definitions/base.rb b/lib/ruby_units/unit_definitions/base.rb index 6795cf32..c87572e5 100644 --- a/lib/ruby_units/unit_definitions/base.rb +++ b/lib/ruby_units/unit_definitions/base.rb @@ -1,103 +1,100 @@ # seed the cache -Unit('1') +RubyUnits::Unit.new('1') -Unit.define("meter") do |unit| +RubyUnits::Unit.define('meter') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{m meter meters metre metres} + unit.numerator = %w[] + unit.aliases = %w[m meter meters metre metres] unit.kind = :length end -Unit.define("kilogram") do |unit| +RubyUnits::Unit.define('kilogram') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{kg kilogram kilograms} + unit.numerator = %w[] + unit.aliases = %w[kg kilogram kilograms] unit.kind = :mass end -Unit.define("second") do |unit| +RubyUnits::Unit.define('second') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{s sec second seconds} + unit.numerator = %w[] + unit.aliases = %w[s sec second seconds] unit.kind = :time end -Unit.define("mole") do |unit| +RubyUnits::Unit.define('mole') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{mol mole} + unit.numerator = %w[] + unit.aliases = %w[mol mole] unit.kind = :substance end -Unit.define("ampere") do |unit| +RubyUnits::Unit.define('ampere') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{A ampere amperes amp amps} + unit.numerator = %w[] + unit.aliases = %w[A ampere amperes amp amps] unit.kind = :current end -Unit.define("radian") do |unit| +RubyUnits::Unit.define('radian') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{rad radian radians} + unit.numerator = %w[] + unit.aliases = %w[rad radian radians] unit.kind = :angle end -Unit.define("kelvin") do |unit| +RubyUnits::Unit.define('kelvin') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{degK kelvin} + unit.numerator = %w[] + unit.aliases = %w[degK kelvin] unit.kind = :temperature end -Unit.define("tempK") do |unit| +RubyUnits::Unit.define('tempK') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{tempK} + unit.numerator = %w[] + unit.aliases = %w[tempK] unit.kind = :temperature end -Unit.define("byte") do |unit| +RubyUnits::Unit.define('byte') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{B byte bytes} - unit.kind = :memory + unit.numerator = %w[] + unit.aliases = %w[B byte bytes] + unit.kind = :information end -Unit.define("dollar") do |unit| +RubyUnits::Unit.define('dollar') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{USD dollar} + unit.numerator = %w[] + unit.aliases = %w[USD dollar] unit.kind = :currency end -Unit.define("candela") do |unit| +RubyUnits::Unit.define('candela') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{cd candela} + unit.numerator = %w[] + unit.aliases = %w[cd candela] unit.kind = :luminosity end -Unit.define("each") do |unit| +RubyUnits::Unit.define('each') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{each} + unit.numerator = %w[] + unit.aliases = %w[each] unit.kind = :counting end -Unit.define("steradian") do |unit| +RubyUnits::Unit.define('steradian') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{sr steradian steradians} + unit.numerator = %w[] + unit.aliases = %w[sr steradian steradians] unit.kind = :solid_angle end -Unit.define("decibel") do |unit| +RubyUnits::Unit.define('decibel') do |unit| unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{dB decibel decibels} + unit.numerator = %w[] + unit.aliases = %w[dB decibel decibels] unit.kind = :logarithmic end - - - diff --git a/lib/ruby_units/unit_definitions/prefix.rb b/lib/ruby_units/unit_definitions/prefix.rb index 3b3e9da1..852497b7 100644 --- a/lib/ruby_units/unit_definitions/prefix.rb +++ b/lib/ruby_units/unit_definitions/prefix.rb @@ -1,40 +1,39 @@ - { - 'googol' => [%w{googol}, 1e100], - 'yebi' => [%w{Yi Yebi yebi}, 2**80], - 'zebi' => [%w{Zi Zebi zebi}, 2**70], - 'exi' => [%w{Ei Exi exi}, 2**60], - 'pebi' => [%w{Pi Pebi pebi}, 2**50], - 'tebi' => [%w{Ti Tebi tebi}, 2**40], - 'gibi' => [%w{Gi Gibi gibi}, 2**30], - 'mebi' => [%w{Mi Mebi mebi}, 2**20], - 'kibi' => [%w{Ki Kibi kibi}, 2**10], - 'yotta' => [%w{Y Yotta yotta}, 1e24], - 'zetta' => [%w{Z Zetta zetta}, 1e21], - 'exa' => [%w{E Exa exa}, 1e18], - 'peta' => [%w{P Peta peta}, 1e15], - 'tera' => [%w{T Tera tera}, 1e12], - 'giga' => [%w{G Giga giga}, 1e9], - 'mega' => [%w{M Mega mega}, 1e6], - 'kilo' => [%w{k kilo}, 1e3], - 'hecto' => [%w{h Hecto hecto}, 1e2], - 'deca' => [%w{da Deca deca deka}, 1e1], - '1' => [%w{1}, 1], - 'deci' => [%w{d Deci deci}, Rational(1,1e1)], - 'centi' => [%w{c Centi centi}, Rational(1,1e2)], - 'milli' => [%w{m Milli milli}, Rational(1,1e3)], - 'micro' => [%w{u Micro micro}, Rational(1,1e6)], - 'nano' => [%w{n Nano nano}, Rational(1,1e9)], - 'pico' => [%w{p Pico pico}, Rational(1,1e12)], - 'femto' => [%w{f Femto femto}, Rational(1,1e15)], - 'atto' => [%w{a Atto atto}, Rational(1,1e18)], - 'zepto' => [%w{z Zepto zepto}, Rational(1,1e21)], - 'yocto' => [%w{y Yocto yocto}, Rational(1,1e24)] + 'googol' => [%w[googol], 1e100], + 'yobi' => [%w[Yi Yobi yobi], 2**80], + 'zebi' => [%w[Zi Zebi zebi], 2**70], + 'exbi' => [%w[Ei Exbi exbi], 2**60], + 'pebi' => [%w[Pi Pebi pebi], 2**50], + 'tebi' => [%w[Ti Tebi tebi], 2**40], + 'gibi' => [%w[Gi Gibi gibi], 2**30], + 'mebi' => [%w[Mi Mebi mebi], 2**20], + 'kibi' => [%w[Ki Kibi kibi], 2**10], + 'yotta' => [%w[Y Yotta yotta], 1e24], + 'zetta' => [%w[Z Zetta zetta], 1e21], + 'exa' => [%w[E Exa exa], 1e18], + 'peta' => [%w[P Peta peta], 1e15], + 'tera' => [%w[T Tera tera], 1e12], + 'giga' => [%w[G Giga giga], 1e9], + 'mega' => [%w[M Mega mega], 1e6], + 'kilo' => [%w[k kilo], 1e3], + 'hecto' => [%w[h Hecto hecto], 1e2], + 'deca' => [%w[da Deca deca deka], 1e1], + '1' => [%w[1], 1], + 'deci' => [%w[d Deci deci], Rational(1, 1e1)], + 'centi' => [%w[c Centi centi], Rational(1, 1e2)], + 'milli' => [%w[m Milli milli], Rational(1, 1e3)], + 'micro' => [%w[u µ Micro micro mc], Rational(1, 1e6)], + 'nano' => [%w[n Nano nano], Rational(1, 1e9)], + 'pico' => [%w[p Pico pico], Rational(1, 1e12)], + 'femto' => [%w[f Femto femto], Rational(1, 1e15)], + 'atto' => [%w[a Atto atto], Rational(1, 1e18)], + 'zepto' => [%w[z Zepto zepto], Rational(1, 1e21)], + 'yocto' => [%w[y Yocto yocto], Rational(1, 1e24)] }.each do |name, definition| - Unit.define(name) do |unit| + RubyUnits::Unit.define(name) do |unit| aliases, scalar = definition unit.aliases = aliases unit.scalar = scalar unit.kind = :prefix end -end \ No newline at end of file +end diff --git a/lib/ruby_units/unit_definitions/standard.rb b/lib/ruby_units/unit_definitions/standard.rb index a1987f4c..26510df4 100644 --- a/lib/ruby_units/unit_definitions/standard.rb +++ b/lib/ruby_units/unit_definitions/standard.rb @@ -1,705 +1,718 @@ # length units -Unit.define('inch') do |inch| - inch.definition = Unit('254/10000 meter') - inch.aliases = %w{in inch inches "} +RubyUnits::Unit.define('inch') do |inch| + inch.definition = RubyUnits::Unit.new('254/10000 meter') + inch.aliases = %w[in inch inches "] end -Unit.define('foot') do |foot| - foot.definition = Unit('12 inches') - foot.aliases = %w{ft foot feet '} +RubyUnits::Unit.define('foot') do |foot| + foot.definition = RubyUnits::Unit.new('12 inches') + foot.aliases = %w[ft foot feet '] end -Unit.define('survey-foot') do |sft| - sft.definition = Unit('1200/3937 meter') - sft.aliases = %w{sft sfoot sfeet} +RubyUnits::Unit.define('survey-foot') do |sft| + sft.definition = RubyUnits::Unit.new('1200/3937 meter') + sft.aliases = %w[sft sfoot sfeet] end -Unit.define('yard') do |yard| - yard.definition = Unit('3 ft') - yard.aliases = %w{yd yard yards} +RubyUnits::Unit.define('yard') do |yard| + yard.definition = RubyUnits::Unit.new('3 ft') + yard.aliases = %w[yd yard yards] end -Unit.define('mile') do |mile| - mile.definition = Unit('5280 ft') - mile.aliases = %w{mi mile miles} +RubyUnits::Unit.define('mile') do |mile| + mile.definition = RubyUnits::Unit.new('5280 ft') + mile.aliases = %w[mi mile miles] end -Unit.define('naut-mile') do |naut| - naut.definition = Unit('1852 m') - naut.aliases = %w{nmi M NM} +RubyUnits::Unit.define('naut-mile') do |naut| + naut.definition = RubyUnits::Unit.new('1852 m') + naut.aliases = %w[nmi M NM] end # on land -Unit.define('league') do |league| - league.definition = Unit('3 miles') - league.aliases = %w{league leagues} +RubyUnits::Unit.define('league') do |league| + league.definition = RubyUnits::Unit.new('3 miles') + league.aliases = %w[league leagues] end # at sea -Unit.define('naut-league') do |naut_league| - naut_league.definition = Unit('3 nmi') - naut_league.aliases = %w{nleague nleagues} +RubyUnits::Unit.define('naut-league') do |naut_league| + naut_league.definition = RubyUnits::Unit.new('3 nmi') + naut_league.aliases = %w[nleague nleagues] end -Unit.define('furlong') do |furlong| - furlong.definition = Unit('1/8 mile') - furlong.aliases = %w{fur furlong furlongs} +RubyUnits::Unit.define('furlong') do |furlong| + furlong.definition = RubyUnits::Unit.new('1/8 mile') + furlong.aliases = %w[fur furlong furlongs] end -Unit.define('rod') do |rod| - rod.definition = Unit('33/2 feet') - rod.aliases = %w{rd rod rods} +RubyUnits::Unit.define('rod') do |rod| + rod.definition = RubyUnits::Unit.new('33/2 feet') + rod.aliases = %w[rd rod rods] end -Unit.define('fathom') do |fathom| - fathom.definition = Unit('6 ft') - fathom.aliases = %w{fathom fathoms} +RubyUnits::Unit.define('fathom') do |fathom| + fathom.definition = RubyUnits::Unit.new('6 ft') + fathom.aliases = %w[fathom fathoms] end -Unit.define('mil') do |mil| - mil.definition = Unit('1/1000 inch') - mil.aliases = %w{mil mils} +RubyUnits::Unit.define('mil') do |mil| + mil.definition = RubyUnits::Unit.new('1/1000 inch') + mil.aliases = %w[mil mils] end -Unit.define('angstrom') do |ang| - ang.definition = Unit('1/10 nm') - ang.aliases = %w{ang angstrom angstroms} +RubyUnits::Unit.define('angstrom') do |ang| + ang.definition = RubyUnits::Unit.new('1/10 nm') + ang.aliases = %w[ang angstrom angstroms] end # typesetting -Unit.define('point') do |point| - point.definition = Unit('1/72 ft') - point.aliases = %w{point points} +RubyUnits::Unit.define('pica') do |pica| + pica.definition = RubyUnits::Unit.new('1/72 ft') + pica.aliases = %w[P pica picas] end -Unit.define('pica') do |pica| - pica.definition = Unit('12 point') - pica.aliases = %w{P pica picas} +RubyUnits::Unit.define('point') do |point| + point.definition = RubyUnits::Unit.new('1/12 pica') + point.aliases = %w[point points] end -Unit.define('dot') do |dot| - dot.definition = Unit('1 each') - dot.aliases = %w{dot dots} +RubyUnits::Unit.define('dot') do |dot| + dot.definition = RubyUnits::Unit.new('1 each') + dot.aliases = %w[dot dots] dot.kind = :counting end -Unit.define('pixel') do |pixel| - pixel.definition = Unit('1 each') - pixel.aliases = %w{px pixel pixels} +RubyUnits::Unit.define('pixel') do |pixel| + pixel.definition = RubyUnits::Unit.new('1 each') + pixel.aliases = %w[px pixel pixels] pixel.kind = :counting end -Unit.define('ppi') do |ppi| - ppi.definition = Unit('1 pixel/inch') +RubyUnits::Unit.define('ppi') do |ppi| + ppi.definition = RubyUnits::Unit.new('1 pixel/inch') end -Unit.define('dpi') do |dpi| - dpi.definition = Unit('1 dot/inch') +RubyUnits::Unit.define('dpi') do |dpi| + dpi.definition = RubyUnits::Unit.new('1 dot/inch') end # Mass -avagadro_constant = Unit('6.02214129e23 1/mol') +avagadro_constant = RubyUnits::Unit.new('6.02214129e23 1/mol') -Unit.define('AMU') do |amu| - amu.definition = Unit('12 kg/mol') / (12 * avagadro_constant) - amu.aliases = %w{u AMU amu} +RubyUnits::Unit.define('AMU') do |amu| + amu.definition = RubyUnits::Unit.new('0.012 kg/mol') / (12 * avagadro_constant) + amu.aliases = %w[u AMU amu] end -Unit.define('dalton') do |dalton| - dalton.definition = Unit('1 amu') - dalton.aliases = %w{Da dalton daltons} +RubyUnits::Unit.define('dalton') do |dalton| + dalton.definition = RubyUnits::Unit.new('1 amu') + dalton.aliases = %w[Da dalton daltons] end -standard_gravitation = Unit('9.80665 m/s^2') - -Unit.define('metric-ton') do |mton| - mton.definition = Unit('1000 kg') - mton.aliases = %w{tonne} +RubyUnits::Unit.define('metric-ton') do |mton| + mton.definition = RubyUnits::Unit.new('1000 kg') + mton.aliases = %w[tonne] end # defined as a rational number to preserve accuracy and minimize round-off errors during # calculations -Unit.define('pound') do |pound| - pound.definition = Unit(Rational(45359237,1e8), 'kg') - pound.aliases = %w{lbs lb lbm pound-mass pound pounds #} +RubyUnits::Unit.define('pound') do |pound| + pound.definition = RubyUnits::Unit.new(Rational(45_359_237, 1e8), 'kg') + pound.aliases = %w[lbs lb lbm pound-mass pound pounds #] +end + +RubyUnits::Unit.define('ounce') do |ounce| + ounce.definition = RubyUnits::Unit.new('1/16 lbs') + ounce.aliases = %w[oz ounce ounces] end -Unit.define('ounce') do |ounce| - ounce.definition = Unit('1/16 lbs') - ounce.aliases = %w{oz ounce ounces} +RubyUnits::Unit.define('gram') do |gram| + gram.definition = RubyUnits::Unit.new('1/1000 kg') + gram.aliases = %w[g gram grams gramme grammes] end -Unit.define('gram') do |gram| - gram.definition = Unit('1/1000 kg') - gram.aliases = %w{g gram grams gramme grammes} +RubyUnits::Unit.define('short-ton') do |ton| + ton.definition = RubyUnits::Unit.new('2000 lbs') + ton.aliases = %w[tn ton tons short-tons] end -Unit.define('short-ton') do |ton| - ton.definition = Unit('2000 lbs') - ton.aliases = %w{ton tn} +RubyUnits::Unit.define('carat') do |carat| + carat.definition = RubyUnits::Unit.new('1/5000 kg') + carat.aliases = %w[ct carat carats] end -Unit.define('carat') do |carat| - carat.definition = Unit('1/5000 kg') - carat.aliases = %w{ct carat carats} +RubyUnits::Unit.define('stone') do |stone| + stone.definition = RubyUnits::Unit.new('14 lbs') + stone.aliases = %w[st stone] end # time -Unit.define('minute') do |min| - min.definition = Unit('60 seconds') - min.aliases = %w{min minute minutes} +RubyUnits::Unit.define('minute') do |min| + min.definition = RubyUnits::Unit.new('60 seconds') + min.aliases = %w[min minute minutes] end -Unit.define('hour') do |hour| - hour.definition = Unit('60 minutes') - hour.aliases = %w{h hr hrs hour hours} +RubyUnits::Unit.define('hour') do |hour| + hour.definition = RubyUnits::Unit.new('60 minutes') + hour.aliases = %w[h hr hrs hour hours] end -Unit.define('day') do |day| - day.definition = Unit('24 hours') - day.aliases = %w{d day days} +RubyUnits::Unit.define('day') do |day| + day.definition = RubyUnits::Unit.new('24 hours') + day.aliases = %w[d day days] end -Unit.define('week') do |week| - week.definition = Unit('7 days') - week.aliases = %w{wk week weeks} +RubyUnits::Unit.define('week') do |week| + week.definition = RubyUnits::Unit.new('7 days') + week.aliases = %w[wk week weeks] end -Unit.define('fortnight') do |fortnight| - fortnight.definition = Unit('2 weeks') - fortnight.aliases = %w{fortnight fortnights} +RubyUnits::Unit.define('fortnight') do |fortnight| + fortnight.definition = RubyUnits::Unit.new('2 weeks') + fortnight.aliases = %w[fortnight fortnights] end -Unit.define('year') do |year| - year.definition = Unit('31556926 seconds') # works out to 365.24219907407405 days - year.aliases = %w{y yr year years annum} +RubyUnits::Unit.define('year') do |year| + year.definition = RubyUnits::Unit.new('31556926 seconds') # works out to 365.24219907407405 days + year.aliases = %w[y yr year years annum] end -Unit.define('decade') do |decade| - decade.definition = Unit('10 years') - decade.aliases = %w{decade decades} +RubyUnits::Unit.define('decade') do |decade| + decade.definition = RubyUnits::Unit.new('10 years') + decade.aliases = %w[decade decades] end -Unit.define('century') do |century| - century.definition = Unit('100 years') - century.aliases = %w{century centuries} +RubyUnits::Unit.define('century') do |century| + century.definition = RubyUnits::Unit.new('100 years') + century.aliases = %w[century centuries] end # area -Unit.define('hectare') do |hectare| - hectare.definition = Unit('10000 m^2') +RubyUnits::Unit.define('hectare') do |hectare| + hectare.definition = RubyUnits::Unit.new('10000 m^2') end -Unit.define('acre') do |acre| - acre.definition = Unit('1 mi')**2 / 640 - acre.aliases = %w{acre acres} +RubyUnits::Unit.define('acre') do |acre| + acre.definition = RubyUnits::Unit.new('1 mi')**2 / 640 + acre.aliases = %w[acre acres] end -Unit.define('sqft') do |sqft| - sqft.definition = Unit('1 ft^2') +RubyUnits::Unit.define('sqft') do |sqft| + sqft.definition = RubyUnits::Unit.new('1 ft^2') end -Unit.define('sqin') do |sqin| - sqin.definition = Unit('1 in^2') +RubyUnits::Unit.define('sqin') do |sqin| + sqin.definition = RubyUnits::Unit.new('1 in^2') end # volume -Unit.define('liter') do |liter| - liter.definition = Unit('1/1000 m^3') - liter.aliases = %w{l L liter liters litre litres} +RubyUnits::Unit.define('liter') do |liter| + liter.definition = RubyUnits::Unit.new('1/1000 m^3') + liter.aliases = %w[l L liter liters litre litres] +end + +RubyUnits::Unit.define('gallon') do |gallon| + gallon.definition = RubyUnits::Unit.new('231 in^3') + gallon.aliases = %w[gal gallon gallons] end -Unit.define('gallon') do |gallon| - gallon.definition = Unit('231 in^3') - gallon.aliases = %w{gal gallon gallons} +RubyUnits::Unit.define('quart') do |quart| + quart.definition = RubyUnits::Unit.new('1/4 gal') + quart.aliases = %w[qt quart quarts] end -Unit.define('quart') do |quart| - quart.definition = Unit('1/4 gal') - quart.aliases = %w{qt quart quarts} +RubyUnits::Unit.define('pint') do |pint| + pint.definition = RubyUnits::Unit.new('1/8 gal') + pint.aliases = %w[pt pint pints] end -Unit.define('pint') do |pint| - pint.definition = Unit('1/8 gal') - pint.aliases = %w{pt pint pints} +RubyUnits::Unit.define('cup') do |cup| + cup.definition = RubyUnits::Unit.new('1/16 gal') + cup.aliases = %w[cu cup cups] end -Unit.define('cup') do |cup| - cup.definition = Unit('1/16 gal') - cup.aliases = %w{cu cup cups} +RubyUnits::Unit.define('fluid-ounce') do |floz| + floz.definition = RubyUnits::Unit.new('1/128 gal') + floz.aliases = %w[floz fluid-ounce fluid-ounces] end -Unit.define('fluid-ounce') do |floz| - floz.definition = Unit('1/128 gal') - floz.aliases = %w{floz fluid-ounce fluid-ounces} +RubyUnits::Unit.define('tablespoon') do |tbsp| + tbsp.definition = RubyUnits::Unit.new('1/2 floz') + tbsp.aliases = %w[tbs tbsp tablespoon tablespoons] end -Unit.define('tablespoon') do |tbsp| - tbsp.definition = Unit('1/2 floz') - tbsp.aliases = %w{tbs tbsp tablespoon tablespoons} +RubyUnits::Unit.define('teaspoon') do |tsp| + tsp.definition = RubyUnits::Unit.new('1/3 tablespoon') + tsp.aliases = %w[tsp teaspoon teaspoons] end -Unit.define('teaspoon') do |tsp| - tsp.definition = Unit('1/3 tablespoon') - tsp.aliases = %w{tsp teaspoon teaspoons} +## +# The board-foot is a specialized unit of measure for the volume of lumber in +# the United States and Canada. It is the volume of a one-foot length of a board +# one foot wide and one inch thick. +# http://en.wikipedia.org/wiki/Board_foot +RubyUnits::Unit.define('bdft') do |bdft| + bdft.definition = RubyUnits::Unit.new('1/12 ft^3') + bdft.aliases = %w[fbm boardfoot boardfeet bf] end # volumetric flow -Unit.define('cfm') do |cfm| - cfm.definition = Unit('1 ft^3/minute') - cfm.aliases = %w{cfm CFM CFPM} +RubyUnits::Unit.define('cfm') do |cfm| + cfm.definition = RubyUnits::Unit.new('1 ft^3/minute') + cfm.aliases = %w[cfm CFM CFPM] end # speed -Unit.define('kph') do |kph| - kph.definition = Unit('1 kilometer/hour') +RubyUnits::Unit.define('kph') do |kph| + kph.definition = RubyUnits::Unit.new('1 kilometer/hour') end -Unit.define('mph') do |mph| - mph.definition = Unit('1 mile/hour') +RubyUnits::Unit.define('mph') do |mph| + mph.definition = RubyUnits::Unit.new('1 mile/hour') end -Unit.define('fps') do |fps| - fps.definition = Unit('1 foot/second') +RubyUnits::Unit.define('fps') do |fps| + fps.definition = RubyUnits::Unit.new('1 foot/second') end -Unit.define('knot') do |knot| - knot.definition = Unit('1 nmi/hour') - knot.aliases = %w{kt kn kts knot knots} +RubyUnits::Unit.define('knot') do |knot| + knot.definition = RubyUnits::Unit.new('1 nmi/hour') + knot.aliases = %w[kt kn kts knot knots] end -Unit.define('gee') do |gee| +RubyUnits::Unit.define('gee') do |gee| # approximated as a rational number to minimize round-off errors - gee.definition = Unit(Rational(196133,20000), 'm/s^2') # equivalent to 9.80665 m/s^2 - gee.aliases = %w{gee standard-gravitation} + gee.definition = RubyUnits::Unit.new(Rational(196_133, 20_000), 'm/s^2') # equivalent to 9.80665 m/s^2 + gee.aliases = %w[gee standard-gravitation] end # temperature differences -Unit.define('newton') do |newton| - newton.definition = Unit('1 kg*m/s^2') - newton.aliases = %w{N newton newtons} +RubyUnits::Unit.define('newton') do |newton| + newton.definition = RubyUnits::Unit.new('1 kg*m/s^2') + newton.aliases = %w[N newton newtons] end -Unit.define('dyne') do |dyne| - dyne.definition = Unit('1/100000 N') - dyne.aliases = %w{dyn dyne} +RubyUnits::Unit.define('dyne') do |dyne| + dyne.definition = RubyUnits::Unit.new('1/100000 N') + dyne.aliases = %w[dyn dyne] end -Unit.define('pound-force') do |lbf| - lbf.definition = Unit('1 lb') * Unit('1 gee') - lbf.aliases = %w{lbf pound-force} +RubyUnits::Unit.define('pound-force') do |lbf| + lbf.definition = RubyUnits::Unit.new('1 lb') * RubyUnits::Unit.new('1 gee') + lbf.aliases = %w[lbf pound-force] end -Unit.define('poundal') do |poundal| - poundal.definition = Unit('1 lb') * Unit('1 ft/s^2') - poundal.aliases = %w{pdl poundal poundals} +RubyUnits::Unit.define('poundal') do |poundal| + poundal.definition = RubyUnits::Unit.new('1 lb') * RubyUnits::Unit.new('1 ft/s^2') + poundal.aliases = %w[pdl poundal poundals] end -temp_convert_factor = Rational(2501999792983609,4503599627370496) # approximates 1/1.8 +temp_convert_factor = Rational(2_501_999_792_983_609, 4_503_599_627_370_496) # approximates 1/1.8 -Unit.define('celsius') do |celsius| - celsius.definition = Unit('1 degK') - celsius.aliases = %w{degC celsius centigrade} +RubyUnits::Unit.define('celsius') do |celsius| + celsius.definition = RubyUnits::Unit.new('1 degK') + celsius.aliases = %w[degC celsius centigrade] end -Unit.define('fahrenheit') do |fahrenheit| - fahrenheit.definition = Unit(temp_convert_factor, 'degK') - fahrenheit.aliases = %w{degF fahrenheit} +RubyUnits::Unit.define('fahrenheit') do |fahrenheit| + fahrenheit.definition = RubyUnits::Unit.new(temp_convert_factor, 'degK') + fahrenheit.aliases = %w[degF fahrenheit] end -Unit.define('rankine') do |rankine| - rankine.definition = Unit('1 degF') - rankine.aliases = %w{degR rankine} +RubyUnits::Unit.define('rankine') do |rankine| + rankine.definition = RubyUnits::Unit.new('1 degF') + rankine.aliases = %w[degR rankine] end -Unit.define('tempC') do |tempC| - tempC.definition = Unit('1 tempK') +RubyUnits::Unit.define('tempC') do |temp_c| + temp_c.definition = RubyUnits::Unit.new('1 tempK') end -Unit.define('tempF') do |tempF| - tempF.definition = Unit(temp_convert_factor, 'tempK') +RubyUnits::Unit.define('tempF') do |temp_f| + temp_f.definition = RubyUnits::Unit.new(temp_convert_factor, 'tempK') end -Unit.define('tempR') do |tempR| - tempR.definition = Unit('1 tempF') +RubyUnits::Unit.define('tempR') do |temp_r| + temp_r.definition = RubyUnits::Unit.new('1 tempF') end # astronomy -speed_of_light = Unit('299792458 m/s') +speed_of_light = RubyUnits::Unit.new('299792458 m/s') -Unit.define('light-second') do |ls| - ls.definition = Unit('1 s') * speed_of_light - ls.aliases = %w{ls lsec light-second} +RubyUnits::Unit.define('light-second') do |ls| + ls.definition = RubyUnits::Unit.new('1 s') * speed_of_light + ls.aliases = %w[ls lsec light-second] end -Unit.define('light-minute') do |lmin| - lmin.definition = Unit('1 min') * speed_of_light - lmin.aliases = %w{lmin light-minute} +RubyUnits::Unit.define('light-minute') do |lmin| + lmin.definition = RubyUnits::Unit.new('1 min') * speed_of_light + lmin.aliases = %w[lmin light-minute] end -Unit.define('light-year') do |ly| - ly.definition = Unit('1 y') * speed_of_light - ly.aliases = %w{ly light-year} +RubyUnits::Unit.define('light-year') do |ly| + ly.definition = RubyUnits::Unit.new('1 y') * speed_of_light + ly.aliases = %w[ly light-year] end -Unit.define('parsec') do |parsec| - parsec.definition = Unit('3.26163626 ly') - parsec.aliases = %w{pc parsec parsecs} +RubyUnits::Unit.define('parsec') do |parsec| + parsec.definition = RubyUnits::Unit.new('3.26163626 ly') + parsec.aliases = %w[pc parsec parsecs] end # once was '149597900000 m' but there appears to be a more accurate estimate according to wikipedia # see http://en.wikipedia.org/wiki/Astronomical_unit -Unit.define('AU') do |au| - au.definition = Unit('149597870700 m') - au.aliases = %w{AU astronomical-unit} +RubyUnits::Unit.define('AU') do |au| + au.definition = RubyUnits::Unit.new('149597870700 m') + au.aliases = %w[AU astronomical-unit] end -Unit.define('redshift') do |red| - red.definition = Unit('1.302773e26 m') - red.aliases = %w{z red-shift} +RubyUnits::Unit.define('redshift') do |red| + red.definition = RubyUnits::Unit.new('1.302773e26 m') + red.aliases = %w[z red-shift] end # mass -Unit.define('slug') do |slug| - slug.definition = Unit('1 lbf*s^2/ft') - slug.aliases = %w{slug slugs} +RubyUnits::Unit.define('slug') do |slug| + slug.definition = RubyUnits::Unit.new('1 lbf*s^2/ft') + slug.aliases = %w[slug slugs] end # pressure -Unit.define('pascal') do |pascal| - pascal.definition = Unit('1 kg/m*s^2') - pascal.aliases = %w{Pa pascal pascals} +RubyUnits::Unit.define('pascal') do |pascal| + pascal.definition = RubyUnits::Unit.new('1 kg/m*s^2') + pascal.aliases = %w[Pa pascal pascals] end -Unit.define('bar') do |bar| - bar.definition = Unit('100 kPa') - bar.aliases = %w{bar bars} +RubyUnits::Unit.define('bar') do |bar| + bar.definition = RubyUnits::Unit.new('100 kPa') + bar.aliases = %w[bar bars] end -Unit.define('atm') do |atm| - atm.definition = Unit('101325 Pa') - atm.aliases = %w{atm ATM atmosphere atmospheres} +RubyUnits::Unit.define('atm') do |atm| + atm.definition = RubyUnits::Unit.new('101325 Pa') + atm.aliases = %w[atm ATM atmosphere atmospheres] end -Unit.define('mmHg') do |mmhg| - density_of_mercury = Unit('7653360911758079/562949953421312 g/cm^3') # 13.5951 g/cm^3 at 0 tempC - mmhg.definition = Unit('1 mm') * Unit('1 gee') * density_of_mercury +RubyUnits::Unit.define('mmHg') do |mmhg| + density_of_mercury = RubyUnits::Unit.new('7653360911758079/562949953421312 g/cm^3') # 13.5951 g/cm^3 at 0 tempC + mmhg.definition = RubyUnits::Unit.new('1 mm') * RubyUnits::Unit.new('1 gee') * density_of_mercury end -Unit.define('inHg') do |inhg| - density_of_mercury = Unit('7653360911758079/562949953421312 g/cm^3') # 13.5951 g/cm^3 at 0 tempC - inhg.definition = Unit('1 in') * Unit('1 gee') * density_of_mercury +RubyUnits::Unit.define('inHg') do |inhg| + density_of_mercury = RubyUnits::Unit.new('7653360911758079/562949953421312 g/cm^3') # 13.5951 g/cm^3 at 0 tempC + inhg.definition = RubyUnits::Unit.new('1 in') * RubyUnits::Unit.new('1 gee') * density_of_mercury end -Unit.define('torr') do |torr| - torr.definition = Unit('1/760 atm') - torr.aliases = %w{Torr torr} +RubyUnits::Unit.define('torr') do |torr| + torr.definition = RubyUnits::Unit.new('1/760 atm') + torr.aliases = %w[Torr torr] end -Unit.define('psi') do |psi| - psi.definition = Unit('1 lbf/in^2') +RubyUnits::Unit.define('psi') do |psi| + psi.definition = RubyUnits::Unit.new('1 lbf/in^2') end -Unit.define('cmh2o') do |cmh2o| - density_of_water = Unit('1 g/cm^3') # at 4 tempC - cmh2o.definition = Unit('1 cm') * Unit('1 gee') * density_of_water - cmh2o.aliases = %w{cmH2O cmh2o cmAq} +RubyUnits::Unit.define('cmh2o') do |cmh2o| + density_of_water = RubyUnits::Unit.new('1 g/cm^3') # at 4 tempC + cmh2o.definition = RubyUnits::Unit.new('1 cm') * RubyUnits::Unit.new('1 gee') * density_of_water + cmh2o.aliases = %w[cmH2O cmh2o cmAq] end -Unit.define('inh2o') do |inh2o| - density_of_water = Unit('1 g/cm^3') # at 4 tempC - inh2o.definition = Unit('1 in') * Unit('1 gee') * density_of_water - inh2o.aliases = %w{inH2O inh2o inAq} +RubyUnits::Unit.define('inh2o') do |inh2o| + density_of_water = RubyUnits::Unit.new('1 g/cm^3') # at 4 tempC + inh2o.definition = RubyUnits::Unit.new('1 in') * RubyUnits::Unit.new('1 gee') * density_of_water + inh2o.aliases = %w[inH2O inh2o inAq] end -#viscosity +# viscosity -Unit.define('poise') do |poise| - poise.definition = Unit('dPa*s') - poise.aliases = %w{P poise} +RubyUnits::Unit.define('poise') do |poise| + poise.definition = RubyUnits::Unit.new('dPa*s') + poise.aliases = %w[P poise] end -Unit.define('stokes') do |stokes| - stokes.definition = Unit('1 cm^2/s') - stokes.aliases = %w{St stokes} +RubyUnits::Unit.define('stokes') do |stokes| + stokes.definition = RubyUnits::Unit.new('1 cm^2/s') + stokes.aliases = %w[St stokes] end # #energy -Unit.define('joule') do |joule| - joule.definition = Unit('1 N*m') - joule.aliases = %w{J joule joules} +RubyUnits::Unit.define('joule') do |joule| + joule.definition = RubyUnits::Unit.new('1 N*m') + joule.aliases = %w[J joule joules] end -Unit.define('erg') do |erg| - erg.definition = Unit('1 g*cm^2/s^2') - erg.aliases = %w{erg ergs} +RubyUnits::Unit.define('erg') do |erg| + erg.definition = RubyUnits::Unit.new('1 g*cm^2/s^2') + erg.aliases = %w[erg ergs] end -#power +# power -Unit.define('watt') do |watt| - watt.definition = Unit('1 N*m/s') - watt.aliases = %w{W Watt watt watts} +RubyUnits::Unit.define('watt') do |watt| + watt.definition = RubyUnits::Unit.new('1 N*m/s') + watt.aliases = %w[W Watt watt watts] end -Unit.define('horsepower') do |hp| - hp.definition = Unit('33000 ft*lbf/min') - hp.aliases = %w{hp horsepower} +RubyUnits::Unit.define('horsepower') do |hp| + hp.definition = RubyUnits::Unit.new('33000 ft*lbf/min') + hp.aliases = %w[hp horsepower] end # energy -Unit.define('btu') do |btu| - btu.definition = Unit('2320092679909671/2199023255552 J') # 1055.056 J --- ISO standard - btu.aliases = %w{Btu btu Btus btus} +RubyUnits::Unit.define('btu') do |btu| + btu.definition = RubyUnits::Unit.new('2320092679909671/2199023255552 J') # 1055.056 J --- ISO standard + btu.aliases = %w[Btu btu Btus btus] end -Unit.define('therm') do |therm| - therm.definition = Unit('100 kBtu') - therm.aliases = %w{thm therm therms Therm} +RubyUnits::Unit.define('therm') do |therm| + therm.definition = RubyUnits::Unit.new('100 kBtu') + therm.aliases = %w[thm therm therms Therm] end # "small" calorie -Unit.define('calorie') do |calorie| - calorie.definition = Unit('4.184 J') - calorie.aliases = %w{cal calorie calories} +RubyUnits::Unit.define('calorie') do |calorie| + calorie.definition = RubyUnits::Unit.new('4.184 J') + calorie.aliases = %w[cal calorie calories] end # "big" calorie -Unit.define('Calorie') do |calorie| - calorie.definition = Unit('1 kcal') - calorie.aliases = %w{Cal Calorie Calories} +RubyUnits::Unit.define('Calorie') do |calorie| + calorie.definition = RubyUnits::Unit.new('1 kcal') + calorie.aliases = %w[Cal Calorie Calories] end -Unit.define('molar') do |molar| - molar.definition = Unit('1 mole/l') - molar.aliases = %w{M molar} +RubyUnits::Unit.define('molar') do |molar| + molar.definition = RubyUnits::Unit.new('1 mole/l') + molar.aliases = %w[M molar] end # potential -Unit.define('volt') do |volt| - volt.definition = Unit('1 W/A') - volt.aliases = %w{V volt volts} +RubyUnits::Unit.define('volt') do |volt| + volt.definition = RubyUnits::Unit.new('1 W/A') + volt.aliases = %w[V volt volts] end # capacitance -Unit.define('farad') do |farad| - farad.definition = Unit('1 A*s/V') - farad.aliases = %w{F farad farads} +RubyUnits::Unit.define('farad') do |farad| + farad.definition = RubyUnits::Unit.new('1 A*s/V') + farad.aliases = %w[F farad farads] end # charge -Unit.define('coulomb') do |coulomb| - coulomb.definition = Unit('1 A*s') - coulomb.aliases = %w{C coulomb coulombs} +RubyUnits::Unit.define('coulomb') do |coulomb| + coulomb.definition = RubyUnits::Unit.new('1 A*s') + coulomb.aliases = %w[C coulomb coulombs] end # conductance -Unit.define('siemens') do |siemens| - siemens.definition = Unit('1 A/V') - siemens.aliases = %w{S siemens} +RubyUnits::Unit.define('siemens') do |siemens| + siemens.definition = RubyUnits::Unit.new('1 A/V') + siemens.aliases = %w[S siemens] end # inductance -Unit.define('henry') do |henry| - henry.definition = Unit('1 J/A^2') - henry.aliases = %w{H henry henries} +RubyUnits::Unit.define('henry') do |henry| + henry.definition = RubyUnits::Unit.new('1 J/A^2') + henry.aliases = %w[H henry henries] end # resistance -Unit.define('ohm') do |ohm| - ohm.definition = Unit('1 V/A') - ohm.aliases = %w{Ohm ohm ohms} +RubyUnits::Unit.define('ohm') do |ohm| + ohm.definition = RubyUnits::Unit.new('1 V/A') + ohm.aliases = %w[Ohm ohm ohms] end -# magnetism +# magnetism -Unit.define('weber') do |weber| - weber.definition = Unit('1 V*s') - weber.aliases = %w{Wb weber webers} +RubyUnits::Unit.define('weber') do |weber| + weber.definition = RubyUnits::Unit.new('1 V*s') + weber.aliases = %w[Wb weber webers] end -Unit.define('tesla') do |tesla| - tesla.definition = Unit('1 V*s/m^2') - tesla.aliases = %w{T tesla teslas} +RubyUnits::Unit.define('tesla') do |tesla| + tesla.definition = RubyUnits::Unit.new('1 V*s/m^2') + tesla.aliases = %w[T tesla teslas] end -Unit.define('gauss') do |gauss| - gauss.definition = Unit('100 microT') - gauss.aliases = %w{G gauss} +RubyUnits::Unit.define('gauss') do |gauss| + gauss.definition = RubyUnits::Unit.new('100 microT') + gauss.aliases = %w[G gauss] end -Unit.define('maxwell') do |maxwell| - maxwell.definition = Unit('1 gauss*cm^2') - maxwell.aliases = %w{Mx maxwell maxwells} +RubyUnits::Unit.define('maxwell') do |maxwell| + maxwell.definition = RubyUnits::Unit.new('1 gauss*cm^2') + maxwell.aliases = %w[Mx maxwell maxwells] end -Unit.define('oersted') do |oersted| - oersted.definition = Unit(250.0/Math::PI, 'A/m') - oersted.aliases = %w{Oe oersted oersteds} +RubyUnits::Unit.define('oersted') do |oersted| + oersted.definition = RubyUnits::Unit.new(250.0 / Math::PI, 'A/m') + oersted.aliases = %w[Oe oersted oersteds] end -#activity -Unit.define('katal') do |katal| - katal.definition = Unit('1 mole/sec') - katal.aliases = %w{kat katal} +# activity +RubyUnits::Unit.define('katal') do |katal| + katal.definition = RubyUnits::Unit.new('1 mole/sec') + katal.aliases = %w[kat katal] end -Unit.define('unit') do |unit| - unit.definition = Unit('1/60 microkatal') - unit.aliases = %w{U enzUnit} +RubyUnits::Unit.define('unit') do |unit| + unit.definition = RubyUnits::Unit.new('1/60 microkatal') + unit.aliases = %w[U enzUnit units] end -#frequency +# frequency -Unit.define('hertz') do |hz| - hz.definition = Unit('1 1/s') - hz.aliases = %w{Hz hertz} +RubyUnits::Unit.define('hertz') do |hz| + hz.definition = RubyUnits::Unit.new('1 1/s') + hz.aliases = %w[Hz hertz] end -#angle -Unit.define('degree') do |deg| - deg.definition = Unit(Math::PI / 180.0, 'radian') - deg.aliases = %w{deg degree degrees} +# angle +RubyUnits::Unit.define('degree') do |deg| + deg.definition = RubyUnits::Unit.new(Math::PI / 180.0, 'radian') + deg.aliases = %w[deg degree degrees] end -Unit.define('grad') do |grad| - grad.definition = Unit(Math::PI / 200.0, 'radian') - grad.aliases = %w{grad gradian grads} +RubyUnits::Unit.define('gon') do |grad| + grad.definition = RubyUnits::Unit.new(Math::PI / 200.0, 'radian') + grad.aliases = %w[gon grad gradian grads] end -#rotation -Unit.define('rotation') do |rotation| - rotation.definition = Unit(2.0*Math::PI, 'radian') +# rotation +RubyUnits::Unit.define('rotation') do |rotation| + rotation.definition = RubyUnits::Unit.new(2.0 * Math::PI, 'radian') end -Unit.define('rpm') do |rpm| - rpm.definition = Unit('1 rotation/min') +RubyUnits::Unit.define('rpm') do |rpm| + rpm.definition = RubyUnits::Unit.new('1 rotation/min') end -#memory -Unit.define('bit') do |bit| - bit.definition = Unit('1/8 byte') - bit.aliases = %w{b bit} +# memory +RubyUnits::Unit.define('bit') do |bit| + bit.definition = RubyUnits::Unit.new('1/8 byte') + bit.aliases = %w[b bit] end -#currency -Unit.define('cents') do |cents| - cents.definition = Unit('1/100 dollar') +# currency +RubyUnits::Unit.define('cents') do |cents| + cents.definition = RubyUnits::Unit.new('1/100 dollar') end -#luminosity -Unit.define('lumen') do |lumen| - lumen.definition = Unit('1 cd*steradian') - lumen.aliases = %w{lm lumen} +# luminosity +RubyUnits::Unit.define('lumen') do |lumen| + lumen.definition = RubyUnits::Unit.new('1 cd*steradian') + lumen.aliases = %w[lm lumen] end -Unit.define('lux') do |lux| - lux.definition = Unit('1 lumen/m^2') +RubyUnits::Unit.define('lux') do |lux| + lux.definition = RubyUnits::Unit.new('1 lumen/m^2') end -#radiation -Unit.define('gray') do |gray| - gray.definition = Unit('1 J/kg') - gray.aliases = %w{Gy gray grays} +# radiation +RubyUnits::Unit.define('gray') do |gray| + gray.definition = RubyUnits::Unit.new('1 J/kg') + gray.aliases = %w[Gy gray grays] end -Unit.define('roentgen') do |roentgen| - roentgen.definition = Unit('2.58e-4 C/kg') - roentgen.aliases = %w{R roentgen} +RubyUnits::Unit.define('roentgen') do |roentgen| + roentgen.definition = RubyUnits::Unit.new('2.58e-4 C/kg') + roentgen.aliases = %w[R roentgen] end -Unit.define('sievert') do |sievert| - sievert.definition = Unit('1 J/kg') - sievert.aliases = %w{Sv sievert sieverts} +RubyUnits::Unit.define('sievert') do |sievert| + sievert.definition = RubyUnits::Unit.new('1 J/kg') + sievert.aliases = %w[Sv sievert sieverts] end -Unit.define('becquerel') do |becquerel| - becquerel.definition = Unit('1 1/s') - becquerel.aliases = %w{Bq becquerel becquerels} +RubyUnits::Unit.define('becquerel') do |becquerel| + becquerel.definition = RubyUnits::Unit.new('1 1/s') + becquerel.aliases = %w[Bq becquerel becquerels] end -Unit.define('curie') do |curie| - curie.definition = Unit('37 GBq') - curie.aliases = %w{Ci curie curies} +RubyUnits::Unit.define('curie') do |curie| + curie.definition = RubyUnits::Unit.new('37 GBq') + curie.aliases = %w[Ci curie curies] end -Unit.define('count') do |count| - count.definition = Unit('1 each') +RubyUnits::Unit.define('count') do |count| + count.definition = RubyUnits::Unit.new('1 each') count.kind = :counting end # rate -Unit.define('cpm') do |cpm| - cpm.definition = Unit('1 count/min') +RubyUnits::Unit.define('cpm') do |cpm| + cpm.definition = RubyUnits::Unit.new('1 count/min') end -Unit.define('dpm') do |dpm| - dpm.definition = Unit('1 count/min') +RubyUnits::Unit.define('dpm') do |dpm| + dpm.definition = RubyUnits::Unit.new('1 count/min') end -Unit.define('bpm') do |bpm| - bpm.definition = Unit('1 count/min') +RubyUnits::Unit.define('bpm') do |bpm| + bpm.definition = RubyUnits::Unit.new('1 count/min') end # misc -Unit.define('dozen') do |dozen| - dozen.definition = Unit('12 each') - dozen.aliases = %w{doz dz dozen} - dozen.kind = :counting +RubyUnits::Unit.define('dozen') do |dozen| + dozen.definition = RubyUnits::Unit.new('12 each') + dozen.aliases = %w[doz dz dozen] + dozen.kind = :counting end -Unit.define('gross') do |gross| - gross.definition = Unit('12 dozen') - gross.aliases = %w{gr gross} - gross.kind = :counting +RubyUnits::Unit.define('gross') do |gross| + gross.definition = RubyUnits::Unit.new('12 dozen') + gross.aliases = %w[gr gross] + gross.kind = :counting end -Unit.define('cell') do |cell| - cell.definition = Unit('1 each') - cell.aliases = %w{cells cell} +RubyUnits::Unit.define('cell') do |cell| + cell.definition = RubyUnits::Unit.new('1 each') + cell.aliases = %w[cells cell] cell.kind = :counting end -Unit.define('base-pair') do |bp| - bp.definition = Unit('1 each') - bp.aliases = %w{bp base-pair} +RubyUnits::Unit.define('base-pair') do |bp| + bp.definition = RubyUnits::Unit.new('1 each') + bp.aliases = %w[bp base-pair] bp.kind = :counting end -Unit.define('nucleotide') do |nt| - nt.definition = Unit('1 each') - nt.aliases = %w{nt} +RubyUnits::Unit.define('nucleotide') do |nt| + nt.definition = RubyUnits::Unit.new('1 each') + nt.aliases = %w[nt] nt.kind = :counting end -Unit.define('molecule') do |molecule| - molecule.definition = Unit('1 each') - molecule.aliases = %w{molecule molecules} +RubyUnits::Unit.define('molecule') do |molecule| + molecule.definition = RubyUnits::Unit.new('1 each') + molecule.aliases = %w[molecule molecules] molecule.kind = :counting end -Unit.define('percent') do |percent| - percent.definition = Unit('1/100') - percent.aliases = %w{% percent} +RubyUnits::Unit.define('percent') do |percent| + percent.definition = RubyUnits::Unit.new('1/100') + percent.aliases = %w[% percent] end -Unit.define('ppm') do |ppm| - ppm.definition = Unit(1) / 1_000_000 +RubyUnits::Unit.define('ppm') do |ppm| + ppm.definition = RubyUnits::Unit.new(1) / 1_000_000 end -Unit.define('ppb') do |ppb| - ppb.definition = Unit(1) / 1_000_000_000 +RubyUnits::Unit.define('ppb') do |ppb| + ppb.definition = RubyUnits::Unit.new(1) / 1_000_000_000 end diff --git a/lib/ruby_units/version.rb b/lib/ruby_units/version.rb index 1d6dee39..b11b0209 100644 --- a/lib/ruby_units/version.rb +++ b/lib/ruby_units/version.rb @@ -1,6 +1,5 @@ -class Unit < Numeric - # Pull the version number from the VERSION file - module Version - STRING = File.read(File.dirname(__FILE__) + "/../../VERSION") +module RubyUnits + class Unit < Numeric + VERSION = '2.3.1'.freeze end -end \ No newline at end of file +end diff --git a/ruby-units.gemspec b/ruby-units.gemspec index 01a25952..391bf1fc 100644 --- a/ruby-units.gemspec +++ b/ruby-units.gemspec @@ -1,84 +1,40 @@ -# Generated by jeweler -# DO NOT EDIT THIS FILE DIRECTLY -# Instead, edit Jeweler::Tasks in RakeFile, and run 'rake gemspec' -# -*- encoding: utf-8 -*- +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) +require 'ruby_units/version' -Gem::Specification.new do |s| - s.name = "ruby-units" - s.version = "1.4.2" +Gem::Specification.new do |spec| + spec.name = 'ruby-units' + spec.version = RubyUnits::Unit::VERSION + spec.authors = ['Kevin Olbrich'] + spec.email = ['kevin.olbrich@gmail.com'] - s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version= - s.authors = ["Kevin Olbrich, Ph.D."] - s.date = "2012-09-16" - s.description = "Provides classes and methods to perform unit math and conversions" - s.email = ["kevin.olbrich+ruby_units@gmail.com"] - s.extra_rdoc_files = [ - "LICENSE.txt", - "README.md", - "TODO" - ] - s.files = [ - "CHANGELOG.txt", - "LICENSE.txt", - "README.md", - "RakeFile", - "TODO", - "VERSION", - "lib/ruby-units.rb", - "lib/ruby_units.rb", - "lib/ruby_units/array.rb", - "lib/ruby_units/cache.rb", - "lib/ruby_units/date.rb", - "lib/ruby_units/definition.rb", - "lib/ruby_units/fixnum.rb", - "lib/ruby_units/math.rb", - "lib/ruby_units/numeric.rb", - "lib/ruby_units/object.rb", - "lib/ruby_units/string.rb", - "lib/ruby_units/time.rb", - "lib/ruby_units/unit.rb", - "lib/ruby_units/unit_definitions.rb", - "lib/ruby_units/unit_definitions/base.rb", - "lib/ruby_units/unit_definitions/prefix.rb", - "lib/ruby_units/unit_definitions/standard.rb", - "lib/ruby_units/version.rb", - "ruby-units.gemspec" - ] - s.homepage = "https://github.com/olbrich/ruby-units" - s.licenses = ["MIT"] - s.post_install_message = "====================\nDeprecation Warning\n====================\n\nSeveral convenience methods that ruby-units added to the string class have\nbeen deprecated in this release. These methods include String#to, String#from, String#ago, String#before and others.\nIf your code relies on these functions, they can be added back by adding this line to your code.\n\nrequire 'ruby-units/string/extras'\n# note that these methods do not play well with Rails, which is one of the reasons they are being removed.\n\nThe extra functions mostly work the same, but will no longer properly handle cases when they are called with strings..\n\n'min'.from(\"4-1-2011\") # => Exception\n\nPass in a Date, Time, or DateTime object to get the expected result.\n\nThey will go away completely in the next release, so it would be a good idea to refactor your code\nto avoid using them. They will also throw deprecation warnings when they are used.\n" - s.require_paths = ["lib"] - s.rubygems_version = "1.8.24" - s.summary = "A class that performs unit conversions and unit math" + spec.required_rubygems_version = '>= 2.0' + spec.required_ruby_version = '>= 2.3' + spec.summary = 'Provides classes and methods to perform unit math and conversions' + spec.description = 'Provides classes and methods to perform unit math and conversions' + spec.homepage = 'https://github.com/olbrich/ruby-units' + spec.license = 'MIT' - if s.respond_to? :specification_version then - s.specification_version = 3 + spec.metadata['homepage_uri'] = spec.homepage + spec.metadata['source_code_uri'] = 'https://github.com/olbrich/ruby-units' + spec.metadata['changelog_uri'] = 'https://github.com/olbrich/ruby-units/releases' - if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then - s.add_development_dependency(%q, ["~> 1.0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, [">= 0"]) - s.add_development_dependency(%q, ["~> 2.5"]) - s.add_development_dependency(%q, [">= 0"]) - else - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 2.5"]) - s.add_dependency(%q, [">= 0"]) - end - else - s.add_dependency(%q, ["~> 1.0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, [">= 0"]) - s.add_dependency(%q, ["~> 2.5"]) - s.add_dependency(%q, [">= 0"]) + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(File.expand_path(__dir__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } end -end + spec.require_paths = ['lib'] + spec.add_development_dependency 'bundler' + spec.add_development_dependency 'guard-rspec' + spec.add_development_dependency 'pry' + spec.add_development_dependency 'rake' + spec.add_development_dependency 'rspec', '~> 3.0' + spec.add_development_dependency 'rubocop', '~> 0.77.0' # match codeclimate + spec.add_development_dependency 'simplecov-html' + spec.add_development_dependency 'simplecov' + spec.add_development_dependency 'terminal-notifier-guard' + spec.add_development_dependency 'terminal-notifier' + spec.add_development_dependency 'wwtd' +end diff --git a/spec/benchmarks/bigdecimal.rb b/spec/benchmarks/bigdecimal.rb new file mode 100644 index 00000000..fd243362 --- /dev/null +++ b/spec/benchmarks/bigdecimal.rb @@ -0,0 +1,41 @@ +require_relative '../spec_helper' +require 'bigdecimal' +require 'bigdecimal/util' +require 'benchmark' +require 'ruby-prof' +a = [ + [2.025, "gal"], + [5.575, "gal"], + [8.975, "gal"], + [1.5, "gal"], + [9, "gal"], + [1.85, "gal"], + [2.25, "gal"], + [1.05, "gal"], + [4.725, "gal"], + [3.55, "gal"], + [4.725, "gal"], + [3.75, "gal"], + [6.275, "gal"], + [0.525, "gal"], + [3.475, "gal"], + [0.85, "gal"] +] + +b = a.map{|ns,nu| Unit.new(ns.to_d, nu)} + +result = RubyProf.profile(merge_fibers: true) do + puts b.reduce(:+) +end + +# print a graph profile to text +printer = RubyProf::GraphPrinter.new(result) +printer.print(STDOUT, {}) + +result = RubyProf.profile(merge_fibers: true) do + puts b.reduce(:-) +end + +# print a graph profile to text +printer = RubyProf::GraphPrinter.new(result) +printer.print(STDOUT, {}) diff --git a/spec/ruby-units/array_spec.rb b/spec/ruby-units/array_spec.rb deleted file mode 100644 index 2a3b7424..00000000 --- a/spec/ruby-units/array_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe Array do - - subject { [1, 'cm'] } - - it {should be_kind_of Array} - it {should respond_to :to_unit} - - specify { subject.to_unit.should be_instance_of Unit} - specify { subject.to_unit.should == "1 cm".to_unit } - specify { subject.to_unit('mm').should == "10 mm".to_unit} - -end \ No newline at end of file diff --git a/spec/ruby-units/bugs_spec.rb b/spec/ruby-units/bugs_spec.rb deleted file mode 100644 index 8b0709b9..00000000 --- a/spec/ruby-units/bugs_spec.rb +++ /dev/null @@ -1,10 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe "Github issue #49" do - let(:a) { Unit("3 cm^3")} - let(:b) { Unit.new(a)} - - it "should subtract a unit properly from one initialized with a unit" do - (b - Unit("1.5 cm^3")).should == Unit("1.5 cm^3") - end -end \ No newline at end of file diff --git a/spec/ruby-units/cache_spec.rb b/spec/ruby-units/cache_spec.rb deleted file mode 100644 index e7048601..00000000 --- a/spec/ruby-units/cache_spec.rb +++ /dev/null @@ -1,31 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe Unit::Cache do - subject { Unit::Cache } - let(:unit) { Unit('1 m') } - - before(:each) do - subject.clear - subject.set("m", unit) - end - - context ".clear" do - it "should clear the cache" do - subject.clear - subject.get('m').should be_nil - end - end - - context ".get" do - it "should retrieve values already in the cache" do - subject.get['m'].should == unit - end - end - - context ".set" do - it "should put a unit into the cache" do - subject.set('kg', Unit('1 kg')) - subject.get['kg'].should == Unit('1 kg') - end - end -end \ No newline at end of file diff --git a/spec/ruby-units/complex_spec.rb b/spec/ruby-units/complex_spec.rb deleted file mode 100644 index 1077de1c..00000000 --- a/spec/ruby-units/complex_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -# Complex numbers are a bit strange -# Technically you can't really compare them using <=>, and ruby 1.9 does not implement this method for them -# so it stands to reason that complex units should also not support :> or :< - -describe Complex do - subject { Complex(1,1) } - it { should respond_to :to_unit } -end - -describe "Complex Unit" do - subject { Complex(1.0, -1.0).to_unit } - - it { should be_instance_of Unit} - it(:scalar) { should be_kind_of Complex } - - it { should == "1-1i".to_unit } - it { should === "1-1i".to_unit } - - if RUBY_VERSION < "1.9" - context "in Ruby < 1.9" do - it "is comparable" do - subject.should > "1+0.5i".to_unit - subject.should < "2+1i".to_unit - end - end - else - context "in Ruby >= 1.9" do - it "is not comparable" do - expect { subject > "1+1i".to_unit }.to raise_error(NoMethodError) - expect { subject < "1+1i".to_unit }.to raise_error(NoMethodError) - end - end - end - -end \ No newline at end of file diff --git a/spec/ruby-units/date_spec.rb b/spec/ruby-units/date_spec.rb deleted file mode 100644 index 77a11371..00000000 --- a/spec/ruby-units/date_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe Date do - subject { Date.new(2011,4,1) } - - it {should be_instance_of Date} - it {should respond_to :to_unit} - it {should respond_to :to_time} - it {should respond_to :to_date} - - specify { (subject + "5 days".unit).should == Date.new(2011,4,6) } - specify { (subject - "5 days".unit).should == Date.new(2011,3,27) } - # 2012 is a leap year... - specify { (subject + "1 year".unit).should == Date.new(2012,3,31) } - specify { (subject - "1 year".unit).should == Date.new(2010,4,1) } -end - -describe "Date Unit" do - - subject { Date.new(2011,4,1).to_unit } - - it { should be_instance_of Unit } - its(:scalar) { should be_kind_of Rational } - its(:units) { should == "d" } - its(:kind) { should == :time } - - specify { (subject + "5 days".unit).should == Date.new(2011,4,6) } - specify { (subject - "5 days".unit).should == Date.new(2011,3,27) } - - specify { expect { subject + Date.new(2011,4,1) }.to raise_error(ArgumentError) } - specify { expect { subject + DateTime.new(2011,4,1,12,00,00) }.to raise_error(ArgumentError) } - specify { expect { subject + Time.parse("2011-04-01 12:00:00") }.to raise_error(ArgumentError) } - - specify { (subject - Date.new(2011,4,1)).should be_zero } - specify { (subject - DateTime.new(2011,4,1,00,00,00)).should be_zero } - specify { expect {(subject - Time.parse("2011-04-01 00:00"))}.to raise_error(ArgumentError) } - specify { (Date.new(2011,4,1) + 1).should == Date.new(2011,4,2)} -end \ No newline at end of file diff --git a/spec/ruby-units/definition_spec.rb b/spec/ruby-units/definition_spec.rb deleted file mode 100644 index 1e0c53df..00000000 --- a/spec/ruby-units/definition_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe "Unit::Definition('eV')" do - subject { - Unit::Definition.new("eV") do |ev| - ev.aliases = ["eV", "electron-volt"] - ev.definition = Unit("1.602E-19 joule") - ev.display_name = "electron-volt" - end - } - - its(:name) {should == ""} - its(:aliases) {should == %w{eV electron-volt}} - its(:scalar) {should == 1.602E-19} - its(:numerator) {should include("", "", "")} - its(:denominator) {should include("", "")} - its(:display_name) {should == "electron-volt"} -end diff --git a/spec/ruby-units/math_spec.rb b/spec/ruby-units/math_spec.rb deleted file mode 100644 index 4ad5b4f8..00000000 --- a/spec/ruby-units/math_spec.rb +++ /dev/null @@ -1,61 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe Math do - - describe "#sqrt" do - specify { Math.sqrt(Unit('1 mm^6')).should == Unit('1 mm^3') } - specify { Math.sqrt(4).should == 2 } - specify { Math.sqrt(Unit("-9 mm^2")).should be_kind_of(Complex) } - end - - if RUBY_VERSION > "1.9" - # cbrt is only defined in Ruby > 1.9 - describe '#cbrt' do - specify { Math.cbrt(Unit('1 mm^6')).should == Unit('1 mm^2') } - specify { Math.cbrt(8).should == 2 } - end - end - - context "Trigonometry functions" do - - context "with '45 deg' unit" do - subject { Unit("45 deg") } - specify { Math.sin(subject).should be_within(0.01).of(0.70710678) } - specify { Math.cos(subject).should be_within(0.01).of(0.70710678) } - specify { Math.tan(subject).should be_within(0.01).of(1) } - specify { Math.sinh(subject).should be_within(0.01).of(0.8686709614860095) } - specify { Math.cosh(subject).should be_within(0.01).of(1.3246090892520057) } - specify { Math.tanh(subject).should be_within(0.01).of(0.6557942026326724) } - end - - context "with 'PI/4 radians' unit" do - subject { Unit((Math::PI/4),'radians') } - specify { Math.sin(subject).should be_within(0.01).of(0.70710678) } - specify { Math.cos(subject).should be_within(0.01).of(0.70710678) } - specify { Math.tan(subject).should be_within(0.01).of(1) } - specify { Math.sinh(subject).should be_within(0.01).of(0.8686709614860095) } - specify { Math.cosh(subject).should be_within(0.01).of(1.3246090892520057) } - specify { Math.tanh(subject).should be_within(0.01).of(0.6557942026326724) } - end - - context "with 'PI/4' continues to work" do - subject { (Math::PI/4) } - specify { Math.sin(subject).should be_within(0.01).of(0.70710678) } - specify { Math.cos(subject).should be_within(0.01).of(0.70710678) } - specify { Math.tan(subject).should be_within(0.01).of(1) } - specify { Math.sinh(subject).should be_within(0.01).of(0.8686709614860095) } - specify { Math.cosh(subject).should be_within(0.01).of(1.3246090892520057) } - specify { Math.tanh(subject).should be_within(0.01).of(0.6557942026326724) } - end - - specify { Math.hypot(Unit("1 m"), Unit("2 m")).should be_within(Unit("0.01 m")).of(Unit("2.23607 m")) } - specify { Math.hypot(Unit("1 m"), Unit("2 ft")).should be_within(Unit("0.01 m")).of(Unit("1.17116 m")) } - specify { Math.hypot(3,4).should == 5} - specify { expect {Math.hypot(Unit("1 m"), Unit("2 lbs")) }.to raise_error(ArgumentError) } - - specify { Math.atan2(Unit("1 m"), Unit("2 m")).should be_within(0.01).of(0.4636476090008061) } - specify { Math.atan2(Unit("1 m"), Unit("2 ft")).should be_within(0.01).of(1.0233478888629426) } - specify { Math.atan2(1,1).should be_within(0.01).of(0.785398163397448)} - specify { expect {Math.atan2(Unit("1 m"), Unit("2 lbs"))}.to raise_error(ArgumentError) } - end -end \ No newline at end of file diff --git a/spec/ruby-units/numeric_spec.rb b/spec/ruby-units/numeric_spec.rb deleted file mode 100644 index b0721852..00000000 --- a/spec/ruby-units/numeric_spec.rb +++ /dev/null @@ -1,12 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -# some rubies return an array of strings for .instance_methods and others return an array of symbols -# so let's stringify them before we compare -describe Numeric do - specify { Float.instance_methods.map {|m| m.to_s}.should include("to_unit") } - specify { Integer.instance_methods.map {|m| m.to_s}.should include("to_unit") } - specify { Fixnum.instance_methods.map {|m| m.to_s}.should include("to_unit") } - specify { Complex.instance_methods.map {|m| m.to_s}.should include("to_unit") } - specify { Bignum.instance_methods.map {|m| m.to_s}.should include("to_unit") } - specify { Rational.instance_methods.map {|m| m.to_s}.should include("to_unit") } -end \ No newline at end of file diff --git a/spec/ruby-units/object_spec.rb b/spec/ruby-units/object_spec.rb deleted file mode 100644 index f4ac0953..00000000 --- a/spec/ruby-units/object_spec.rb +++ /dev/null @@ -1,9 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe Object do - specify { Unit('1 mm').should be_instance_of Unit} - specify { U('1 mm').should be_instance_of Unit} - specify { u('1 mm').should be_instance_of Unit} - specify { (Unit(0) + Unit(0)).should be_instance_of Unit} - specify { (Unit(0) - Unit(0)).should be_instance_of Unit} -end \ No newline at end of file diff --git a/spec/ruby-units/range_spec.rb b/spec/ruby-units/range_spec.rb deleted file mode 100644 index bd486b41..00000000 --- a/spec/ruby-units/range_spec.rb +++ /dev/null @@ -1,16 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe "Range" do - - context "of integer units" do - subject { (Unit('1 mm')..Unit('3 mm')) } - it { should include(Unit('2 mm')) } - its(:to_a) { should == [ Unit('1 mm'), Unit('2 mm'), Unit('3 mm') ] } - end - - context "of floating point units" do - subject { (Unit('1.5 mm')..Unit('3.5 mm')) } - it { should include(Unit('2.0 mm')) } - specify { expect { subject.to_a }.to raise_exception(ArgumentError)} - end -end \ No newline at end of file diff --git a/spec/ruby-units/string_spec.rb b/spec/ruby-units/string_spec.rb deleted file mode 100644 index bb893445..00000000 --- a/spec/ruby-units/string_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe String do - context "Unit creation from strings" do - specify { "1 mm".to_unit.should be_instance_of Unit } - specify { "1 mm".unit.should be_instance_of Unit } - specify { "1 mm".u.should be_instance_of Unit } - specify { "1 m".convert_to("ft").should be_within(Unit("0.01 ft")).of Unit("3.28084 ft") } - end - - context "output format" do - subject { Unit("1.23456 m/s^2") } - specify { ("" % subject).should == ""} - specify { ("%0.2f" % subject).should == "1.23 m/s^2"} - specify { ("%0.2f km/h^2" % subject).should == "15999.90 km/h^2"} - specify { ("km/h^2" % subject).should == "15999.9 km/h^2"} - specify { ("%H:%M:%S" % Unit("1.5 h")).should == "01:30:00"} - end - -end \ No newline at end of file diff --git a/spec/ruby-units/temperature_spec.rb b/spec/ruby-units/temperature_spec.rb deleted file mode 100644 index 4836f96b..00000000 --- a/spec/ruby-units/temperature_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe 'temperatures' do - describe 'redfine display name' do - before(:all) do - Unit.redefine!("tempC") do |c| - c.aliases = %w{tC tempC} - c.display_name = "tC" - end - - Unit.redefine!("tempF") do |f| - f.aliases = %w{tF tempF} - f.display_name = "tF" - end - - Unit.redefine!("tempR") do |f| - f.aliases = %w{tR tempR} - f.display_name = "tR" - end - - Unit.redefine!("tempK") do |f| - f.aliases = %w{tK tempK} - f.display_name = "tK" - end - end - - after(:all) do - #define the temp units back to normal - Unit.define("tempK") do |unit| - unit.scalar = 1 - unit.numerator = %w{} - unit.aliases = %w{tempK} - unit.kind = :temperature - end - - Unit.define('tempC') do |tempC| - tempC.definition = Unit('1 tempK') - end - - temp_convert_factor = Rational(2501999792983609,4503599627370496) # approximates 1/1.8 - - Unit.define('tempF') do |tempF| - tempF.definition = Unit(temp_convert_factor, 'tempK') - end - - Unit.define('tempR') do |tempR| - tempR.definition = Unit('1 tempF') - end - end - - describe "Unit('100 tC')" do - subject {Unit("100 tC")} - its(:scalar) {should be_within(0.001).of 100} - its(:units) {should == "tC"} - its(:kind) {should == :temperature} - it {should be_temperature} - it {should be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_within(Unit("0.01 degK")).of Unit("373.15 tempK")} - its(:temperature_scale) {should == "degC"} - end - - context "between temperature scales" do - # note that 'temp' units are for temperature readings on a scale, while 'deg' units are used to represent - # differences between temperatures, offsets, or other differential temperatures. - - specify { Unit("100 tC").should be_within(Unit("0.001 degK")).of(Unit("373.15 tempK")) } - specify { Unit("0 tC").should be_within(Unit("0.001 degK")).of(Unit("273.15 tempK")) } - specify { Unit("37 tC").should be_within(Unit("0.01 degK")).of(Unit("310.15 tempK"))} - specify { Unit("-273.15 tC").should == Unit("0 tempK") } - - specify { Unit("212 tF").should be_within(Unit("0.001 degK")).of(Unit("373.15 tempK")) } - specify { Unit("32 tF").should be_within(Unit("0.001 degK")).of(Unit("273.15 tempK")) } - specify { Unit("98.6 tF").should be_within(Unit("0.01 degK")).of(Unit("310.15 tempK"))} - specify { Unit("-459.67 tF").should == Unit("0 tempK") } - - specify { Unit("671.67 tR").should be_within(Unit("0.001 degK")).of(Unit("373.15 tempK")) } - specify { Unit("491.67 tR").should be_within(Unit("0.001 degK")).of(Unit("273.15 tempK")) } - specify { Unit("558.27 tR").should be_within(Unit("0.01 degK")).of(Unit("310.15 tempK"))} - specify { Unit("0 tR").should == Unit("0 tempK") } - - specify { Unit("100 tK").convert_to("tempC").should be_within(U"0.01 degC").of(Unit("-173.15 tempC"))} - specify { Unit("100 tK").convert_to("tempF").should be_within(U"0.01 degF").of(Unit("-279.67 tempF"))} - specify { Unit("100 tK").convert_to("tempR").should be_within(U"0.01 degR").of(Unit("180 tempR"))} - end - - - - - end -end - \ No newline at end of file diff --git a/spec/ruby-units/time_spec.rb b/spec/ruby-units/time_spec.rb deleted file mode 100644 index 019cd993..00000000 --- a/spec/ruby-units/time_spec.rb +++ /dev/null @@ -1,45 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe Time do - let(:now) { Time.at(1303656390) } - before(:each) do - Time.stub(:now).and_return(now) - end - - context ".at" do - subject { Date.new(2011,4,1).to_unit } - specify { Time.at(subject - Date.new(1970,1,1)).getutc.strftime("%D %T").should == "04/01/11 00:00:00"} - specify { Time.at(subject - Date.new(1970,1,1), 500).usec.should == 500} - end - - context ".in" do - specify { Time.in("5 min").should be_a Time} - specify { Time.in("5 min").should > Time.now} - end - - context '#to_date' do - subject { Time.parse("2012-01-31 11:59:59") } - specify { subject.to_date.to_s.should == "2012-01-31" } - specify { (subject+1).to_date.to_s.should == "2012-01-31" } - end - - context '#to_unit' do - subject { now } - its(:to_unit) { should be_an_instance_of(Unit) } - its('to_unit.units') { should == "s" } - specify { subject.to_unit('h').kind.should == :time} - specify { subject.to_unit('h').units.should == 'h'} - end - - context 'addition (+)' do - specify { (Time.now + 1).should == Time.at(1303656390 + 1)} - specify { (Time.now + Unit("10 min")).should == Time.at(1303656390 + 600)} - end - - context 'subtraction (-)' do - specify { (Time.now - 1).should == Time.at(1303656390 - 1)} - specify { (Time.now - Unit("10 min")).should == Time.at(1303656390 - 600)} - specify { (Time.now - Unit("150 years")).should == Time.parse("1861-04-24 09:46:30 -0500")} - end - -end diff --git a/spec/ruby-units/unit_spec.rb b/spec/ruby-units/unit_spec.rb deleted file mode 100644 index dc08adb0..00000000 --- a/spec/ruby-units/unit_spec.rb +++ /dev/null @@ -1,1396 +0,0 @@ -require File.dirname(__FILE__) + '/../spec_helper' - -describe Unit.base_units do - it {should be_a Array} - it {should have(14).elements} - %w{kilogram meter second ampere degK tempK mole candela each dollar steradian radian decibel byte}.each do |u| - it {should include(Unit(u))} - end -end - - -describe "Create some simple units" do - - # zero string - describe Unit("0") do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should === 0} - its(:scalar) {should be_an Integer} - its(:units) {should be_empty} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should be_unitless} - it {should be_zero} - its(:base) {should == subject} - end - - # non-zero string - describe Unit("1") do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should === 1} - its(:scalar) {should be_an Integer} - its(:units) {should be_empty} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should be_unitless} - it {should_not be_zero} - its(:base) {should == subject} - end - - # numeric - describe Unit(1) do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should === 1} - its(:scalar) {should be_an Integer} - its(:units) {should be_empty} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should be_unitless} - it {should_not be_zero} - its(:base) {should == subject} - end - - # rational - describe Unit(Rational(1,2)) do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should === Rational(1,2)} - its(:scalar) {should be_a Rational} - its(:units) {should be_empty} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should be_unitless} - it {should_not be_zero} - its(:base) {should == subject} - end - - # float - describe Unit(0.5) do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should === 0.5} - its(:scalar) {should be_a Float} - its(:units) {should be_empty} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should be_unitless} - it {should_not be_zero} - its(:base) {should == subject} - end - - # complex - describe Unit(Complex(1,1)) do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should === Complex(1,1)} - its(:scalar) {should be_a Complex} - its(:units) {should be_empty} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should be_unitless} - it {should_not be_zero} - its(:base) {should == subject} - end - - describe Unit("1+1i m") do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should === Complex(1,1)} - its(:scalar) {should be_a Complex} - its(:units) {should == "m"} - its(:kind) {should == :length} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should == subject} - end - - # scalar and unit - describe Unit("1 mm") do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should == 1} - its(:scalar) {should be_an Integer} - its(:units) {should == "mm"} - its(:kind) {should == :length} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should == Unit("0.001 m")} - end - - # with a zero power - describe Unit("1 m^0") do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should == 1} - its(:scalar) {should be_an Integer} - its(:units) {should == ""} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should be_unitless} - it {should_not be_zero} - its(:base) {should == Unit("1")} - end - - # unit only - describe Unit("mm") do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should == 1} - its(:scalar) {should be_an Integer} - its(:units) {should == "mm"} - its(:kind) {should == :length} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should == Unit("0.001 m")} - end - - # Compound unit - describe Unit("1 N*m") do - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should == 1} - its(:scalar) {should be_an Integer} - its(:units) {should == "N*m"} - its(:kind) {should == :energy} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should == Unit("1 kg*m^2/s^2")} - end - - # scalar and unit with powers - describe Unit("10 m/s^2") do - it {should be_an_instance_of Unit} - its(:scalar) {should == 10} - its(:scalar) {should be_an Integer} - its(:units) {should == "m/s^2"} - its(:kind) {should == :acceleration} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should == Unit("10 m/s^2")} - end - - # feet/in form - describe Unit("5ft 6in") do - it {should be_an_instance_of Unit} - its(:scalar) {should == 5.5} - its(:units) {should == "ft"} - its(:kind) {should == :length} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_within(Unit("0.01 m")).of Unit("1.6764 m")} - specify { subject.to_s(:ft).should == %{5'6"} } - end - - # pound/ounces form - describe Unit("6lbs 5oz") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_within(0.001).of 6.312} - its(:units) {should == "lbs"} - its(:kind) {should == :mass} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_within(Unit("0.01 kg")).of Unit("2.8633 kg")} - specify { subject.to_s(:lbs).should == "6 lbs, 5 oz" } - end - - # temperature - describe Unit("100 tempC") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_within(0.001).of 100} - its(:units) {should == "tempC"} - its(:kind) {should == :temperature} - it {should be_temperature} - it {should be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_within(Unit("0.01 degK")).of Unit("373.15 tempK")} - its(:temperature_scale) {should == "degC"} - end - - # Time - describe Unit(Time.now) do - it {should be_an_instance_of Unit} - its(:scalar) {should be_a(Numeric)} - its(:units) {should == "s"} - its(:kind) {should == :time} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a(Numeric)} - its(:temperature_scale) {should be_nil} - end - - # degrees - describe Unit("100 degC") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_within(0.001).of 100} - its(:units) {should == "degC"} - its(:kind) {should == :temperature} - it {should_not be_temperature} - it {should be_degree} - it {should_not be_base} - it {should_not be_unitless} - its(:base) {should be_within(Unit("0.01 degK")).of Unit("100 degK")} - end - - # percent - describe Unit("75%") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:units) {should == "%"} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a(Numeric)} - its(:temperature_scale) {should be_nil} - end - - # angle - describe Unit("180 deg") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_a Numeric} - its(:units) {should == "deg"} - its(:kind) {should == :angle} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a(Numeric)} - its(:temperature_scale) {should be_nil} - end - - # radians - describe Unit("1 radian") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_a Numeric} - its(:units) {should == "rad"} - its(:kind) {should == :angle} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - # counting - describe Unit("12 dozen") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:units) {should == "doz"} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - # rational scalar with unit - describe Unit("1/2 kg") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Rational} - its(:units) {should == "kg"} - its(:kind) {should == :mass} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - # rational scalar with compound unit - describe Unit("1/2 kg/m") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Rational} - its(:units) {should == "kg/m"} - its(:kind) {should be_nil} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - # time string - describe Unit("1:23:45,200") do - it {should be_an_instance_of Unit} - it {should == Unit("1 h") + Unit("23 min") + Unit("45 seconds") + Unit("200 usec")} - its(:scalar) {should be_an Rational} - its(:units) {should == "h"} - its(:kind) {should == :time} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - # also '1 hours as minutes' - # '1 hour to minutes' - describe Unit.parse("1 hour in minutes") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:units) {should == "min"} - its(:kind) {should == :time} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - # funky unit - describe Unit("1 attoparsec/microfortnight") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:units) {should == "apc/ufortnight"} - its(:kind) {should == :speed} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - it { subject.convert_to("in/s").should be_within(Unit("0.0001 in/s")).of(Unit("1.0043269330917 in/s"))} - end - - # Farads - describe Unit("1 F") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:units) {should == "F"} - its(:kind) {should == :capacitance} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - describe Unit("1 m^2 s^-2") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:units) {should == "m^2/s^2"} - its(:kind) {should == :radiation} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - describe Unit(1,"m^2","s^2") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:units) {should == "m^2/s^2"} - its(:kind) {should == :radiation} - it {should_not be_temperature} - it {should_not be_degree} - it {should be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - #scientific notation - describe Unit("1e6 cells") do - it {should be_an_instance_of Unit} - its(:scalar) {should be_an Integer} - its(:scalar) {should == 1e6 } - its(:units) {should == "cells"} - its(:kind) {should == :unitless} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should be_a Numeric} - its(:temperature_scale) {should be_nil} - end - - #could be m*m - describe Unit("1 mm") do - its(:kind) {should == :length} - end - - #could be centi-day - describe Unit("1 cd") do - its(:kind) {should == :luminous_power} - end - - # could be milli-inch - describe Unit("1 min") do - its(:kind) {should == :time} - end - - #could be femto-tons - describe Unit("1 ft") do - its(:kind) {should == :length} - end - - #could be deci-ounce - describe Unit("1 doz") do - its(:kind) {should == :unitless} - end - - # create with another unit - describe 10.unit(Unit("1 mm")) do - its(:units) {should == "mm"} - its(:scalar) {should == 10} - end - - #explicit create - describe Unit("1 /") do - its(:kind) {should == :speed} - its(:units) {should == "m/s"} - end - -end - -describe "Unit handles attempts to create bad units" do - specify "no empty strings" do - expect {Unit("")}.to raise_error(ArgumentError,"No Unit Specified") - end - - specify "no blank strings" do - expect {Unit(" ")}.to raise_error(ArgumentError,"No Unit Specified") - end - - specify "no strings with tabs" do - expect {Unit("\t")}.to raise_error(ArgumentError,"No Unit Specified") - end - - specify "no strings with newlines" do - expect {Unit("\n")}.to raise_error(ArgumentError,"No Unit Specified") - end - - specify "no double slashes" do - expect {Unit("3 s/s/ft")}.to raise_error(ArgumentError,/Unit not recognized/) - end - - specify "no pipes or commas" do - expect {Unit("3 s**2|,s**2")}.to raise_error(ArgumentError,/Unit not recognized/) - end - - specify "no multiple spaces" do - expect {Unit("3 s**2 4s s**2")}.to raise_error(ArgumentError,/Unit not recognized/) - end - - specify "no exponentiation of numbers" do - expect {Unit("3 s 5^6")}.to raise_error(ArgumentError,/Unit not recognized/) - end - - specify "no strings that don't specify a valid unit" do - expect {Unit("random string")}.to raise_error(ArgumentError,"'random string' Unit not recognized") - end - - specify "no unhandled classes" do - expect {Unit(STDIN)}.to raise_error(ArgumentError,"Invalid Unit Format") - end - - specify "no undefined units" do - expect {Unit("1 mFoo")}.to raise_error(ArgumentError,"'1 mFoo' Unit not recognized") - expect {Unit("1 second/mFoo")}.to raise_error(ArgumentError,"'1 second/mFoo' Unit not recognized") - end - - specify "no units with powers greater than 19" do - expect {Unit("1 m^20")}.to raise_error(ArgumentError, "Power out of range (-20 < net power of a unit < 20)") - end - - specify "no units with powers less than 19" do - expect {Unit("1 m^-20")}.to raise_error(ArgumentError, "Power out of range (-20 < net power of a unit < 20)") - end - - specify "no temperatures less than absolute zero" do - expect {Unit("-100 tempK")}.to raise_error(ArgumentError,"Temperatures must not be less than absolute zero") - expect {Unit("-100 tempR")}.to raise_error(ArgumentError,"Temperatures must not be less than absolute zero") - expect {Unit("-500/9 tempR")}.to raise_error(ArgumentError,"Temperatures must not be less than absolute zero") - end - - specify "no nil scalar" do - expect {Unit(nil, "feet")}.to raise_error(ArgumentError, "Invalid Unit Format") - expect {Unit(nil, "feet", "min")}.to raise_error(ArgumentError, "Invalid Unit Format") - end - -end - -describe Unit do - it "is a subclass of Numeric" do - described_class.should < Numeric - end - - it "is Comparable" do - described_class.should < Comparable - end - - describe "#defined?" do - it "should return true when asked about a defined unit" do - Unit.defined?("meter").should be_true - end - - it "should return true when asked about an alias for a unit" do - Unit.defined?("m").should be_true - end - - it "should return false when asked about a unit that is not defined" do - Unit.defined?("doohickey").should be_false - end - end - - describe '#to_yaml' do - subject { Unit('1 mm') } - its(:to_yaml) {should =~ /--- !ruby\/object:Unit/ } - end - - describe "#definition" do - context "The requested unit is defined" do - before(:each) do - @definition = Unit.definition('mph') - end - - it "should return a Unit::Definition" do - @definition.should be_instance_of(Unit::Definition) - end - - specify { @definition.name.should == ""} - specify { @definition.aliases.should == %w{mph}} - specify { @definition.numerator.should == [''] } - specify { @definition.denominator.should == [''] } - specify { @definition.kind.should == :speed } - specify { @definition.scalar.should === 0.44704} - end - - context "The requested unit is not defined" do - it "should return nil" do - Unit.definition("doohickey").should be_nil - end - end - end - - describe "#define" do - describe "a new unit" do - before(:each) do - @jiffy = Unit.define("jiffy") do |jiffy| - jiffy.scalar = (1/100) - jiffy.aliases = %w{jif} - jiffy.numerator = [""] - jiffy.kind = :time - end - end - - after(:each) do - Unit.undefine!('jiffy') - end - - describe "Unit('1e6 jiffy')" do - # do this because the unit is not defined at the time this file is parsed, so it fails - subject {Unit("1e6 jiffy")} - - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should == 1e6} - its(:scalar) {should be_an Integer} - its(:units) {should == "jif"} - its(:kind) {should == :time} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - its(:base) {should == Unit("10000 s")} - end - - it "should register the new unit" do - Unit.defined?('jiffy').should be_true - end - end - - describe "an existing unit again" do - before(:each) do - @cups = Unit.definition('cup') - @original_display_name = @cups.display_name - @cups.display_name = "cupz" - Unit.define(@cups) - end - - after(:each) do - Unit.redefine!("cup") do |cup| - cup.display_name = @original_display_name - end - end - - describe "Unit('1 cup')" do - # do this because the unit is going to be redefined - subject {Unit("1 cup")} - - it {should be_a Numeric} - it {should be_an_instance_of Unit} - its(:scalar) {should == 1} - its(:scalar) {should be_an Integer} - its(:units) {should == "cupz"} - its(:kind) {should == :volume} - it {should_not be_temperature} - it {should_not be_degree} - it {should_not be_base} - it {should_not be_unitless} - it {should_not be_zero} - end - - end - - end - - describe '#redefine!' do - before(:each) do - @jiffy = Unit.define("jiffy") do |jiffy| - jiffy.scalar = (1/100) - jiffy.aliases = %w{jif} - jiffy.numerator = [""] - jiffy.kind = :time - end - - Unit.redefine!('jiffy') do |jiffy| - jiffy.scalar = (1/1000) - end - end - - after(:each) do - Unit.undefine!("jiffy") - end - - specify { Unit('1 jiffy').to_base.scalar.should == (1/1000) } - end - - describe '#undefine!' do - before(:each) do - @jiffy = Unit.define("jiffy") do |jiffy| - jiffy.scalar = (1/100) - jiffy.aliases = %w{jif} - jiffy.numerator = [""] - jiffy.kind = :time - end - Unit.undefine!("jiffy") - end - - specify "the unit should be undefined" do - Unit.defined?('jiffy').should be_false - end - - specify "attempting to use an undefined unit fails" do - expect { Unit("1 jiffy") }.to raise_exception(ArgumentError) - end - - it "should return true when undefining an unknown unit" do - Unit.defined?("unknown").should be_false - Unit.undefine!("unknown").should be_true - end - - end - - describe '#clone' do - subject { Unit('1 mm') } - its(:clone) {should === subject} - end -end - -describe "Unit Comparisons" do - context "Unit should detect if two units are 'compatible' (i.e., can be converted into each other)" do - specify { Unit("1 ft").should =~ Unit('1 m')} - specify { Unit("1 ft").should =~ "m"} - specify { Unit("1 ft").should be_compatible_with Unit('1 m')} - specify { Unit("1 ft").should be_compatible_with "m"} - specify { Unit("1 m").should be_compatible_with Unit('1 kg*m/kg')} - specify { Unit("1 ft").should_not =~ Unit('1 kg')} - specify { Unit("1 ft").should_not be_compatible_with Unit('1 kg')} - specify { Unit("1 ft").should_not be_compatible_with nil} - end - - context "Equality" do - - context "with uncoercable objects" do - specify { Unit("1 mm").should_not == nil } - end - - context "units of same kind" do - specify { Unit("1000 m").should == Unit('1 km')} - specify { Unit("100 m").should_not == Unit('1 km')} - specify { Unit("1 m").should == Unit('100 cm')} - end - - context "units of incompatible types" do - specify { Unit("1 m").should_not == Unit("1 kg")} - end - - context "units with a zero scalar are equal" do - specify {Unit("0 m").should == Unit("0 s")} - specify {Unit("0 m").should == Unit("0 kg")} - - context "except for temperature units" do - specify {Unit("0 tempK").should == Unit("0 m")} - specify {Unit("0 tempR").should == Unit("0 m")} - specify {Unit("0 tempC").should_not == Unit("0 m")} - specify {Unit("0 tempF").should_not == Unit("0 m")} - end - end - end - - context "Equivalence" do - context "units and scalars are the exactly the same" do - specify { Unit("1 m").should === Unit("1 m")} - specify { Unit("1 m").should be_same Unit("1 m")} - specify { Unit("1 m").should be_same_as Unit("1 m")} - end - - context "units are compatible but not identical" do - specify { Unit("1000 m").should_not === Unit("1 km")} - specify { Unit("1000 m").should_not be_same Unit("1 km")} - specify { Unit("1000 m").should_not be_same_as Unit("1 km")} - end - - context "units are not compatible" do - specify { Unit("1000 m").should_not === Unit("1 hour")} - specify { Unit("1000 m").should_not be_same Unit("1 hour")} - specify { Unit("1000 m").should_not be_same_as Unit("1 hour")} - end - - context "scalars are different" do - specify { Unit("1 m").should_not === Unit("2 m")} - specify { Unit("1 m").should_not be_same Unit("2 m")} - specify { Unit("1 m").should_not be_same_as Unit("2 m")} - end - - specify { Unit("1 m").should_not === nil} - end - - context "Comparisons" do - context "compatible units can be compared" do - specify { Unit("1 m").should < Unit("2 m")} - specify { Unit("2 m").should > Unit("1 m")} - specify { Unit("1 m").should < Unit("1 mi")} - specify { Unit("2 m").should > Unit("1 ft")} - specify { Unit("70 tempF").should > Unit("10 degC")} - specify { Unit("1 m").should > 0 } - specify { expect { Unit("1 m").should_not > nil}.to raise_error(ArgumentError, /comparison of Unit with (nil failed|NilClass)/) } - end - - context "incompatible units cannot be compared" do - specify { expect { Unit("1 m") < Unit("1 liter")}.to raise_error(ArgumentError,"Incompatible Units (m !~ l)")} - specify { expect { Unit("1 kg") > Unit("60 mph")}.to raise_error(ArgumentError,"Incompatible Units (kg !~ mph)")} - end - end - -end - -describe "Unit Conversions" do - - context "between compatible units" do - specify { Unit("1 s").convert_to("ns").should == Unit("1e9 ns")} - specify { Unit("1 s").convert_to("ns").should == Unit("1e9 ns")} - specify { (Unit("1 s") >> "ns").should == Unit("1e9 ns")} - - specify { Unit("1 m").convert_to(Unit("ft")).should be_within(Unit("0.001 ft")).of(Unit("3.28084 ft"))} - end - - context "between incompatible units" do - specify { expect { Unit("1 s").convert_to("m")}.to raise_error(ArgumentError,"Incompatible Units")} - end - - context "given bad input" do - specify { expect { Unit("1 m").convert_to("random string")}.to raise_error(ArgumentError,"'random string' Unit not recognized")} - specify { expect { Unit("1 m").convert_to(STDOUT)}.to raise_error(ArgumentError,"Unknown target units")} - end - - context "between temperature scales" do - # note that 'temp' units are for temperature readings on a scale, while 'deg' units are used to represent - # differences between temperatures, offsets, or other differential temperatures. - - specify { Unit("100 tempC").should be_within(Unit("0.001 degK")).of(Unit("373.15 tempK")) } - specify { Unit("0 tempC").should be_within(Unit("0.001 degK")).of(Unit("273.15 tempK")) } - specify { Unit("37 tempC").should be_within(Unit("0.01 degK")).of(Unit("310.15 tempK"))} - specify { Unit("-273.15 tempC").should == Unit("0 tempK") } - - specify { Unit("212 tempF").should be_within(Unit("0.001 degK")).of(Unit("373.15 tempK")) } - specify { Unit("32 tempF").should be_within(Unit("0.001 degK")).of(Unit("273.15 tempK")) } - specify { Unit("98.6 tempF").should be_within(Unit("0.01 degK")).of(Unit("310.15 tempK"))} - specify { Unit("-459.67 tempF").should == Unit("0 tempK") } - - specify { Unit("671.67 tempR").should be_within(Unit("0.001 degK")).of(Unit("373.15 tempK")) } - specify { Unit("491.67 tempR").should be_within(Unit("0.001 degK")).of(Unit("273.15 tempK")) } - specify { Unit("558.27 tempR").should be_within(Unit("0.01 degK")).of(Unit("310.15 tempK"))} - specify { Unit("0 tempR").should == Unit("0 tempK") } - - specify { Unit("100 tempK").convert_to("tempC").should be_within(U"0.01 degC").of(Unit("-173.15 tempC"))} - specify { Unit("100 tempK").convert_to("tempF").should be_within(U"0.01 degF").of(Unit("-279.67 tempF"))} - specify { Unit("100 tempK").convert_to("tempR").should be_within(U"0.01 degR").of(Unit("180 tempR"))} - - specify { Unit("1 degC").should == Unit("1 degK")} - specify { Unit("1 degF").should == Unit("1 degR")} - specify { Unit("1 degC").should == Unit("1.8 degR")} - specify { Unit("1 degF").should be_within(Unit("0.001 degK")).of(Unit("0.5555 degK"))} - end - - context "reported bugs" do - specify { (Unit("189 Mtonne") * Unit("1189 g/tonne")).should == Unit("224721 tonne") } - specify { (Unit("189 Mtonne") * Unit("1189 g/tonne")).convert_to("tonne").should == Unit("224721 tonne") } - end - - describe "Foot-inch conversions" do - [ - ["76 in", %Q{6'4"}], - ["77 in", %Q{6'5"}], - ["78 in", %Q{6'6"}], - ["79 in", %Q{6'7"}], - ["80 in", %Q{6'8"}], - ["87 in", %Q{7'3"}], - ["88 in", %Q{7'4"}], - ["89 in", %Q{7'5"}] - ].each do |inches, feet| - specify { Unit(inches).convert_to("ft").should == Unit(feet)} - specify { Unit(inches).to_s(:ft).should == feet} - end - end - - describe "pound-ounce conversions" do - [ - ["76 oz", "4 lbs, 12 oz"], - ["77 oz", "4 lbs, 13 oz"], - ["78 oz", "4 lbs, 14 oz"], - ["79 oz", "4 lbs, 15 oz"], - ["80 oz", "5 lbs, 0 oz"], - ["87 oz", "5 lbs, 7 oz"], - ["88 oz", "5 lbs, 8 oz"], - ["89 oz", "5 lbs, 9 oz"] - ].each do |ounces, pounds| - specify { Unit(ounces).convert_to("lbs").should == Unit(pounds)} - specify { Unit(ounces).to_s(:lbs).should == pounds} - end - end -end - -describe "Unit Math" do - context "operators:" do - context "addition (+)" do - context "between compatible units" do - specify { (Unit("0 m") + Unit("10 m")).should == Unit("10 m")} - specify { (Unit("5 kg") + Unit("10 kg")).should == Unit("15 kg")} - end - - context "between a zero unit and another unit" do - specify { (Unit("0 kg") + Unit("10 m")).should == Unit("10 m")} - specify { (Unit("0 m") + Unit("10 kg")).should == Unit("10 kg")} - end - - context "between incompatible units" do - specify { expect {Unit("10 kg") + Unit("10 m")}.to raise_error(ArgumentError)} - specify { expect {Unit("10 m") + Unit("10 kg")}.to raise_error(ArgumentError)} - specify { expect {Unit("10 m") + nil}.to raise_error(ArgumentError)} - end - - context "a number from a unit" do - specify { expect { Unit("10 kg") + 1 }.to raise_error(ArgumentError)} - specify { expect { 10 + Unit("10 kg") }.to raise_error(ArgumentError)} - end - - context "between a unit and coerceable types" do - specify { (Unit('10 kg') + %w{1 kg}).should == Unit('11 kg') } - specify { (Unit('10 kg') + "1 kg").should == Unit('11 kg') } - end - - context "between two temperatures" do - specify { expect {(Unit("100 tempK") + Unit("100 tempK"))}.to raise_error(ArgumentError,"Cannot add two temperatures") } - end - - context "between a temperature and a degree" do - specify { (Unit("100 tempK") + Unit("100 degK")).should == Unit("200 tempK") } - end - - context "between a degree and a temperature" do - specify { (Unit("100 degK") + Unit("100 tempK")).should == Unit("200 tempK") } - end - - end - - context "subtracting (-)" do - context "compatible units" do - specify { (Unit("0 m") - Unit("10 m")).should == Unit("-10 m")} - specify { (Unit("5 kg") - Unit("10 kg")).should == Unit("-5 kg")} - end - - context "a unit from a zero unit" do - specify { (Unit("0 kg") - Unit("10 m")).should == Unit("-10 m")} - specify { (Unit("0 m") - Unit("10 kg")).should == Unit("-10 kg")} - end - - context "incompatible units" do - specify { expect {Unit("10 kg") - Unit("10 m")}.to raise_error(ArgumentError)} - specify { expect {Unit("10 m") - Unit("10 kg")}.to raise_error(ArgumentError)} - specify { expect {Unit("10 m") - nil}.to raise_error(ArgumentError)} - end - - context "between a unit and coerceable types" do - specify { (Unit('10 kg') - %w{1 kg}).should == Unit('9 kg') } - specify { (Unit('10 kg') - "1 kg").should == Unit('9 kg') } - end - - context "a number from a unit" do - specify { expect { Unit("10 kg") - 1 }.to raise_error(ArgumentError)} - specify { expect { 10 - Unit("10 kg") }.to raise_error(ArgumentError)} - end - - context "between two temperatures" do - specify { (Unit("100 tempK") - Unit("100 tempK")).should == Unit("0 degK") } - end - - context "between a temperature and a degree" do - specify { (Unit("100 tempK") - Unit("100 degK")).should == Unit("0 tempK") } - end - - context "between a degree and a temperature" do - specify { expect {(Unit("100 degK") - Unit("100 tempK"))}.to raise_error(ArgumentError,"Cannot subtract a temperature from a differential degree unit")} - end - - end - - context "multiplying (*)" do - context "between compatible units" do - specify { (Unit("0 m") * Unit("10 m")).should == Unit("0 m^2")} - specify { (Unit("5 kg") * Unit("10 kg")).should == Unit("50 kg^2")} - end - - context "between incompatible units" do - specify { (Unit("0 m") * Unit("10 kg")).should == Unit("0 kg*m")} - specify { (Unit("5 m") * Unit("10 kg")).should == Unit("50 kg*m")} - specify { expect {Unit("10 m") * nil}.to raise_error(ArgumentError)} - end - - context "between a unit and coerceable types" do - specify { (Unit('10 kg') * %w{1 kg}).should == Unit('10 kg^2') } - specify { (Unit('10 kg') * "1 kg").should == Unit('10 kg^2') } - end - - context "by a temperature" do - specify { expect { Unit("5 kg") * Unit("100 tempF")}.to raise_exception(ArgumentError) } - end - - context "by a number" do - specify { (10 * Unit("5 kg")).should == Unit("50 kg")} - end - - end - - context "dividing (/)" do - context "compatible units" do - specify { (Unit("0 m") / Unit("10 m")).should == Unit(0)} - specify { (Unit("5 kg") / Unit("10 kg")).should == Rational(1,2)} - specify { (Unit("5 kg") / Unit("5 kg")).should == 1} - end - - context "incompatible units" do - specify { (Unit("0 m") / Unit("10 kg")).should == Unit("0 m/kg")} - specify { (Unit("5 m") / Unit("10 kg")).should == Unit("1/2 m/kg")} - specify { expect {Unit("10 m") / nil}.to raise_error(ArgumentError)} - end - - context "between a unit and coerceable types" do - specify { (Unit('10 kg^2') / %w{1 kg}).should == Unit('10 kg') } - specify { (Unit('10 kg^2') / "1 kg").should == Unit('10 kg') } - end - - context "by a temperature" do - specify { expect { Unit("5 kg") / Unit("100 tempF")}.to raise_exception(ArgumentError) } - end - - context "a number by a unit" do - specify { (10 / Unit("5 kg")).should == Unit("2 1/kg")} - end - - context "a unit by a number" do - specify { (Unit("5 kg") / 2).should == Unit("2.5 kg")} - end - - context "by zero" do - specify { expect { Unit("10 m") / 0}.to raise_error(ZeroDivisionError)} - specify { expect { Unit("10 m") / Unit("0 m")}.to raise_error(ZeroDivisionError)} - specify { expect { Unit("10 m") / Unit("0 kg")}.to raise_error(ZeroDivisionError)} - end - end - - context "exponentiating (**)" do - - specify "a temperature raises an execption" do - expect { Unit("100 tempK")**2 }.to raise_error(ArgumentError,"Cannot raise a temperature to a power") - end - - context Unit("0 m") do - it { (subject**1).should == subject } - it { (subject**2).should == subject } - end - - context Unit("1 m") do - it { (subject**0).should == 1 } - it { (subject**1).should == subject } - it { (subject**(-1)).should == 1/subject } - it { (subject**(2)).should == Unit("1 m^2")} - it { (subject**(-2)).should == Unit("1 1/m^2")} - specify { expect { subject**(1/2)}.to raise_error(ArgumentError, "Illegal root")} - # because 1 m^(1/2) doesn't make any sense - specify { expect { subject**(Complex(1,1))}.to raise_error(ArgumentError, "exponentiation of complex numbers is not yet supported.")} - specify { expect { subject**(Unit("1 m"))}.to raise_error(ArgumentError, "Invalid Exponent")} - end - - context Unit("1 m^2") do - it { (subject**(Rational(1,2))).should == Unit("1 m")} - it { (subject**(0.5)).should == Unit("1 m")} - - specify { expect { subject**(0.12345) }.to raise_error(ArgumentError,"Not a n-th root (1..9), use 1/n")} - specify { expect { subject**("abcdefg") }.to raise_error(ArgumentError,"Invalid Exponent")} - end - - end - - context "modulo (%)" do - context "compatible units" do - specify { (Unit("2 m") % Unit("1 m")).should == 0 } - specify { (Unit("5 m") % Unit("2 m")).should == 1 } - end - - specify "incompatible units raises an exception" do - expect { Unit("1 m") % Unit("1 kg")}.to raise_error(ArgumentError,"Incompatible Units") - end - end - - context "unary negation (-)" do - specify { (-Unit("1 mm")).should == Unit("-1 mm")} - end - - context "unary plus (+)" do - specify { (+Unit('1 mm')).should == Unit('1 mm')} - end - end - - context "#power" do - subject { Unit("1 m") } - it "raises an exception when passed a Float argument" do - expect {subject.power(1.5)}.to raise_error(ArgumentError,"Exponent must an Integer") - end - it "raises an exception when passed a Rational argument" do - expect {subject.power(Rational(1,2))}.to raise_error(ArgumentError,"Exponent must an Integer") - end - it "raises an exception when passed a Complex argument" do - expect {subject.power(Complex(1,2))}.to raise_error(ArgumentError,"Exponent must an Integer") - end - it "raises an exception when called on a temperature unit" do - expect { Unit("100 tempC").power(2)}.to raise_error(ArgumentError,"Cannot raise a temperature to a power") - end - - specify { (subject.power(-1)).should == Unit("1 1/m") } - specify { (subject.power(0)).should == 1 } - specify { (subject.power(1)).should == subject } - specify { (subject.power(2)).should == Unit("1 m^2") } - - end - - context "#root" do - subject { Unit("1 m") } - it "raises an exception when passed a Float argument" do - expect {subject.root(1.5)}.to raise_error(ArgumentError,"Exponent must an Integer") - end - it "raises an exception when passed a Rational argument" do - expect {subject.root(Rational(1,2))}.to raise_error(ArgumentError,"Exponent must an Integer") - end - it "raises an exception when passed a Complex argument" do - expect {subject.root(Complex(1,2))}.to raise_error(ArgumentError,"Exponent must an Integer") - end - it "raises an exception when called on a temperature unit" do - expect { Unit("100 tempC").root(2)}.to raise_error(ArgumentError,"Cannot take the root of a temperature") - end - - specify { (Unit("1 m^2").root(-2)).should == Unit("1 1/m") } - specify { (subject.root(-1)).should == Unit("1 1/m") } - specify { expect {(subject.root(0))}.to raise_error(ArgumentError, "0th root undefined")} - specify { (subject.root(1)).should == subject } - specify { (Unit("1 m^2").root(2)).should == Unit("1 m") } - - end - - context "#inverse" do - specify { Unit("1 m").inverse.should == Unit("1 1/m") } - specify { expect {Unit("100 tempK").inverse}.to raise_error(ArgumentError,"Cannot divide with temperatures") } - end - - context "convert to scalars" do - specify {Unit("10").to_i.should be_kind_of(Integer)} - specify { expect { Unit("10 m").to_i }.to raise_error(RuntimeError,"Cannot convert '10 m' to Integer unless unitless. Use Unit#scalar") } - - specify {Unit("10.0").to_f.should be_kind_of(Float)} - specify { expect { Unit("10.0 m").to_f }.to raise_error(RuntimeError,"Cannot convert '10 m' to Float unless unitless. Use Unit#scalar") } - - specify {Unit("1+1i").to_c.should be_kind_of(Complex)} - specify { expect { Unit("1+1i m").to_c }.to raise_error(RuntimeError,"Cannot convert '1.0+1.0i m' to Complex unless unitless. Use Unit#scalar") } - - specify {Unit("3/7").to_r.should be_kind_of(Rational)} - specify { expect { Unit("3/7 m").to_r }.to raise_error(RuntimeError,"Cannot convert '3/7 m' to Rational unless unitless. Use Unit#scalar") } - - end - - context "absolute value (#abs)" do - context "of a unitless unit" do - specify "returns the absolute value of the scalar" do - Unit("-10").abs.should == 10 - end - end - - context "of a unit" do - specify "returns a unit with the absolute value of the scalar" do - Unit("-10 m").abs.should == Unit("10 m") - end - end - end - - context "#ceil" do - context "of a unitless unit" do - specify "returns the ceil of the scalar" do - Unit("10.1").ceil.should == 11 - end - end - - context "of a unit" do - specify "returns a unit with the ceil of the scalar" do - Unit("10.1 m").ceil.should == Unit("11 m") - end - end - end - - context "#floor" do - context "of a unitless unit" do - specify "returns the floor of the scalar" do - Unit("10.1").floor.should == 10 - end - end - - context "of a unit" do - specify "returns a unit with the floor of the scalar" do - Unit("10.1 m").floor.should == Unit("10 m") - end - end - end - - context "#round" do - context "of a unitless unit" do - specify "returns the round of the scalar" do - Unit("10.5").round.should == 11 - end - end - - context "of a unit" do - specify "returns a unit with the round of the scalar" do - Unit("10.5 m").round.should == Unit("11 m") - end - end - end - - context "#truncate" do - context "of a unitless unit" do - specify "returns the truncate of the scalar" do - Unit("10.5").truncate.should == 10 - end - end - - context "of a unit" do - specify "returns a unit with the truncate of the scalar" do - Unit("10.5 m").truncate.should == Unit("10 m") - end - end - end - - context '#zero?' do - it "is true when the scalar is zero on the base scale" do - Unit("0").should be_zero - Unit("0 mm").should be_zero - Unit("-273.15 tempC").should be_zero - end - - it "is false when the scalar is not zero" do - Unit("1").should_not be_zero - Unit("1 mm").should_not be_zero - Unit("0 tempC").should_not be_zero - end - end - - context '#succ' do - specify { Unit("1").succ.should == Unit("2")} - specify { Unit("1 mm").succ.should == Unit("2 mm")} - specify { Unit("1 mm").next.should == Unit("2 mm")} - specify { Unit("-1 mm").succ.should == Unit("0 mm")} - specify { expect {Unit("1.5 mm").succ}.to raise_error(ArgumentError,"Non Integer Scalar")} - end - - context '#pred' do - specify { Unit("1").pred.should == Unit("0")} - specify { Unit("1 mm").pred.should == Unit("0 mm")} - specify { Unit("-1 mm").pred.should == Unit("-2 mm")} - specify { expect {Unit("1.5 mm").pred}.to raise_error(ArgumentError,"Non Integer Scalar")} - end - - context '#divmod' do - specify { Unit("5 mm").divmod(Unit("2 mm")).should == [2,1] } - specify { Unit("1 km").divmod(Unit("2 m")).should == [500,0] } - specify { expect {Unit('1 m').divmod(Unit('2 kg'))}.to raise_error(ArgumentError,"Incompatible Units")} - end - - context '#div' do - specify { Unit('23 m').div(Unit('2 m')).should == 11 } - end - - context "Time helper functions" do - before do - Time.stub!(:now).and_return(Time.utc(2011,10,16)) - DateTime.stub!(:now).and_return(DateTime.civil(2011,10,16)) - Date.stub!(:today).and_return(Date.civil(2011,10,16)) - end - - context '#since' do - specify { Unit("min").since(Time.utc(2001,4,1,0,0,0)).should == Unit("5544000 min")} - specify { Unit("min").since(DateTime.civil(2001,4,1,0,0,0)).should == Unit("5544000 min")} - specify { Unit("min").since(Date.civil(2001,4,1)).should == Unit("5544000 min")} - specify { expect {Unit("min").since("4-1-2001")}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") } - specify { expect {Unit("min").since(nil)}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime") } - end - - context '#before' do - specify { Unit("5 min").before(Time.now).should == Time.utc(2011,10,15,23,55)} - specify { Unit("5 min").before(DateTime.now).should == DateTime.civil(2011,10,15,23,55)} - specify { Unit("5 min").before(Date.today).should == DateTime.civil(2011,10,15,23,55)} - specify { expect {Unit('5 min').before(nil)}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime")} - specify { expect {Unit('5 min').before("12:00")}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime")} - end - - context '#ago' do - specify { Unit("5 min").ago.should be_kind_of Time} - specify { Unit("10000 y").ago.should be_kind_of Time} - specify { Unit("1 year").ago.should == Time.utc(2010,10,16)} - end - - context '#until' do - specify { Unit("min").until(Date.civil(2011,10,17)).should == Unit("1440 min")} - specify { Unit("min").until(DateTime.civil(2011,10,21)).should == Unit("7200 min")} - specify { Unit("min").until(Time.utc(2011,10,21)).should == Unit("7200 min")} - specify { expect {Unit('5 min').until(nil)}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime")} - specify { expect {Unit('5 min').until("12:00")}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime")} - end - - context '#from' do - specify { Unit("1 day").from(Date.civil(2011,10,17)).should == Date.civil(2011,10,18)} - specify { Unit("5 min").from(DateTime.civil(2011,10,21)).should == DateTime.civil(2011,10,21,00,05)} - specify { Unit("5 min").from(Time.utc(2011,10,21)).should == Time.utc(2011,10,21,00,05)} - specify { expect {Unit('5 min').from(nil)}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime")} - specify { expect {Unit('5 min').from("12:00")}.to raise_error(ArgumentError, "Must specify a Time, Date, or DateTime")} - end - - end - -end - -describe "Unit Output formatting" do - context Unit("10.5 m/s^2") do - specify { subject.to_s.should == "10.5 m/s^2" } - specify { subject.to_s("%0.2f").should == "10.50 m/s^2"} - specify { subject.to_s("%0.2e km/s^2").should == "1.05e-02 km/s^2"} - specify { subject.to_s("km/s^2").should == "0.0105 km/s^2"} - specify { subject.to_s(STDOUT).should == "10.5 m/s^2" } - specify { expect {subject.to_s("random string")}.to raise_error(ArgumentError,"'random' Unit not recognized")} - end - - context "for a unit with a custom display_name" do - before(:each) do - Unit.redefine!("cup") do |cup| - cup.display_name = "cupz" - end - end - - after(:each) do - Unit.redefine!("cup") do |cup| - cup.display_name = cup.aliases.first - end - end - - subject { Unit("8 cups")} - - specify { subject.to_s.should == "8 cupz" } - - end - -end - -describe "Equations with Units" do - context "Ideal Gas Law" do - let(:p) { Unit('100 kPa') } - let(:v) { Unit('1 m^3') } - let(:n) { Unit("1 mole") } - let(:r) { Unit("8.31451 J/mol*degK") } - specify { ((p*v)/(n*r)).convert_to('tempK').should be_within(Unit("0.1 degK")).of(Unit("12027.2 tempK")) } - end -end diff --git a/spec/ruby-units/utf-8/unit_spec.rb b/spec/ruby-units/utf-8/unit_spec.rb deleted file mode 100644 index 045fafed..00000000 --- a/spec/ruby-units/utf-8/unit_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -# encoding: utf-8 - -require File.dirname(__FILE__) + '/../../spec_helper' - -if RUBY_VERSION.include?('1.9') - describe Unit, 'Degrees' do - context 'when the UTF-8 symbol is used' do - context 'Angles' do - it 'should be a degree' do - Unit("180\u00B0").units.should == 'deg' - end - end - - context 'Temperature' do - it 'should be a degree Celcius' do - Unit("180\u00B0C").units.should == 'degC' - end - - it 'should be a degree Fahrenheit' do - Unit("180\u00B0F").units.should == 'degF' - end - end - end - end -end diff --git a/spec/ruby_units/array_spec.rb b/spec/ruby_units/array_spec.rb new file mode 100644 index 00000000..3c5157fe --- /dev/null +++ b/spec/ruby_units/array_spec.rb @@ -0,0 +1,12 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe Array do + subject { [1, 'cm'] } + + it { is_expected.to be_kind_of Array } + it { is_expected.to respond_to :to_unit } + + specify { expect(subject.to_unit).to be_instance_of Unit } + specify { expect(subject.to_unit).to eq('1 cm'.to_unit) } + specify { expect(subject.to_unit('mm')).to eq('10 mm'.to_unit) } +end diff --git a/spec/ruby_units/bugs_spec.rb b/spec/ruby_units/bugs_spec.rb new file mode 100644 index 00000000..d90338f0 --- /dev/null +++ b/spec/ruby_units/bugs_spec.rb @@ -0,0 +1,10 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe 'Github issue #49' do + let(:a) { RubyUnits::Unit.new('3 cm^3') } + let(:b) { RubyUnits::Unit.new(a) } + + it 'should subtract a unit properly from one initialized with a unit' do + expect(b - RubyUnits::Unit.new('1.5 cm^3')).to eq(RubyUnits::Unit.new('1.5 cm^3')) + end +end diff --git a/spec/ruby_units/cache_spec.rb b/spec/ruby_units/cache_spec.rb new file mode 100644 index 00000000..5856570a --- /dev/null +++ b/spec/ruby_units/cache_spec.rb @@ -0,0 +1,31 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe Unit::Cache do + subject { Unit::Cache } + let(:unit) { RubyUnits::Unit.new('1 m') } + + before(:each) do + subject.clear + subject.set('m', unit) + end + + context '.clear' do + it 'should clear the cache' do + subject.clear + expect(subject.get('m')).to be_nil + end + end + + context '.get' do + it 'should retrieve values already in the cache' do + expect(subject.get['m']).to eq(unit) + end + end + + context '.set' do + it 'should put a unit into the cache' do + subject.set('kg', RubyUnits::Unit.new('1 kg')) + expect(subject.get['kg']).to eq(RubyUnits::Unit.new('1 kg')) + end + end +end diff --git a/spec/ruby_units/complex_spec.rb b/spec/ruby_units/complex_spec.rb new file mode 100644 index 00000000..82bb89c5 --- /dev/null +++ b/spec/ruby_units/complex_spec.rb @@ -0,0 +1,25 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +# Complex numbers are a bit strange +# Technically you can't really compare them using <=>, and ruby 1.9 does not implement this method for them +# so it stands to reason that complex units should also not support :> or :< + +describe Complex do + subject { Complex(1, 1) } + it { is_expected.to respond_to :to_unit } +end + +describe 'Complex Unit' do + subject { Complex(1.0, -1.0).to_unit } + + it { is_expected.to be_a Unit } + it { expect(subject.scalar).to be_a Complex } + + it { is_expected.to eq('1-1i'.to_unit) } + it { is_expected.to be === '1-1i'.to_unit } + + it 'is not comparable' do + expect { subject > '1+1i'.to_unit }.to raise_error(NoMethodError) + expect { subject < '1+1i'.to_unit }.to raise_error(NoMethodError) + end +end diff --git a/spec/ruby_units/configuration_spec.rb b/spec/ruby_units/configuration_spec.rb new file mode 100644 index 00000000..f32fb88e --- /dev/null +++ b/spec/ruby_units/configuration_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe RubyUnits::Configuration do + context '.separator is true' do + it 'has a space between the scalar and the unit' do + expect(RubyUnits::Unit.new('1 m').to_s).to eq '1 m' + end + end + + context '.separator is false' do + around(:each) do |example| + RubyUnits.configure do |config| + config.separator = false + end + example.run + RubyUnits.reset + end + + it 'does not have a space between the scalar and the unit' do + expect(RubyUnits::Unit.new('1 m').to_s).to eq '1m' + expect(RubyUnits::Unit.new('14.5 lbs').to_s(:lbs)).to eq '14lbs, 8oz' + expect(RubyUnits::Unit.new('220 lbs').to_s(:stone)).to eq '15stone, 10lb' + expect(RubyUnits::Unit.new('14.2 ft').to_s(:ft)).to eq %(14'2") + expect(RubyUnits::Unit.new('1/2 cup').to_s).to eq '1/2cu' + expect(RubyUnits::Unit.new('123.55 lbs').to_s('%0.2f')).to eq '123.55lbs' + end + end +end diff --git a/spec/ruby_units/date_spec.rb b/spec/ruby_units/date_spec.rb new file mode 100644 index 00000000..2ae06542 --- /dev/null +++ b/spec/ruby_units/date_spec.rb @@ -0,0 +1,49 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe Date do + subject { Date.new(2011, 4, 1) } + + it { is_expected.to be_instance_of Date } + it { is_expected.to respond_to :to_unit } + it { is_expected.to respond_to :to_time } + it { is_expected.to respond_to :to_date } + + specify { expect(subject + '5 days'.to_unit).to eq(Date.new(2011, 4, 6)) } + specify { expect(subject - '5 days'.to_unit).to eq(Date.new(2011, 3, 27)) } + # 2012 is a leap year... + specify { expect(subject + '1 year'.to_unit).to eq(Date.new(2012, 3, 31)) } + specify { expect(subject - '1 year'.to_unit).to eq(Date.new(2010, 4, 1)) } +end + +describe 'Date Unit' do + subject { Date.new(2011, 4, 1).to_unit } + + it { is_expected.to be_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_kind_of Rational } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('d') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:time) } + end + + specify { expect(subject + '5 days'.to_unit).to eq(Date.new(2011, 4, 6)) } + specify { expect(subject - '5 days'.to_unit).to eq(Date.new(2011, 3, 27)) } + + specify { expect { subject + Date.new(2011, 4, 1) }.to raise_error(ArgumentError) } + specify { expect { subject + DateTime.new(2011, 4, 1, 12, 0, 0) }.to raise_error(ArgumentError) } + specify { expect { subject + Time.parse('2011-04-01 12:00:00') }.to raise_error(ArgumentError) } + + specify { expect(subject - Date.new(2011, 4, 1)).to be_zero } + specify { expect(subject - DateTime.new(2011, 4, 1, 0, 0, 0)).to be_zero } + specify { expect { (subject - Time.parse('2011-04-01 00:00')) }.to raise_error(ArgumentError) } + specify { expect(Date.new(2011, 4, 1) + 1).to eq(Date.new(2011, 4, 2)) } +end diff --git a/spec/ruby_units/definition_spec.rb b/spec/ruby_units/definition_spec.rb new file mode 100644 index 00000000..b9ab7e38 --- /dev/null +++ b/spec/ruby_units/definition_spec.rb @@ -0,0 +1,41 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe "Unit::Definition('eV')" do + subject do + Unit::Definition.new('eV') do |ev| + ev.aliases = ['eV', 'electron-volt'] + ev.definition = RubyUnits::Unit.new('1.602E-19 joule') + ev.display_name = 'electron-volt' + end + end + + describe '#name' do + subject { super().name } + it { is_expected.to eq('') } + end + + describe '#aliases' do + subject { super().aliases } + it { is_expected.to eq(%w[eV electron-volt]) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1.602E-19) } + end + + describe '#numerator' do + subject { super().numerator } + it { is_expected.to include('', '', '') } + end + + describe '#denominator' do + subject { super().denominator } + it { is_expected.to include('', '') } + end + + describe '#display_name' do + subject { super().display_name } + it { is_expected.to eq('electron-volt') } + end +end diff --git a/spec/ruby_units/math_spec.rb b/spec/ruby_units/math_spec.rb new file mode 100644 index 00000000..f7f65922 --- /dev/null +++ b/spec/ruby_units/math_spec.rb @@ -0,0 +1,81 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe Math do + describe '#sqrt' do + specify { expect(Math.sqrt(RubyUnits::Unit.new('1 mm^6'))).to eq(RubyUnits::Unit.new('1 mm^3')) } + specify { expect(Math.sqrt(4)).to eq(2) } + specify { expect(Math.sqrt(RubyUnits::Unit.new('-9 mm^2')).scalar).to be_kind_of(Complex) } + end + + describe '#cbrt' do + specify { expect(Math.cbrt(RubyUnits::Unit.new('1 mm^6'))).to eq(RubyUnits::Unit.new('1 mm^2')) } + specify { expect(Math.cbrt(8)).to eq(2) } + end + + context 'Trigonometry functions' do + context "with '45 deg' unit" do + subject { RubyUnits::Unit.new('45 deg') } + specify { expect(Math.sin(subject)).to be_within(0.01).of(0.70710678) } + specify { expect(Math.cos(subject)).to be_within(0.01).of(0.70710678) } + specify { expect(Math.tan(subject)).to be_within(0.01).of(1) } + specify { expect(Math.sinh(subject)).to be_within(0.01).of(0.8686709614860095) } + specify { expect(Math.cosh(subject)).to be_within(0.01).of(1.3246090892520057) } + specify { expect(Math.tanh(subject)).to be_within(0.01).of(0.6557942026326724) } + end + + context "with 'PI/4 radians' unit" do + subject { RubyUnits::Unit.new((Math::PI / 4), 'radians') } + specify { expect(Math.sin(subject)).to be_within(0.01).of(0.70710678) } + specify { expect(Math.cos(subject)).to be_within(0.01).of(0.70710678) } + specify { expect(Math.tan(subject)).to be_within(0.01).of(1) } + specify { expect(Math.sinh(subject)).to be_within(0.01).of(0.8686709614860095) } + specify { expect(Math.cosh(subject)).to be_within(0.01).of(1.3246090892520057) } + specify { expect(Math.tanh(subject)).to be_within(0.01).of(0.6557942026326724) } + end + + context "with 'PI/4' continues to work" do + subject { (Math::PI / 4) } + specify { expect(Math.sin(subject)).to be_within(0.01).of(0.70710678) } + specify { expect(Math.cos(subject)).to be_within(0.01).of(0.70710678) } + specify { expect(Math.tan(subject)).to be_within(0.01).of(1) } + specify { expect(Math.sinh(subject)).to be_within(0.01).of(0.8686709614860095) } + specify { expect(Math.cosh(subject)).to be_within(0.01).of(1.3246090892520057) } + specify { expect(Math.tanh(subject)).to be_within(0.01).of(0.6557942026326724) } + end + + specify do + expect( + Math.hypot( + RubyUnits::Unit.new('1 m'), + RubyUnits::Unit.new('2 m') + ) + ).to be_within(RubyUnits::Unit.new('0.01 m')).of(RubyUnits::Unit.new('2.23607 m')) + end + specify do + expect( + Math.hypot( + RubyUnits::Unit.new('1 m'), + RubyUnits::Unit.new('2 ft') + ) + ).to be_within(RubyUnits::Unit.new('0.01 m')).of(RubyUnits::Unit.new('1.17116 m')) + end + specify { expect(Math.hypot(3, 4)).to eq(5) } + specify do + expect do + Math.hypot( + RubyUnits::Unit.new('1 m'), + RubyUnits::Unit.new('2 lbs') + ) + end.to raise_error(ArgumentError) + end + + specify do + expect(Math.atan2(RubyUnits::Unit.new('1 m'), RubyUnits::Unit.new('2 m'))).to be_within(0.01).of(0.4636476090008061) + end + specify do + expect(Math.atan2(RubyUnits::Unit.new('1 m'), RubyUnits::Unit.new('2 ft'))).to be_within(0.01).of(1.0233478888629426) + end + specify { expect(Math.atan2(1, 1)).to be_within(0.01).of(0.785398163397448) } + specify { expect { Math.atan2(RubyUnits::Unit.new('1 m'), RubyUnits::Unit.new('2 lbs')) }.to raise_error(ArgumentError) } + end +end diff --git a/spec/ruby_units/numeric_spec.rb b/spec/ruby_units/numeric_spec.rb new file mode 100644 index 00000000..92f7375f --- /dev/null +++ b/spec/ruby_units/numeric_spec.rb @@ -0,0 +1,12 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +# some rubies return an array of strings for .instance_methods and others return an array of symbols +# so let's stringify them before we compare +describe Numeric do + specify { expect(Float.instance_methods.map(&:to_s)).to include('to_unit') } + specify { expect(Integer.instance_methods.map(&:to_s)).to include('to_unit') } + specify { expect(Integer.instance_methods.map(&:to_s)).to include('to_unit') } + specify { expect(Complex.instance_methods.map(&:to_s)).to include('to_unit') } + specify { expect(Integer.instance_methods.map(&:to_s)).to include('to_unit') } + specify { expect(Rational.instance_methods.map(&:to_s)).to include('to_unit') } +end diff --git a/spec/ruby_units/range_spec.rb b/spec/ruby_units/range_spec.rb new file mode 100644 index 00000000..38b1dd02 --- /dev/null +++ b/spec/ruby_units/range_spec.rb @@ -0,0 +1,19 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe 'Range' do + context 'of integer units' do + subject { (RubyUnits::Unit.new('1 mm')..RubyUnits::Unit.new('3 mm')) } + it { is_expected.to include(RubyUnits::Unit.new('2 mm')) } + + describe '#to_a' do + subject { super().to_a } + it { is_expected.to eq([RubyUnits::Unit.new('1 mm'), RubyUnits::Unit.new('2 mm'), RubyUnits::Unit.new('3 mm')]) } + end + end + + context 'of floating point units' do + subject { (RubyUnits::Unit.new('1.5 mm')..RubyUnits::Unit.new('3.5 mm')) } + it { is_expected.to include(RubyUnits::Unit.new('2.0 mm')) } + specify { expect { subject.to_a }.to raise_exception(ArgumentError) } + end +end diff --git a/spec/ruby_units/string_spec.rb b/spec/ruby_units/string_spec.rb new file mode 100644 index 00000000..eb55ea8e --- /dev/null +++ b/spec/ruby_units/string_spec.rb @@ -0,0 +1,17 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe String do + context 'Unit creation from strings' do + specify { expect('1 mm'.to_unit).to be_instance_of Unit } + specify { expect('1 m'.convert_to('ft')).to be_within(RubyUnits::Unit.new('0.01 ft')).of RubyUnits::Unit.new('3.28084 ft') } + end + + context 'output format' do + subject { RubyUnits::Unit.new('1.23456 m/s^2') } + specify { expect('' % subject).to eq('') } + specify { expect('%0.2f' % subject).to eq('1.23 m/s^2') } + specify { expect('%0.2f km/h^2' % subject).to eq('15999.90 km/h^2') } + specify { expect('km/h^2' % subject).to eq('15999.9 km/h^2') } + specify { expect('%H:%M:%S' % RubyUnits::Unit.new('1.5 h')).to eq('01:30:00') } + end +end diff --git a/spec/ruby_units/temperature_spec.rb b/spec/ruby_units/temperature_spec.rb new file mode 100644 index 00000000..2f7e8582 --- /dev/null +++ b/spec/ruby_units/temperature_spec.rb @@ -0,0 +1,111 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe 'temperatures' do + describe 'redfine display name' do + before(:all) do + Unit.redefine!('tempC') do |c| + c.aliases = %w[tC tempC] + c.display_name = 'tC' + end + + Unit.redefine!('tempF') do |f| + f.aliases = %w[tF tempF] + f.display_name = 'tF' + end + + Unit.redefine!('tempR') do |f| + f.aliases = %w[tR tempR] + f.display_name = 'tR' + end + + Unit.redefine!('tempK') do |f| + f.aliases = %w[tK tempK] + f.display_name = 'tK' + end + end + + after(:all) do + # define the temp units back to normal + Unit.define('tempK') do |unit| + unit.scalar = 1 + unit.numerator = %w[] + unit.aliases = %w[tempK] + unit.kind = :temperature + end + + Unit.define('tempC') do |tempc| + tempc.definition = RubyUnits::Unit.new('1 tempK') + end + + temp_convert_factor = Rational(2_501_999_792_983_609, 4_503_599_627_370_496) # approximates 1/1.8 + + Unit.define('tempF') do |tempf| + tempf.definition = RubyUnits::Unit.new(temp_convert_factor, 'tempK') + end + + Unit.define('tempR') do |tempr| + tempr.definition = RubyUnits::Unit.new('1 tempF') + end + end + + describe "RubyUnits::Unit.new('100 tC')" do + subject { RubyUnits::Unit.new('100 tC') } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_within(0.001).of 100 } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('tC') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:temperature) } + end + it { is_expected.to be_temperature } + it { is_expected.to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_within(RubyUnits::Unit.new('0.01 degK')).of RubyUnits::Unit.new('373.15 tempK') } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to eq('degC') } + end + end + + context 'between temperature scales' do + # note that 'temp' units are for temperature readings on a scale, while 'deg' units are used to represent + # differences between temperatures, offsets, or other differential temperatures. + + specify { expect(RubyUnits::Unit.new('100 tC')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('373.15 tempK')) } + specify { expect(RubyUnits::Unit.new('0 tC')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('273.15 tempK')) } + specify { expect(RubyUnits::Unit.new('37 tC')).to be_within(RubyUnits::Unit.new('0.01 degK')).of(RubyUnits::Unit.new('310.15 tempK')) } + specify { expect(RubyUnits::Unit.new('-273.15 tC')).to eq(RubyUnits::Unit.new('0 tempK')) } + + specify { expect(RubyUnits::Unit.new('212 tF')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('373.15 tempK')) } + specify { expect(RubyUnits::Unit.new('32 tF')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('273.15 tempK')) } + specify { expect(RubyUnits::Unit.new('98.6 tF')).to be_within(RubyUnits::Unit.new('0.01 degK')).of(RubyUnits::Unit.new('310.15 tempK')) } + specify { expect(RubyUnits::Unit.new('-459.67 tF')).to eq(RubyUnits::Unit.new('0 tempK')) } + + specify { expect(RubyUnits::Unit.new('671.67 tR')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('373.15 tempK')) } + specify { expect(RubyUnits::Unit.new('491.67 tR')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('273.15 tempK')) } + specify { expect(RubyUnits::Unit.new('558.27 tR')).to be_within(RubyUnits::Unit.new('0.01 degK')).of(RubyUnits::Unit.new('310.15 tempK')) } + specify { expect(RubyUnits::Unit.new('0 tR')).to eq(RubyUnits::Unit.new('0 tempK')) } + + 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')) } + 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')) } + 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')) } + + specify { expect(RubyUnits::Unit.new('32 tF').convert_to('tempC')).to eq(RubyUnits::Unit.new('0 tC')) } + end + end +end diff --git a/spec/ruby_units/time_spec.rb b/spec/ruby_units/time_spec.rb new file mode 100644 index 00000000..7bad22f3 --- /dev/null +++ b/spec/ruby_units/time_spec.rb @@ -0,0 +1,56 @@ +require File.dirname(__FILE__) + '/../spec_helper' + +describe Time do + let(:now) { Time.at(1_303_656_390) } + before(:each) do + allow(Time).to receive(:now).and_return(now) + end + + context '.at' do + subject { Date.new(2011, 4, 1).to_unit } + specify { expect(Time.at(Time.at(0)).utc.strftime('%D %T')).to eq('01/01/70 00:00:00') } + specify { expect(Time.at(subject - Date.new(1970, 1, 1)).getutc.strftime('%D %T')).to eq('04/01/11 00:00:00') } + specify { expect(Time.at(subject - Date.new(1970, 1, 1), 500).usec).to eq(500) } + end + + context '.in' do + specify { expect(Time.in('5 min')).to be_a Time } + specify { expect(Time.in('5 min')).to be > Time.now } + end + + context '#to_date' do + subject { Time.parse('2012-01-31 11:59:59') } + specify { expect(subject.to_date.to_s).to eq('2012-01-31') } + specify { expect((subject + 1).to_date.to_s).to eq('2012-01-31') } + end + + context '#to_unit' do + subject { now } + + describe '#to_unit' do + subject { super().to_unit } + it { is_expected.to be_an_instance_of(Unit) } + end + + describe '#to_unit' do + subject { super().to_unit } + describe '#units' do + subject { super().units } + it { is_expected.to eq('s') } + end + end + specify { expect(subject.to_unit('h').kind).to eq(:time) } + specify { expect(subject.to_unit('h').units).to eq('h') } + end + + context 'addition (+)' do + specify { expect(Time.now + 1).to eq(Time.at(1_303_656_390 + 1)) } + specify { expect(Time.now + RubyUnits::Unit.new('10 min')).to eq(Time.at(1_303_656_390 + 600)) } + end + + context 'subtraction (-)' do + specify { expect(Time.now - 1).to eq(Time.at(1_303_656_390 - 1)) } + specify { expect(Time.now - RubyUnits::Unit.new('10 min')).to eq(Time.at(1_303_656_390 - 600)) } + specify { expect(Time.now - RubyUnits::Unit.new('150 years')).to eq(Time.parse('1861-04-24 09:46:30 -0500')) } + end +end diff --git a/spec/ruby_units/unit_spec.rb b/spec/ruby_units/unit_spec.rb new file mode 100644 index 00000000..ac91458f --- /dev/null +++ b/spec/ruby_units/unit_spec.rb @@ -0,0 +1,2286 @@ +require File.dirname(__FILE__) + '/../spec_helper' +require 'yaml' + +describe Unit.base_units do + it { is_expected.to be_a Array } + it 'has 14 elements' do + expect(subject.size).to eq(14) + end + %w[kilogram meter second ampere degK tempK mole candela each dollar steradian radian decibel byte].each do |u| + it { is_expected.to include(RubyUnits::Unit.new(u)) } + end +end + +describe 'Create some simple units' do + # zero string + describe RubyUnits::Unit.new('0') do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be === 0 } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_empty } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.to be_unitless } + it { is_expected.to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(subject) } + end + end + + # non-zero string + describe RubyUnits::Unit.new('1') do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be === 1 } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_empty } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(subject) } + end + end + + # numeric + describe RubyUnits::Unit.new(1) do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be === 1 } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_empty } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(subject) } + end + end + + # rational + describe RubyUnits::Unit.new(Rational(1, 2)) do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be === Rational(1, 2) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_a Rational } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_empty } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(subject) } + end + end + + # float + describe RubyUnits::Unit.new(0.5) do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be === 0.5 } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_a Float } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_empty } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(subject) } + end + end + + # complex + describe RubyUnits::Unit.new(Complex(1, 1)) do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be === Complex(1, 1) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_a Complex } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_empty } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(subject) } + end + end + + describe RubyUnits::Unit.new('1+1i m') do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be === Complex(1, 1) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_a Complex } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('m') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:length) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(subject) } + end + end + + # scalar and unit + describe RubyUnits::Unit.new('1 mm') do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('mm') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:length) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(RubyUnits::Unit.new('0.001 m')) } + end + end + + # with a zero power + describe RubyUnits::Unit.new('1 m^0') do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(RubyUnits::Unit.new('1')) } + end + end + + # unit only + describe RubyUnits::Unit.new('mm') do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('mm') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:length) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(RubyUnits::Unit.new('0.001 m')) } + end + end + + # Compound unit + describe RubyUnits::Unit.new('1 N*m') do + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('N*m') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:energy) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(RubyUnits::Unit.new('1 kg*m^2/s^2')) } + end + end + + # scalar and unit with powers + describe RubyUnits::Unit.new('10 m/s^2') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(10) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('m/s^2') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:acceleration) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(RubyUnits::Unit.new('10 m/s^2')) } + end + end + + # feet/in form + ['5 feet 6 inches', '5 feet 6 inch', '5ft 6in', '5 ft 6 in', %(5'6"), %(5' 6")].each do |unit| + describe unit do + subject { RubyUnits::Unit.new(unit) } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(5.5) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('ft') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:length) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_within(RubyUnits::Unit.new('0.01 m')).of RubyUnits::Unit.new('1.6764 m') } + end + specify { expect(subject.to_s(:ft)).to eq(%(5'6")) } + end + end + + # pound/ounces form + describe RubyUnits::Unit.new('6lbs 5oz') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_within(0.001).of 6.312 } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('lbs') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:mass) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_within(RubyUnits::Unit.new('0.01 kg')).of RubyUnits::Unit.new('2.8633 kg') } + end + specify { expect(subject.to_s(:lbs)).to eq('6 lbs, 5 oz') } + end + + # temperature + describe RubyUnits::Unit.new('100 tempC') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_within(0.001).of 100 } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('tempC') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:temperature) } + end + it { is_expected.to be_temperature } + it { is_expected.to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_within(RubyUnits::Unit.new('0.01 degK')).of RubyUnits::Unit.new('373.15 tempK') } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to eq('degC') } + end + end + + # Time + describe RubyUnits::Unit.new(Time.now) do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_a(Numeric) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('s') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:time) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a(Numeric) } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # degrees + describe RubyUnits::Unit.new('100 degC') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_within(0.001).of 100 } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('degC') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:temperature) } + end + it { is_expected.not_to be_temperature } + it { is_expected.to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + + describe '#base' do + subject { super().base } + it { is_expected.to be_within(RubyUnits::Unit.new('0.01 degK')).of RubyUnits::Unit.new('100 degK') } + end + end + + # percent + describe RubyUnits::Unit.new('75%') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('%') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a(Numeric) } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # angle + describe RubyUnits::Unit.new('180 deg') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_a Numeric } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('deg') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:angle) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a(Numeric) } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # radians + describe RubyUnits::Unit.new('1 radian') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_a Numeric } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('rad') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:angle) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # counting + describe RubyUnits::Unit.new('12 dozen') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('doz') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # rational scalar with unit + describe RubyUnits::Unit.new('1/2 kg') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Rational } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('kg') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:mass) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + describe RubyUnits::Unit.new('6/1 lbs') do + it { is_expected.to be_an_instance_of Unit } + describe '#to_s' do + subject { super().to_s } + it { is_expected.to eq '6 lbs' } + end + end + + # rational scalar with compound unit + describe RubyUnits::Unit.new('1/2 kg/m') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Rational } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('kg/m') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to be_nil } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # rational scalar with numeric modified unit + describe Unit.new('12.0mg/6.0mL') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Numeric } + it { is_expected.to eq(12.0 / 6.0) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq 'mg/ml' } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq :density } + end + + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + describe 'Units with rates of per N ' do + subject { Unit.new(2, 'kg/100kg') } + + before do + Unit.clear_cache # Needed for the following definition to consistently work + Unit.define('100kg') do |u| + u.definition = Unit.new('100 kg') + u.aliases = %w[100kg] + end + end + + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Numeric } + it { is_expected.to eq(2) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_a String } + it { is_expected.to eq('kg/100kg') } + end + + context 'when there is a power involved (e.g. liters squared)' do + subject { Unit.new(2, 'l^2/100kg') } + + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Numeric } + it { is_expected.to eq(2) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to be_a String } + it { is_expected.to eq('l^2/100kg') } + end + end + + describe 'Calculations' do + describe 'Addition' do + subject { Unit.new(2, 'kg/100kg') + Unit.new(2, 'kg/100kg') } + + describe '#scalar', skip: 'TODO: Fix the 4/1 here' do + subject { super().scalar } + it { is_expected.to eq('4') } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('kg/100kg') } + end + end + + describe 'Multiplication' do + subject { Unit.new(2, 'kg/100kg') * Unit.new(2, 'kg/100kg') } + + describe '#scalar', skip: 'TODO: Fix the infinite loop here' do + subject { super().scalar } + it { is_expected.to eq('4') } + end + + describe '#units', skip: 'TODO: Fix the infinite loop here' do + subject { super().units } + it { is_expected.to eq('kg/100kg') } + end + end + + describe 'Division' do + subject { Unit.new(4, 'kg/100kg') / Unit.new(2, 'kg/100kg') } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(2) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('') } + end + end + end + end + + # time string + describe RubyUnits::Unit.new('1:23:45,200') do + it { is_expected.to be_an_instance_of Unit } + it { is_expected.to eq(RubyUnits::Unit.new('1 h') + RubyUnits::Unit.new('23 min') + RubyUnits::Unit.new('45 seconds') + RubyUnits::Unit.new('200 usec')) } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Rational } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('h') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:time) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # also '1 hours as minutes' + # '1 hour to minutes' + describe Unit.parse('1 hour in minutes') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq 60 } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('min') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:time) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # funky unit + describe RubyUnits::Unit.new('1 attoparsec/microfortnight') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('apc/ufortnight') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:speed) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + it { expect(subject.convert_to('in/s')).to be_within(RubyUnits::Unit.new('0.0001 in/s')).of(RubyUnits::Unit.new('1.0043269330917 in/s')) } + end + + # Farads + describe RubyUnits::Unit.new('1 F') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('F') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:capacitance) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + describe RubyUnits::Unit.new('1 m^2 s^-2') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('m^2/s^2') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:radiation) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + describe RubyUnits::Unit.new(1, 'm^2', 's^2') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('m^2/s^2') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:radiation) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # scientific notation + describe RubyUnits::Unit.new('1e6 cells') do + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1e6) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('cells') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to be_a Numeric } + end + + describe '#temperature_scale' do + subject { super().temperature_scale } + it { is_expected.to be_nil } + end + end + + # could be m*m + describe RubyUnits::Unit.new('1 mm') do + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:length) } + end + end + + # could be centi-day + describe RubyUnits::Unit.new('1 cd') do + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:luminous_power) } + end + end + + # could be milli-inch + describe RubyUnits::Unit.new('1 min') do + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:time) } + end + end + + # could be femto-tons + describe RubyUnits::Unit.new('1 ft') do + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:length) } + end + end + + # could be deci-ounce + describe RubyUnits::Unit.new('1 doz') do + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:unitless) } + end + end + + # create with another unit + describe 10.to_unit(RubyUnits::Unit.new('1 mm')) do + describe '#units' do + subject { super().units } + it { is_expected.to eq('mm') } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(10) } + end + end + + # explicit create + describe RubyUnits::Unit.new('1 /') do + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:speed) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('m/s') } + end + end + + describe RubyUnits::Unit.new('1 /') do + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:yank) } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('kg*m/s^3') } + end + end + + # without spaces + describe RubyUnits::Unit.new('1g') do + specify { expect(subject).to eq(RubyUnits::Unit.new('1 g')) } + end + + describe RubyUnits::Unit.new('-1g') do + specify { expect(subject).to eq(RubyUnits::Unit.new('-1 g')) } + end + + describe RubyUnits::Unit.new('11/s') do + specify { expect(subject).to eq(RubyUnits::Unit.new('11 1/s')) } + end + + describe Unit.new('63.5029318kg') do + specify { expect(subject).to eq(Unit.new('63.5029318 kg')) } + end + + # mixed fraction + describe Unit.new('6 1/2 cups') do + specify { expect(subject).to eq(Unit.new('13/2 cu')) } + end + + # mixed fraction + describe Unit.new('-6-1/2 cups') do + specify { expect(subject).to eq(Unit.new('-13/2 cu')) } + end + + describe Unit.new('100 mcg') do + specify { expect(subject).to eq(Unit.new('100 ug')) } + end + + describe Unit.new('100 mcL') do + specify { expect(subject).to eq(Unit.new('100 uL')) } + end +end + +describe 'Unit handles attempts to create bad units' do + specify 'no empty strings' do + expect { RubyUnits::Unit.new('') }.to raise_error(ArgumentError, 'No Unit Specified') + end + + specify 'no blank strings' do + expect { RubyUnits::Unit.new(' ') }.to raise_error(ArgumentError, 'No Unit Specified') + end + + specify 'no strings with tabs' do + expect { RubyUnits::Unit.new("\t") }.to raise_error(ArgumentError, 'No Unit Specified') + end + + specify 'no strings with newlines' do + expect { RubyUnits::Unit.new("\n") }.to raise_error(ArgumentError, 'No Unit Specified') + end + + specify 'no double slashes' do + expect { RubyUnits::Unit.new('3 s/s/ft') }.to raise_error(ArgumentError, /Unit not recognized/) + end + + specify 'no pipes or commas' do + expect { RubyUnits::Unit.new('3 s**2|,s**2') }.to raise_error(ArgumentError, /Unit not recognized/) + end + + specify 'no multiple spaces' do + expect { RubyUnits::Unit.new('3 s**2 4s s**2') }.to raise_error(ArgumentError, /Unit not recognized/) + end + + specify 'no exponentiation of numbers' do + expect { RubyUnits::Unit.new('3 s 5^6') }.to raise_error(ArgumentError, /Unit not recognized/) + end + + specify "no strings that don't specify a valid unit" do + expect { RubyUnits::Unit.new('random string') }.to raise_error(ArgumentError, "'random string' Unit not recognized") + end + + specify 'no unhandled classes' do + expect { RubyUnits::Unit.new(STDIN) }.to raise_error(ArgumentError, 'Invalid Unit Format') + end + + specify 'no undefined units' do + expect { RubyUnits::Unit.new('1 mFoo') }.to raise_error(ArgumentError, "'1 mFoo' Unit not recognized") + expect { RubyUnits::Unit.new('1 second/mFoo') }.to raise_error(ArgumentError, "'1 second/mFoo' Unit not recognized") + end + + specify 'no units with powers greater than 19' do + expect { RubyUnits::Unit.new('1 m^20') }.to raise_error(ArgumentError, 'Power out of range (-20 < net power of a unit < 20)') + end + + specify 'no units with powers less than 19' do + expect { RubyUnits::Unit.new('1 m^-20') }.to raise_error(ArgumentError, 'Power out of range (-20 < net power of a unit < 20)') + end + + specify 'no temperatures less than absolute zero' do + expect { RubyUnits::Unit.new('-100 tempK') }.to raise_error(ArgumentError, 'Temperatures must not be less than absolute zero') + expect { RubyUnits::Unit.new('-100 tempR') }.to raise_error(ArgumentError, 'Temperatures must not be less than absolute zero') + expect { RubyUnits::Unit.new('-500/9 tempR') }.to raise_error(ArgumentError, 'Temperatures must not be less than absolute zero') + end + + specify 'no nil scalar' do + expect { RubyUnits::Unit.new(nil, 'feet') }.to raise_error(ArgumentError, 'Invalid Unit Format') + expect { RubyUnits::Unit.new(nil, 'feet', 'min') }.to raise_error(ArgumentError, 'Invalid Unit Format') + end + + specify 'no double prefixes' do + expect { Unit.new('1 mmm') }.to raise_error(ArgumentError, /Unit not recognized/) + end +end + +describe Unit do + it 'is a subclass of Numeric' do + expect(described_class).to be < Numeric + end + + it 'is Comparable' do + expect(described_class).to be < Comparable + end + + describe '#defined?' do + it 'should return true when asked about a defined unit' do + expect(Unit.defined?('meter')).to be_truthy + end + + it 'should return true when asked about an alias for a unit' do + expect(Unit.defined?('m')).to be_truthy + end + + it 'should return false when asked about a unit that is not defined' do + expect(Unit.defined?('doohickey')).to be_falsey + end + end + + describe '#to_yaml' do + subject { RubyUnits::Unit.new('1 mm') } + + describe '#to_yaml' do + subject { super().to_yaml } + it { is_expected.to match(%r{--- !ruby\/object:RubyUnits::Unit}) } + end + end + + describe '#definition' do + context 'The requested unit is defined' do + before(:each) do + @definition = Unit.definition('mph') + end + + it 'should return a Unit::Definition' do + expect(@definition).to be_instance_of(Unit::Definition) + end + + specify { expect(@definition.name).to eq('') } + specify { expect(@definition.aliases).to eq(%w[mph]) } + specify { expect(@definition.numerator).to eq(['']) } + specify { expect(@definition.denominator).to eq(['']) } + specify { expect(@definition.kind).to eq(:speed) } + specify { expect(@definition.scalar).to be === 0.44704 } + end + + context 'The requested unit is not defined' do + it 'should return nil' do + expect(Unit.definition('doohickey')).to be_nil + end + end + end + + describe '#define' do + describe 'a new unit' do + before(:each) do + # do this because the unit is not defined at the time this file is parsed, so it fails + @jiffy = Unit.define('jiffy') do |jiffy| + jiffy.scalar = Rational(1, 100) + jiffy.aliases = %w[jif] + jiffy.numerator = [''] + jiffy.kind = :time + end + end + + after(:each) do + Unit.undefine!('jiffy') + end + + describe "RubyUnits::Unit.new('1e6 jiffy')" do + subject { RubyUnits::Unit.new('1e6 jiffy') } + + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1e6) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('jif') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:time) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + + describe '#base' do + subject { super().base } + it { is_expected.to eq(RubyUnits::Unit.new('10000 s')) } + end + end + + it 'should register the new unit' do + expect(Unit.defined?('jiffy')).to be_truthy + end + end + + describe 'an existing unit again' do + before(:each) do + @cups = Unit.definition('cup') + @original_display_name = @cups.display_name + @cups.display_name = 'cupz' + Unit.define(@cups) + end + + after(:each) do + Unit.redefine!('cup') do |cup| + cup.display_name = @original_display_name + end + end + + describe "RubyUnits::Unit.new('1 cup')" do + # do this because the unit is going to be redefined + subject { RubyUnits::Unit.new('1 cup') } + + it { is_expected.to be_a Numeric } + it { is_expected.to be_an_instance_of Unit } + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to eq(1) } + end + + describe '#scalar' do + subject { super().scalar } + it { is_expected.to be_an Integer } + end + + describe '#units' do + subject { super().units } + it { is_expected.to eq('cupz') } + end + + describe '#kind' do + subject { super().kind } + it { is_expected.to eq(:volume) } + end + it { is_expected.not_to be_temperature } + it { is_expected.not_to be_degree } + it { is_expected.not_to be_base } + it { is_expected.not_to be_unitless } + it { is_expected.not_to be_zero } + end + end + end + + describe '#redefine!' do + before(:each) do + @jiffy = Unit.define('jiffy') do |jiffy| + jiffy.scalar = (1 / 100) + jiffy.aliases = %w[jif] + jiffy.numerator = [''] + jiffy.kind = :time + end + + Unit.redefine!('jiffy') do |jiffy| + jiffy.scalar = (1 / 1000) + end + end + + after(:each) do + Unit.undefine!('jiffy') + end + + specify { expect(RubyUnits::Unit.new('1 jiffy').to_base.scalar).to eq(1 / 1000) } + end + + describe '#undefine!' do + before(:each) do + @jiffy = Unit.define('jiffy') do |jiffy| + jiffy.scalar = (1 / 100) + jiffy.aliases = %w[jif] + jiffy.numerator = [''] + jiffy.kind = :time + end + Unit.undefine!('jiffy') + end + + specify 'the unit should be undefined' do + expect(Unit.defined?('jiffy')).to be_falsey + end + + specify 'attempting to use an undefined unit fails' do + expect { RubyUnits::Unit.new('1 jiffy') }.to raise_exception(ArgumentError) + end + + it 'should return true when undefining an unknown unit' do + expect(Unit.defined?('unknown')).to be_falsey + expect(Unit.undefine!('unknown')).to be_truthy + end + end + + describe '#clone' do + subject { RubyUnits::Unit.new('1 mm') } + + describe '#clone' do + subject { super().clone } + it { is_expected.to be === subject } + end + end +end + +describe 'Unit Comparisons' do + context "Unit should detect if two units are 'compatible' (i.e., can be converted into each other)" do + specify { expect(RubyUnits::Unit.new('1 ft') =~ RubyUnits::Unit.new('1 m')).to be true } + specify { expect(RubyUnits::Unit.new('1 ft') =~ 'm').to be true } + specify { expect(RubyUnits::Unit.new('1 ft')).to be_compatible_with RubyUnits::Unit.new('1 m') } + specify { expect(RubyUnits::Unit.new('1 ft')).to be_compatible_with 'm' } + specify { expect(RubyUnits::Unit.new('1 m')).to be_compatible_with RubyUnits::Unit.new('1 kg*m/kg') } + specify { expect(RubyUnits::Unit.new('1 ft') =~ RubyUnits::Unit.new('1 kg')).to be false } + specify { expect(RubyUnits::Unit.new('1 ft')).not_to be_compatible_with RubyUnits::Unit.new('1 kg') } + specify { expect(RubyUnits::Unit.new('1 ft')).not_to be_compatible_with nil } + end + + context 'Equality' do + context 'with uncoercable objects' do + specify { expect(RubyUnits::Unit.new('1 mm')).not_to eq(nil) } + end + + context 'units of same kind' do + specify { expect(RubyUnits::Unit.new('1000 m')).to eq(RubyUnits::Unit.new('1 km')) } + specify { expect(RubyUnits::Unit.new('100 m')).not_to eq(RubyUnits::Unit.new('1 km')) } + specify { expect(RubyUnits::Unit.new('1 m')).to eq(RubyUnits::Unit.new('100 cm')) } + end + + context 'units of incompatible types' do + specify { expect(RubyUnits::Unit.new('1 m')).not_to eq(RubyUnits::Unit.new('1 kg')) } + end + + context 'units with a zero scalar are equal' do + specify { expect(RubyUnits::Unit.new('0 m')).to eq(RubyUnits::Unit.new('0 s')) } + specify { expect(RubyUnits::Unit.new('0 m')).to eq(RubyUnits::Unit.new('0 kg')) } + + context 'except for temperature units' do + specify { expect(RubyUnits::Unit.new('0 tempK')).to eq(RubyUnits::Unit.new('0 m')) } + specify { expect(RubyUnits::Unit.new('0 tempR')).to eq(RubyUnits::Unit.new('0 m')) } + specify { expect(RubyUnits::Unit.new('0 tempC')).not_to eq(RubyUnits::Unit.new('0 m')) } + specify { expect(RubyUnits::Unit.new('0 tempF')).not_to eq(RubyUnits::Unit.new('0 m')) } + end + end + end + + context 'Equivalence' do + context 'units and scalars are the exactly the same' do + specify { expect(RubyUnits::Unit.new('1 m')).to be === RubyUnits::Unit.new('1 m') } + specify { expect(RubyUnits::Unit.new('1 m')).to be_same RubyUnits::Unit.new('1 m') } + specify { expect(RubyUnits::Unit.new('1 m')).to be_same_as RubyUnits::Unit.new('1 m') } + end + + context 'units are compatible but not identical' do + specify { expect(RubyUnits::Unit.new('1000 m')).not_to be === RubyUnits::Unit.new('1 km') } + specify { expect(RubyUnits::Unit.new('1000 m')).not_to be_same RubyUnits::Unit.new('1 km') } + specify { expect(RubyUnits::Unit.new('1000 m')).not_to be_same_as RubyUnits::Unit.new('1 km') } + end + + context 'units are not compatible' do + specify { expect(RubyUnits::Unit.new('1000 m')).not_to be === RubyUnits::Unit.new('1 hour') } + specify { expect(RubyUnits::Unit.new('1000 m')).not_to be_same RubyUnits::Unit.new('1 hour') } + specify { expect(RubyUnits::Unit.new('1000 m')).not_to be_same_as RubyUnits::Unit.new('1 hour') } + end + + context 'scalars are different' do + specify { expect(RubyUnits::Unit.new('1 m')).not_to be === RubyUnits::Unit.new('2 m') } + specify { expect(RubyUnits::Unit.new('1 m')).not_to be_same RubyUnits::Unit.new('2 m') } + specify { expect(RubyUnits::Unit.new('1 m')).not_to be_same_as RubyUnits::Unit.new('2 m') } + end + + specify { expect(RubyUnits::Unit.new('1 m')).not_to be_nil } + end + + context 'Comparisons' do + context 'compatible units can be compared' do + specify { expect(RubyUnits::Unit.new('1 m')).to be < RubyUnits::Unit.new('2 m') } + specify { expect(RubyUnits::Unit.new('2 m')).to be > RubyUnits::Unit.new('1 m') } + specify { expect(RubyUnits::Unit.new('1 m')).to be < RubyUnits::Unit.new('1 mi') } + specify { expect(RubyUnits::Unit.new('2 m')).to be > RubyUnits::Unit.new('1 ft') } + specify { expect(RubyUnits::Unit.new('70 tempF')).to be > RubyUnits::Unit.new('10 degC') } + specify { expect(RubyUnits::Unit.new('1 m')).to be > 0 } + specify { expect { RubyUnits::Unit.new('1 m') > nil }.to raise_error(ArgumentError, /comparison of RubyUnits::Unit with (nil failed|NilClass)/) } + end + + context 'incompatible units cannot be compared' do + specify { expect { RubyUnits::Unit.new('1 m') < RubyUnits::Unit.new('1 liter') }.to raise_error(ArgumentError, "Incompatible Units ('m' not compatible with 'l')") } + specify { expect { RubyUnits::Unit.new('1 kg') > RubyUnits::Unit.new('60 mph') }.to raise_error(ArgumentError, "Incompatible Units ('kg' not compatible with 'mph')") } + end + + context 'with coercions should be valid' do + specify { expect(RubyUnits::Unit.new('1GB') > '500MB').to eq(true) } + specify { expect(RubyUnits::Unit.new('0.5GB') < '900MB').to eq(true) } + end + end +end + +describe 'Unit Conversions' do + context 'between compatible units' do + specify { expect(RubyUnits::Unit.new('1 s').convert_to('ns')).to eq(RubyUnits::Unit.new('1e9 ns')) } + specify { expect(RubyUnits::Unit.new('1 s').convert_to('ns')).to eq(RubyUnits::Unit.new('1e9 ns')) } + specify { expect(RubyUnits::Unit.new('1 s') >> 'ns').to eq(RubyUnits::Unit.new('1e9 ns')) } + + specify { expect(RubyUnits::Unit.new('1 m').convert_to(RubyUnits::Unit.new('ft'))).to be_within(RubyUnits::Unit.new('0.001 ft')).of(RubyUnits::Unit.new('3.28084 ft')) } + end + + context 'between incompatible units' do + specify { expect { RubyUnits::Unit.new('1 s').convert_to('m') }.to raise_error(ArgumentError, "Incompatible Units ('1 s' not compatible with 'm')") } + end + + context 'given bad input' do + specify { expect { RubyUnits::Unit.new('1 m').convert_to('random string') }.to raise_error(ArgumentError, "'random string' Unit not recognized") } + specify { expect { RubyUnits::Unit.new('1 m').convert_to(STDOUT) }.to raise_error(ArgumentError, 'Unknown target units') } + end + + context 'between temperature scales' do + # note that 'temp' units are for temperature readings on a scale, while 'deg' units are used to represent + # differences between temperatures, offsets, or other differential temperatures. + + specify { expect(RubyUnits::Unit.new('100 tempC')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('373.15 tempK')) } + specify { expect(RubyUnits::Unit.new('0 tempC')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('273.15 tempK')) } + specify { expect(RubyUnits::Unit.new('37 tempC')).to be_within(RubyUnits::Unit.new('0.01 degK')).of(RubyUnits::Unit.new('310.15 tempK')) } + specify { expect(RubyUnits::Unit.new('-273.15 tempC')).to eq(RubyUnits::Unit.new('0 tempK')) } + + specify { expect(RubyUnits::Unit.new('212 tempF')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('373.15 tempK')) } + specify { expect(RubyUnits::Unit.new('32 tempF')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('273.15 tempK')) } + specify { expect(RubyUnits::Unit.new('98.6 tempF')).to be_within(RubyUnits::Unit.new('0.01 degK')).of(RubyUnits::Unit.new('310.15 tempK')) } + specify { expect(RubyUnits::Unit.new('-459.67 tempF')).to eq(RubyUnits::Unit.new('0 tempK')) } + + specify { expect(RubyUnits::Unit.new('671.67 tempR')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('373.15 tempK')) } + specify { expect(RubyUnits::Unit.new('491.67 tempR')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('273.15 tempK')) } + specify { expect(RubyUnits::Unit.new('558.27 tempR')).to be_within(RubyUnits::Unit.new('0.01 degK')).of(RubyUnits::Unit.new('310.15 tempK')) } + specify { expect(RubyUnits::Unit.new('0 tempR')).to eq(RubyUnits::Unit.new('0 tempK')) } + + specify { expect(RubyUnits::Unit.new('100 tempK').convert_to('tempC')).to be_within(RubyUnits::Unit.new('0.01 degC')).of(RubyUnits::Unit.new('-173.15 tempC')) } + specify { expect(RubyUnits::Unit.new('100 tempK').convert_to('tempF')).to be_within(RubyUnits::Unit.new('0.01 degF')).of(RubyUnits::Unit.new('-279.67 tempF')) } + specify { expect(RubyUnits::Unit.new('100 tempK').convert_to('tempR')).to be_within(RubyUnits::Unit.new('0.01 degR')).of(RubyUnits::Unit.new('180 tempR')) } + + specify { expect(RubyUnits::Unit.new('1 degC')).to eq(RubyUnits::Unit.new('1 degK')) } + specify { expect(RubyUnits::Unit.new('1 degF')).to eq(RubyUnits::Unit.new('1 degR')) } + specify { expect(RubyUnits::Unit.new('1 degC')).to eq(RubyUnits::Unit.new('1.8 degR')) } + specify { expect(RubyUnits::Unit.new('1 degF')).to be_within(RubyUnits::Unit.new('0.001 degK')).of(RubyUnits::Unit.new('0.5555 degK')) } + end + + context 'reported bugs' do + specify { expect(RubyUnits::Unit.new('189 Mtonne') * RubyUnits::Unit.new('1189 g/tonne')).to eq(RubyUnits::Unit.new('224721 tonne')) } + specify { expect((RubyUnits::Unit.new('189 Mtonne') * RubyUnits::Unit.new('1189 g/tonne')).convert_to('tonne')).to eq(RubyUnits::Unit.new('224721 tonne')) } + end + + describe 'Foot-inch conversions' do + [ + ['76 in', %(6'4")], + ['77 in', %(6'5")], + ['78 in', %(6'6")], + ['79 in', %(6'7")], + ['80 in', %(6'8")], + ['87 in', %(7'3")], + ['88 in', %(7'4")], + ['89 in', %(7'5")] + ].each do |inches, feet| + specify { expect(RubyUnits::Unit.new(inches).convert_to('ft')).to eq(RubyUnits::Unit.new(feet)) } + specify { expect(RubyUnits::Unit.new(inches).to_s(:ft)).to eq(feet) } + end + end + + describe 'pound-ounce conversions' do + [ + ['76 oz', '4 lbs, 12 oz'], + ['77 oz', '4 lbs, 13 oz'], + ['78 oz', '4 lbs, 14 oz'], + ['79 oz', '4 lbs, 15 oz'], + ['80 oz', '5 lbs, 0 oz'], + ['87 oz', '5 lbs, 7 oz'], + ['88 oz', '5 lbs, 8 oz'], + ['89 oz', '5 lbs, 9 oz'] + ].each do |ounces, pounds| + specify { expect(RubyUnits::Unit.new(ounces).convert_to('lbs')).to eq(RubyUnits::Unit.new(pounds)) } + specify { expect(RubyUnits::Unit.new(ounces).to_s(:lbs)).to eq(pounds) } + end + end + + describe 'stone-pound conversions' do + [ + ['14 stone 4', '200 lbs'], + ['14 st 4', '200 lbs'], + ['14 stone, 4 pounds', '200 lbs'], + ['14 st, 4 lbs', '200 lbs'] + ].each do |stone, pounds| + specify { expect(RubyUnits::Unit.new(stone).convert_to('lbs')).to eq(RubyUnits::Unit.new(pounds)) } + end + specify { expect(RubyUnits::Unit.new('200 lbs').to_s(:stone)).to eq '14 stone, 4 lb' } + end +end + +describe 'Unit Math' do + context 'operators:' do + context 'addition (+)' do + context 'between compatible units' do + specify { expect(RubyUnits::Unit.new('0 m') + RubyUnits::Unit.new('10 m')).to eq(RubyUnits::Unit.new('10 m')) } + specify { expect(RubyUnits::Unit.new('5 kg') + RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('15 kg')) } + end + + context 'between a zero unit and another unit' do + specify { expect(RubyUnits::Unit.new('0 kg') + RubyUnits::Unit.new('10 m')).to eq(RubyUnits::Unit.new('10 m')) } + specify { expect(RubyUnits::Unit.new('0 m') + RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('10 kg')) } + end + + context 'between incompatible units' do + specify { expect { RubyUnits::Unit.new('10 kg') + RubyUnits::Unit.new('10 m') }.to raise_error(ArgumentError) } + specify { expect { RubyUnits::Unit.new('10 m') + RubyUnits::Unit.new('10 kg') }.to raise_error(ArgumentError) } + specify { expect { RubyUnits::Unit.new('10 m') + nil }.to raise_error(ArgumentError) } + end + + context 'a number from a unit' do + specify { expect { RubyUnits::Unit.new('10 kg') + 1 }.to raise_error(ArgumentError) } + specify { expect { 10 + RubyUnits::Unit.new('10 kg') }.to raise_error(ArgumentError) } + end + + context 'between a unit and coerceable types' do + specify { expect(RubyUnits::Unit.new('10 kg') + %w[1 kg]).to eq(RubyUnits::Unit.new('11 kg')) } + specify { expect(RubyUnits::Unit.new('10 kg') + '1 kg').to eq(RubyUnits::Unit.new('11 kg')) } + end + + context 'between two temperatures' do + specify { expect { (RubyUnits::Unit.new('100 tempK') + RubyUnits::Unit.new('100 tempK')) }.to raise_error(ArgumentError, 'Cannot add two temperatures') } + end + + context 'between a temperature and a degree' do + specify { expect(RubyUnits::Unit.new('100 tempK') + RubyUnits::Unit.new('100 degK')).to eq(RubyUnits::Unit.new('200 tempK')) } + end + + context 'between a degree and a temperature' do + specify { expect(RubyUnits::Unit.new('100 degK') + RubyUnits::Unit.new('100 tempK')).to eq(RubyUnits::Unit.new('200 tempK')) } + end + + context 'between two unitless units' do + specify { expect(RubyUnits::Unit.new('1') + RubyUnits::Unit.new('2')).to eq 3 } + end + end + + context 'subtracting (-)' do + context 'compatible units' do + specify { expect(RubyUnits::Unit.new('0 m') - RubyUnits::Unit.new('10 m')).to eq(RubyUnits::Unit.new('-10 m')) } + specify { expect(RubyUnits::Unit.new('5 kg') - RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('-5 kg')) } + end + + context 'a unit from a zero unit' do + specify { expect(RubyUnits::Unit.new('0 kg') - RubyUnits::Unit.new('10 m')).to eq(RubyUnits::Unit.new('-10 m')) } + specify { expect(RubyUnits::Unit.new('0 m') - RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('-10 kg')) } + end + + context 'incompatible units' do + specify { expect { RubyUnits::Unit.new('10 kg') - RubyUnits::Unit.new('10 m') }.to raise_error(ArgumentError) } + specify { expect { RubyUnits::Unit.new('10 m') - RubyUnits::Unit.new('10 kg') }.to raise_error(ArgumentError) } + specify { expect { RubyUnits::Unit.new('10 m') - nil }.to raise_error(ArgumentError) } + end + + context 'between a unit and coerceable types' do + specify { expect(RubyUnits::Unit.new('10 kg') - %w[1 kg]).to eq(RubyUnits::Unit.new('9 kg')) } + specify { expect(RubyUnits::Unit.new('10 kg') - '1 kg').to eq(RubyUnits::Unit.new('9 kg')) } + end + + context 'a number from a unit' do + specify { expect { RubyUnits::Unit.new('10 kg') - 1 }.to raise_error(ArgumentError) } + specify { expect { 10 - RubyUnits::Unit.new('10 kg') }.to raise_error(ArgumentError) } + end + + context 'between two temperatures' do + specify { expect(RubyUnits::Unit.new('100 tempK') - RubyUnits::Unit.new('100 tempK')).to eq(RubyUnits::Unit.new('0 degK')) } + end + + context 'between a temperature and a degree' do + specify { expect(RubyUnits::Unit.new('100 tempK') - RubyUnits::Unit.new('100 degK')).to eq(RubyUnits::Unit.new('0 tempK')) } + end + + context 'between a degree and a temperature' do + specify { expect { (RubyUnits::Unit.new('100 degK') - RubyUnits::Unit.new('100 tempK')) }.to raise_error(ArgumentError, 'Cannot subtract a temperature from a differential degree unit') } + end + + context 'a unitless unit from another unitless unit' do + specify { expect(RubyUnits::Unit.new('1') - RubyUnits::Unit.new('2')).to eq -1 } + end + end + + context 'multiplying (*)' do + context 'between compatible units' do + specify { expect(RubyUnits::Unit.new('0 m') * RubyUnits::Unit.new('10 m')).to eq(RubyUnits::Unit.new('0 m^2')) } + specify { expect(RubyUnits::Unit.new('5 kg') * RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('50 kg^2')) } + end + + context 'between incompatible units' do + specify { expect(RubyUnits::Unit.new('0 m') * RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('0 kg*m')) } + specify { expect(RubyUnits::Unit.new('5 m') * RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('50 kg*m')) } + specify { expect { RubyUnits::Unit.new('10 m') * nil }.to raise_error(ArgumentError) } + end + + context 'between a unit and coerceable types' do + specify { expect(RubyUnits::Unit.new('10 kg') * %w[1 kg]).to eq(RubyUnits::Unit.new('10 kg^2')) } + specify { expect(RubyUnits::Unit.new('10 kg') * '1 kg').to eq(RubyUnits::Unit.new('10 kg^2')) } + end + + context 'by a temperature' do + specify { expect { RubyUnits::Unit.new('5 kg') * RubyUnits::Unit.new('100 tempF') }.to raise_exception(ArgumentError) } + end + + context 'by a number' do + specify { expect(10 * RubyUnits::Unit.new('5 kg')).to eq(RubyUnits::Unit.new('50 kg')) } + end + end + + context 'dividing (/)' do + context 'compatible units' do + specify { expect(RubyUnits::Unit.new('0 m') / RubyUnits::Unit.new('10 m')).to eq(RubyUnits::Unit.new(0)) } + specify { expect(RubyUnits::Unit.new('5 kg') / RubyUnits::Unit.new('10 kg')).to eq(Rational(1, 2)) } + specify { expect(RubyUnits::Unit.new('5 kg') / RubyUnits::Unit.new('5 kg')).to eq(1) } + end + + context 'incompatible units' do + specify { expect(RubyUnits::Unit.new('0 m') / RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('0 m/kg')) } + specify { expect(RubyUnits::Unit.new('5 m') / RubyUnits::Unit.new('10 kg')).to eq(RubyUnits::Unit.new('1/2 m/kg')) } + specify { expect { RubyUnits::Unit.new('10 m') / nil }.to raise_error(ArgumentError) } + end + + context 'between a unit and coerceable types' do + specify { expect(RubyUnits::Unit.new('10 kg^2') / %w[1 kg]).to eq(RubyUnits::Unit.new('10 kg')) } + specify { expect(RubyUnits::Unit.new('10 kg^2') / '1 kg').to eq(RubyUnits::Unit.new('10 kg')) } + end + + context 'by a temperature' do + specify { expect { RubyUnits::Unit.new('5 kg') / RubyUnits::Unit.new('100 tempF') }.to raise_exception(ArgumentError) } + end + + context 'a number by a unit' do + specify { expect(10 / RubyUnits::Unit.new('5 kg')).to eq(RubyUnits::Unit.new('2 1/kg')) } + end + + context 'a unit by a number' do + specify { expect(RubyUnits::Unit.new('5 kg') / 2).to eq(RubyUnits::Unit.new('2.5 kg')) } + end + + context 'by zero' do + specify { expect { RubyUnits::Unit.new('10 m') / 0 }.to raise_error(ZeroDivisionError) } + specify { expect { RubyUnits::Unit.new('10 m') / RubyUnits::Unit.new('0 m') }.to raise_error(ZeroDivisionError) } + specify { expect { RubyUnits::Unit.new('10 m') / RubyUnits::Unit.new('0 kg') }.to raise_error(ZeroDivisionError) } + end + end + + context 'exponentiating (**)' do + specify 'a temperature raises an execption' do + expect { RubyUnits::Unit.new('100 tempK')**2 }.to raise_error(ArgumentError, 'Cannot raise a temperature to a power') + end + + context RubyUnits::Unit.new('0 m') do + it { expect(subject**1).to eq(subject) } + it { expect(subject**2).to eq(subject) } + end + + context RubyUnits::Unit.new('1 m') do + it { expect(subject**0).to eq(1) } + it { expect(subject**1).to eq(subject) } + it { expect(subject**-1).to eq(1 / subject) } + it { expect(subject**2).to eq(RubyUnits::Unit.new('1 m^2')) } + it { expect(subject**-2).to eq(RubyUnits::Unit.new('1 1/m^2')) } + specify { expect { subject**Rational(1, 2) }.to raise_error(ArgumentError, 'Illegal root') } + # because 1 m^(1/2) doesn't make any sense + specify { expect { subject**Complex(1, 1) }.to raise_error(ArgumentError, 'exponentiation of complex numbers is not supported.') } + specify { expect { subject**RubyUnits::Unit.new('1 m') }.to raise_error(ArgumentError, 'Invalid Exponent') } + end + + context RubyUnits::Unit.new('1 m^2') do + it { expect(subject**Rational(1, 2)).to eq(RubyUnits::Unit.new('1 m')) } + it { expect(subject**0.5).to eq(RubyUnits::Unit.new('1 m')) } + + specify { expect { subject**0.12345 }.to raise_error(ArgumentError, 'Not a n-th root (1..9), use 1/n') } + specify { expect { subject**'abcdefg' }.to raise_error(ArgumentError, 'Invalid Exponent') } + end + end + + context 'modulo (%)' do + context 'compatible units' do + specify { expect(RubyUnits::Unit.new('2 m') % RubyUnits::Unit.new('1 m')).to eq(0) } + specify { expect(RubyUnits::Unit.new('5 m') % RubyUnits::Unit.new('2 m')).to eq(1) } + end + + specify 'incompatible units raises an exception' do + expect { RubyUnits::Unit.new('1 m') % RubyUnits::Unit.new('1 kg') }.to raise_error(ArgumentError, "Incompatible Units ('1 m' not compatible with '1 kg')") + end + end + + context 'unary negation (-)' do + specify { expect(-RubyUnits::Unit.new('1 mm')).to eq(RubyUnits::Unit.new('-1 mm')) } + end + + context 'unary plus (+)' do + specify { expect(+RubyUnits::Unit.new('1 mm')).to eq(RubyUnits::Unit.new('1 mm')) } + end + end + + context '#power' do + subject { RubyUnits::Unit.new('1 m') } + it 'raises an exception when passed a Float argument' do + expect { subject.power(1.5) }.to raise_error(ArgumentError, 'Exponent must an Integer') + end + it 'raises an exception when passed a Rational argument' do + expect { subject.power(Rational(1, 2)) }.to raise_error(ArgumentError, 'Exponent must an Integer') + end + it 'raises an exception when passed a Complex argument' do + expect { subject.power(Complex(1, 2)) }.to raise_error(ArgumentError, 'Exponent must an Integer') + end + it 'raises an exception when called on a temperature unit' do + expect { RubyUnits::Unit.new('100 tempC').power(2) }.to raise_error(ArgumentError, 'Cannot raise a temperature to a power') + end + + specify { expect(subject.power(-1)).to eq(RubyUnits::Unit.new('1 1/m')) } + specify { expect(subject.power(0)).to eq(1) } + specify { expect(subject.power(1)).to eq(subject) } + specify { expect(subject.power(2)).to eq(RubyUnits::Unit.new('1 m^2')) } + end + + context '#root' do + subject { RubyUnits::Unit.new('1 m') } + it 'raises an exception when passed a Float argument' do + expect { subject.root(1.5) }.to raise_error(ArgumentError, 'Exponent must an Integer') + end + it 'raises an exception when passed a Rational argument' do + expect { subject.root(Rational(1, 2)) }.to raise_error(ArgumentError, 'Exponent must an Integer') + end + it 'raises an exception when passed a Complex argument' do + expect { subject.root(Complex(1, 2)) }.to raise_error(ArgumentError, 'Exponent must an Integer') + end + it 'raises an exception when called on a temperature unit' do + expect { RubyUnits::Unit.new('100 tempC').root(2) }.to raise_error(ArgumentError, 'Cannot take the root of a temperature') + end + + specify { expect(RubyUnits::Unit.new('1 m^2').root(-2)).to eq(RubyUnits::Unit.new('1 1/m')) } + specify { expect(subject.root(-1)).to eq(RubyUnits::Unit.new('1 1/m')) } + specify { expect { subject.root(0) }.to raise_error(ArgumentError, '0th root undefined') } + specify { expect(subject.root(1)).to eq(subject) } + specify { expect(RubyUnits::Unit.new('1 m^2').root(2)).to eq(RubyUnits::Unit.new('1 m')) } + end + + context '#inverse' do + specify { expect(RubyUnits::Unit.new('1 m').inverse).to eq(RubyUnits::Unit.new('1 1/m')) } + specify { expect { RubyUnits::Unit.new('100 tempK').inverse }.to raise_error(ArgumentError, 'Cannot divide with temperatures') } + end + + context 'convert to scalars' do + specify { expect(RubyUnits::Unit.new('10').to_i).to be_kind_of(Integer) } + specify { expect { RubyUnits::Unit.new('10 m').to_i }.to raise_error(RuntimeError, "Cannot convert '10 m' to Integer unless unitless. Use Unit#scalar") } + + specify { expect(RubyUnits::Unit.new('10.0').to_f).to be_kind_of(Float) } + specify { expect { RubyUnits::Unit.new('10.0 m').to_f }.to raise_error(RuntimeError, "Cannot convert '10 m' to Float unless unitless. Use Unit#scalar") } + + specify { expect(RubyUnits::Unit.new('1+1i').to_c).to be_kind_of(Complex) } + specify { expect { RubyUnits::Unit.new('1+1i m').to_c }.to raise_error(RuntimeError, "Cannot convert '1.0+1.0i m' to Complex unless unitless. Use Unit#scalar") } + + specify { expect(RubyUnits::Unit.new('3/7').to_r).to be_kind_of(Rational) } + specify { expect { RubyUnits::Unit.new('3/7 m').to_r }.to raise_error(RuntimeError, "Cannot convert '3/7 m' to Rational unless unitless. Use Unit#scalar") } + end + + context 'absolute value (#abs)' do + context 'of a unitless unit' do + specify 'returns the absolute value of the scalar' do + expect(RubyUnits::Unit.new('-10').abs).to eq(10) + end + end + + context 'of a unit' do + specify 'returns a unit with the absolute value of the scalar' do + expect(RubyUnits::Unit.new('-10 m').abs).to eq(RubyUnits::Unit.new('10 m')) + end + end + end + + context '#ceil' do + context 'of a unitless unit' do + specify 'returns the ceil of the scalar' do + expect(RubyUnits::Unit.new('10.1').ceil).to eq(11) + end + end + + context 'of a unit' do + specify 'returns a unit with the ceil of the scalar' do + expect(RubyUnits::Unit.new('10.1 m').ceil).to eq(RubyUnits::Unit.new('11 m')) + end + end + end + + context '#floor' do + context 'of a unitless unit' do + specify 'returns the floor of the scalar' do + expect(RubyUnits::Unit.new('10.1').floor).to eq(10) + end + end + + context 'of a unit' do + specify 'returns a unit with the floor of the scalar' do + expect(RubyUnits::Unit.new('10.1 m').floor).to eq(RubyUnits::Unit.new('10 m')) + end + end + end + + context '#round' do + context 'of a unitless unit' do + specify 'returns the round of the scalar' do + expect(RubyUnits::Unit.new('10.5').round).to eq(11) + end + end + + context 'of a unit' do + specify 'returns a unit with the round of the scalar' do + expect(RubyUnits::Unit.new('10.5 m').round).to eq(RubyUnits::Unit.new('11 m')) + end + end + end + + context '#truncate' do + context 'of a unitless unit' do + specify 'returns the truncate of the scalar' do + expect(RubyUnits::Unit.new('10.5').truncate).to eq(10) + end + end + + context 'of a unit' do + specify 'returns a unit with the truncate of the scalar' do + expect(RubyUnits::Unit.new('10.5 m').truncate).to eq(RubyUnits::Unit.new('10 m')) + end + end + + context 'of a complex unit' do + specify 'returns a unit with the truncate of the scalar' do + expect(RubyUnits::Unit.new('10.5 kg*m/s^3').truncate).to eq(RubyUnits::Unit.new('10 kg*m/s^3')) + end + end + end + + context '#zero?' do + it 'is true when the scalar is zero on the base scale' do + expect(RubyUnits::Unit.new('0')).to be_zero + expect(RubyUnits::Unit.new('0 mm')).to be_zero + expect(RubyUnits::Unit.new('-273.15 tempC')).to be_zero + end + + it 'is false when the scalar is not zero' do + expect(RubyUnits::Unit.new('1')).not_to be_zero + expect(RubyUnits::Unit.new('1 mm')).not_to be_zero + expect(RubyUnits::Unit.new('0 tempC')).not_to be_zero + end + end + + context '#succ' do + specify { expect(RubyUnits::Unit.new('1').succ).to eq(RubyUnits::Unit.new('2')) } + specify { expect(RubyUnits::Unit.new('1 mm').succ).to eq(RubyUnits::Unit.new('2 mm')) } + specify { expect(RubyUnits::Unit.new('1 mm').next).to eq(RubyUnits::Unit.new('2 mm')) } + specify { expect(RubyUnits::Unit.new('-1 mm').succ).to eq(RubyUnits::Unit.new('0 mm')) } + specify { expect { RubyUnits::Unit.new('1.5 mm').succ }.to raise_error(ArgumentError, 'Non Integer Scalar') } + end + + context '#pred' do + specify { expect(RubyUnits::Unit.new('1').pred).to eq(RubyUnits::Unit.new('0')) } + specify { expect(RubyUnits::Unit.new('1 mm').pred).to eq(RubyUnits::Unit.new('0 mm')) } + specify { expect(RubyUnits::Unit.new('-1 mm').pred).to eq(RubyUnits::Unit.new('-2 mm')) } + specify { expect { RubyUnits::Unit.new('1.5 mm').pred }.to raise_error(ArgumentError, 'Non Integer Scalar') } + end + + context '#divmod' do + specify { expect(RubyUnits::Unit.new('5 mm').divmod(RubyUnits::Unit.new('2 mm'))).to eq([2, 1]) } + specify { expect(RubyUnits::Unit.new('1 km').divmod(RubyUnits::Unit.new('2 m'))).to eq([500, 0]) } + specify { expect { RubyUnits::Unit.new('1 m').divmod(RubyUnits::Unit.new('2 kg')) }.to raise_error(ArgumentError, "Incompatible Units ('1 m' not compatible with '2 kg')") } + end + + context '#div' do + specify { expect(RubyUnits::Unit.new('23 m').div(RubyUnits::Unit.new('2 m'))).to eq(11) } + end + + context '#best_prefix' do + specify { expect(RubyUnits::Unit.new('1024 KiB').best_prefix).to eq(RubyUnits::Unit.new('1 MiB')) } + specify { expect(RubyUnits::Unit.new('1000 m').best_prefix).to eq(RubyUnits::Unit.new('1 km')) } + specify { expect { RubyUnits::Unit.new('0 m').best_prefix }.to_not raise_error } + end + + context 'Time helper functions' do + before do + allow(Time).to receive(:now).and_return(Time.utc(2011, 10, 16)) + allow(DateTime).to receive(:now).and_return(DateTime.civil(2011, 10, 16)) + allow(Date).to receive(:today).and_return(Date.civil(2011, 10, 16)) + end + + context '#since' do + specify { expect(RubyUnits::Unit.new('min').since(Time.utc(2001, 4, 1, 0, 0, 0))).to eq(RubyUnits::Unit.new('5544000 min')) } + specify { expect(RubyUnits::Unit.new('min').since(DateTime.civil(2001, 4, 1, 0, 0, 0))).to eq(RubyUnits::Unit.new('5544000 min')) } + specify { expect(RubyUnits::Unit.new('min').since(Date.civil(2001, 4, 1))).to eq(RubyUnits::Unit.new('5544000 min')) } + specify { expect { RubyUnits::Unit.new('min').since('4-1-2001') }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + specify { expect { RubyUnits::Unit.new('min').since(nil) }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + end + + context '#before' do + specify { expect(RubyUnits::Unit.new('5 min').before(Time.now)).to eq(Time.utc(2011, 10, 15, 23, 55)) } + specify { expect(RubyUnits::Unit.new('5 min').before(DateTime.now)).to eq(DateTime.civil(2011, 10, 15, 23, 55)) } + specify { expect(RubyUnits::Unit.new('5 min').before(Date.today)).to eq(DateTime.civil(2011, 10, 15, 23, 55)) } + specify { expect { RubyUnits::Unit.new('5 min').before(nil) }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + specify { expect { RubyUnits::Unit.new('5 min').before('12:00') }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + end + + context '#ago' do + specify { expect(RubyUnits::Unit.new('5 min').ago).to be_kind_of Time } + specify { expect(RubyUnits::Unit.new('10000 y').ago).to be_kind_of Time } + specify { expect(RubyUnits::Unit.new('1 year').ago).to eq(Time.utc(2010, 10, 16)) } + end + + context '#until' do + specify { expect(RubyUnits::Unit.new('min').until(Date.civil(2011, 10, 17))).to eq(RubyUnits::Unit.new('1440 min')) } + specify { expect(RubyUnits::Unit.new('min').until(DateTime.civil(2011, 10, 21))).to eq(RubyUnits::Unit.new('7200 min')) } + specify { expect(RubyUnits::Unit.new('min').until(Time.utc(2011, 10, 21))).to eq(RubyUnits::Unit.new('7200 min')) } + specify { expect { RubyUnits::Unit.new('5 min').until(nil) }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + specify { expect { RubyUnits::Unit.new('5 min').until('12:00') }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + end + + context '#from' do + specify { expect(RubyUnits::Unit.new('1 day').from(Date.civil(2011, 10, 17))).to eq(Date.civil(2011, 10, 18)) } + specify { expect(RubyUnits::Unit.new('5 min').from(DateTime.civil(2011, 10, 21))).to eq(DateTime.civil(2011, 10, 21, 0, 5)) } + specify { expect(RubyUnits::Unit.new('5 min').from(Time.utc(2011, 10, 21))).to eq(Time.utc(2011, 10, 21, 0, 5)) } + specify { expect { RubyUnits::Unit.new('5 min').from(nil) }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + specify { expect { RubyUnits::Unit.new('5 min').from('12:00') }.to raise_error(ArgumentError, 'Must specify a Time, Date, or DateTime') } + end + end +end + +describe 'Unit Output formatting' do + context RubyUnits::Unit.new('10.5 m/s^2') do + specify { expect(subject.to_s).to eq('10.5 m/s^2') } + specify { expect(subject.to_s('%0.2f')).to eq('10.50 m/s^2') } + specify { expect(subject.to_s('%0.2e km/s^2')).to eq('1.05e-02 km/s^2') } + specify { expect(subject.to_s('km/s^2')).to eq('0.0105 km/s^2') } + specify { expect(subject.to_s(STDOUT)).to eq('10.5 m/s^2') } + specify { expect { subject.to_s('random string') }.to raise_error(ArgumentError, "'random' Unit not recognized") } + end + + context 'for a unit with a custom display_name' do + before(:each) do + Unit.redefine!('cup') do |cup| + cup.display_name = 'cupz' + end + end + + after(:each) do + Unit.redefine!('cup') do |cup| + cup.display_name = cup.aliases.first + end + end + + subject { Unit.new('8 cups') } + + specify { expect(subject.to_s).to eq('8 cupz') } + end +end + +describe 'Equations with Units' do + context 'Ideal Gas Law' do + let(:p) { RubyUnits::Unit.new('100 kPa') } + let(:v) { RubyUnits::Unit.new('1 m^3') } + let(:n) { RubyUnits::Unit.new('1 mole') } + let(:r) { RubyUnits::Unit.new('8.31451 J/mol*degK') } + specify { expect(((p * v) / (n * r)).convert_to('tempK')).to be_within(RubyUnits::Unit.new('0.1 degK')).of(RubyUnits::Unit.new('12027.2 tempK')) } + end +end + +describe 'Unit hash method' do + context 'should return equal values for identical units' do + let(:kg_unit_1) { RubyUnits::Unit.new('2.2 kg') } + let(:kg_unit_2) { RubyUnits::Unit.new('2.2 kg') } + + specify { expect(kg_unit_1).to eq(kg_unit_2) } + specify { expect(kg_unit_1.hash).to eq(kg_unit_2.hash) } + specify { expect([kg_unit_1, kg_unit_2].uniq.size).to eq(1) } + end + + context 'should return not equal values for differnet units' do + let(:kg_unit) { RubyUnits::Unit.new('2.2 kg') } + let(:lb_unit) { RubyUnits::Unit.new('2.2 lbs') } + + specify { expect(kg_unit).to_not eq(lb_unit) } + specify { expect(kg_unit.hash).to_not eq(lb_unit.hash) } + specify { expect([kg_unit, lb_unit].uniq.size).to eq(2) } + end +end diff --git a/spec/ruby_units/utf-8/unit_spec.rb b/spec/ruby_units/utf-8/unit_spec.rb new file mode 100644 index 00000000..89e92be9 --- /dev/null +++ b/spec/ruby_units/utf-8/unit_spec.rb @@ -0,0 +1,21 @@ +require File.dirname(__FILE__) + '/../../spec_helper' + +describe Unit, 'Degrees' do + context 'when the UTF-8 symbol is used' do + context 'Angles' do + it 'should be a degree' do + expect(RubyUnits::Unit.new("180\u00B0").units).to eq('deg') + end + end + + context 'Temperature' do + it 'should be a degree Celcius' do + expect(RubyUnits::Unit.new("180\u00B0C").units).to eq('degC') + end + + it 'should be a degree Fahrenheit' do + expect(RubyUnits::Unit.new("180\u00B0F").units).to eq('degF') + end + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index a76dcf53..8fe3372a 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,17 +1,19 @@ require 'rubygems' -require 'bundler' -Bundler.setup(:development) +require 'simplecov' +require 'bundler/setup' +Bundler.require(:development, :test) require 'rspec/core' -# Initiate code coverage generation when needed -begin - require 'simplecov' - SimpleCov.start do - add_filter "/spec/" - add_filter "/test/" - skip_token "nocov_19" - end if ENV['COVERAGE'] -rescue LoadError +SimpleCov.start do + add_filter '/spec/' + add_filter '/test/' + skip_token 'nocov_19' end -require File.dirname(__FILE__) + "/../lib/ruby-units" +RSpec.configure do |config| + config.order = :random + config.filter_run_including focus: true + config.run_all_when_everything_filtered = true +end + +require File.dirname(__FILE__) + '/../lib/ruby-units'