├── config ├── flog.yml ├── yardstick.yml ├── devtools.yml ├── flay.yml ├── mutant.yml ├── roodi.yml ├── reek.yml └── rubocop.yml ├── .rspec ├── .rubocop.yml ├── .gitignore ├── circle.yml ├── spec ├── rcov.opts ├── spec_helper.rb └── unit │ └── auom_spec.rb ├── .travis.yml ├── Gemfile ├── lib ├── auom.rb └── auom │ ├── relational.rb │ ├── equalization.rb │ ├── inspection.rb │ ├── algebra.rb │ └── unit.rb ├── Changelog.md ├── auom.gemspec ├── LICENSE ├── .github └── workflows │ └── ci.yml ├── README.md └── test └── unit └── auom_test.rb /config/flog.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 15.1 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /config/yardstick.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 100 3 | -------------------------------------------------------------------------------- /config/devtools.yml: -------------------------------------------------------------------------------- 1 | --- 2 | unit_test_timeout: 2.0 3 | -------------------------------------------------------------------------------- /config/flay.yml: -------------------------------------------------------------------------------- 1 | --- 2 | threshold: 12 3 | total_score: 84 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | AllCops: 2 | DisplayCopNames: true 3 | TargetRubyVersion: 2.5 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /Gemfile.lock 2 | /tmp 3 | /coverage 4 | /.rbx 5 | /.bundle 6 | /vendor 7 | -------------------------------------------------------------------------------- /config/mutant.yml: -------------------------------------------------------------------------------- 1 | --- 2 | integration: minitest 3 | includes: 4 | - lib 5 | requires: 6 | - auom 7 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | --- 2 | machine: 3 | ruby: 4 | version: '2.4.2' 5 | test: 6 | override: 7 | - bundle exec rake ci 8 | -------------------------------------------------------------------------------- /spec/rcov.opts: -------------------------------------------------------------------------------- 1 | --exclude-only "spec/,^/" 2 | --sort coverage 3 | --callsites 4 | --xrefs 5 | --profile 6 | --text-summary 7 | --failure-threshold 100 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | script: 'bundle exec rake ci' 3 | rvm: 4 | - 2.1 5 | - rbx 6 | matrix: 7 | include: 8 | - rvm: jruby 9 | env: JRUBY_OPTS="$JRUBY_OPTS --debug --2.0" 10 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | source 'https://oss:Px2ENN7S91OmWaD5G7MIQJi1dmtmYrEh@gem.mutant.dev' do 6 | gem 'mutant-license' 7 | end 8 | 9 | gemspec 10 | -------------------------------------------------------------------------------- /lib/auom.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'equalizer' 4 | 5 | # Library namespace 6 | module AUOM 7 | end 8 | 9 | require 'auom/algebra' 10 | require 'auom/equalization' 11 | require 'auom/inspection' 12 | require 'auom/relational' 13 | require 'auom/unit' 14 | -------------------------------------------------------------------------------- /lib/auom/relational.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AUOM 4 | # Mixin to add relational operators 5 | module Relational 6 | include Comparable 7 | 8 | # Perform comparison operation 9 | # 10 | # @param [Unit] other 11 | # 12 | # @return [Object] 13 | # 14 | # @api private 15 | # 16 | def <=>(other) 17 | assert_same_unit(other) 18 | 19 | scalar <=> other.scalar 20 | end 21 | end # Relational 22 | end # AUOM 23 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # v0.4.0 2018-11-xx 2 | 3 | * Change to synbolic algebraic operators 4 | 5 | # v0.3.1 2018-11-10 6 | 7 | * Fix to provide operators for real ;) 8 | 9 | # v0.3.0 2018-11-10 10 | 11 | * [breaking-change] Change to provide all relational operators, remove named ones 12 | * [breaking-change] Drop support for ruby < 2.5 13 | 14 | # v0.2.0 2017-09-29 15 | 16 | * [breaking-change] Drop support for < 2.4 17 | 18 | # v0.1.0 2014-03-24 19 | 20 | * [breaking-change] Drop support for 1.8 21 | 22 | # v0.0.6 2013-01-03 23 | 24 | * [change] Update dependencies 25 | 26 | # v0.0.5 2012-12-10 27 | 28 | * [feature] use devtools and test with mutant 29 | 30 | # v0.0.4 2012-11-20 31 | 32 | * [feature] Support for relational operators >, <, >= and <= 33 | 34 | [Compare v0.0.3..v0.0.4](https://github.com/mbj/auom/compare/v0.0.3...v0.0.4) 35 | 36 | # v0.0.3 2012-11-19 37 | 38 | First public release :) 39 | -------------------------------------------------------------------------------- /config/roodi.yml: -------------------------------------------------------------------------------- 1 | --- 2 | AbcMetricMethodCheck: 3 | score: 25.1 4 | AssignmentInConditionalCheck: { } 5 | CaseMissingElseCheck: { } 6 | ClassLineCountCheck: { line_count: 317 } 7 | ClassNameCheck: 8 | pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ 9 | ClassVariableCheck: { } 10 | CyclomaticComplexityBlockCheck: { complexity: 3 } 11 | CyclomaticComplexityMethodCheck: { complexity: 4 } 12 | EmptyRescueBodyCheck: { } 13 | ForLoopCheck: { } 14 | MethodLineCountCheck: { line_count: 14 } 15 | MethodNameCheck: 16 | pattern: !ruby/regexp /\A(?:[a-z\d](?:_?[a-z\d])+[?!=]?|\[\]=?|==|<=>|<<|[+*&|-])\z/ 17 | ModuleLineCountCheck: { line_count: 320 } 18 | ModuleNameCheck: 19 | pattern: !ruby/regexp /\A(?:[A-Z]+|[A-Z][a-z](?:[A-Z]?[a-z])+)\z/ 20 | ParameterNumberCheck: { parameter_count: 3 } 21 | -------------------------------------------------------------------------------- /auom.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |gem| 4 | gem.name = 'auom' 5 | gem.version = '0.3.1' 6 | 7 | gem.authors = ['Markus Schirp'] 8 | gem.email = 'mbj@schirp-dso.com' 9 | gem.summary = 'Algebra (with) Units Of Measurement' 10 | gem.homepage = 'http://github.com/mbj/auom' 11 | 12 | gem.files = `git ls-files`.split("\n") 13 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 14 | gem.require_paths = %w[lib] 15 | gem.extra_rdoc_files = %w[LICENSE] 16 | 17 | gem.required_ruby_version = '>= 2.5' 18 | 19 | gem.add_dependency('equalizer', '~> 0.0.9') 20 | 21 | gem.add_development_dependency('minitest', '~> 5.11.3') 22 | gem.add_development_dependency('mutant', '~> 0.10') 23 | gem.add_development_dependency('mutant-minitest', '~> 0.10') 24 | gem.add_development_dependency('mutant-rspec', '~> 0.10') 25 | gem.add_development_dependency('rspec-its', '~> 1.3') 26 | end 27 | -------------------------------------------------------------------------------- /lib/auom/equalization.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AUOM 4 | # Equalization for auom units 5 | module Equalization 6 | # Check for equivalent value and try to convert 7 | # 8 | # @param [Object] other 9 | # 10 | # @return [true] 11 | # return true if is other is a unit and scalar and unit is the same after try of conversion. 12 | # 13 | # @return [false] 14 | # return false otherwise 15 | # 16 | # @example 17 | # 18 | # u = Unit.new(1, :meter) 19 | # u == u # => true 20 | # u == 1 # => false 21 | # u == Unit.new(1, :meter) # => true 22 | # 23 | # u = Unit.new(1) 24 | # u == 1 # => true 25 | # u == Rational(1) # => true 26 | # u == 1.0 # => false 27 | # 28 | # @api public 29 | # 30 | def ==(other) 31 | eql?(self.class.try_convert(other)) 32 | end 33 | end # Equalization 34 | end # AUOM 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Markus Schirp 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'auom' 4 | require 'rspec/its' 5 | 6 | RSpec.shared_examples_for 'a command method' do 7 | it 'returns self' do 8 | should equal(object) 9 | end 10 | end 11 | 12 | RSpec.shared_examples_for 'an idempotent method' do 13 | it 'is idempotent' do 14 | first = subject 15 | fail 'RSpec not configured for threadsafety' unless RSpec.configuration.threadsafe? 16 | mutex = __memoized.instance_variable_get(:@mutex) 17 | memoized = __memoized.instance_variable_get(:@memoized) 18 | 19 | mutex.synchronize { memoized.delete(:subject) } 20 | should equal(first) 21 | end 22 | end 23 | 24 | RSpec.shared_examples_for 'unitless unit' do 25 | its(:numerators) { should == [] } 26 | its(:denominators) { should == [] } 27 | its(:unit) { should == [[], []] } 28 | it { should be_unitless } 29 | end 30 | 31 | RSpec.shared_examples_for 'an incompatible operation' do 32 | it 'should raise ArgumentError' do 33 | expect { subject }.to raise_error(ArgumentError, 'Incompatible units') 34 | end 35 | end 36 | 37 | RSpec.shared_examples_for 'an operation' do 38 | it 'returns a new object' do 39 | expect(object).to_not equal(subject) 40 | end 41 | 42 | it 'is idempotent on equivalency' do 43 | first = subject 44 | fail unless RSpec.configuration.threadsafe? 45 | 46 | mutex = __memoized.instance_variable_get(:@mutex) 47 | memoized = __memoized.instance_variable_get(:@memoized) 48 | mutex.synchronize { memoized.delete(:subject) } 49 | should eql(first) 50 | end 51 | 52 | its(:scalar) { should be_kind_of(::Rational) } 53 | end 54 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: push 4 | 5 | jobs: 6 | base: 7 | name: Base steps 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: git diff --check -- HEAD~1 12 | ruby-spec: 13 | name: Specs 14 | runs-on: ${{ matrix.os }} 15 | timeout-minutes: 5 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | ruby: ['ruby-2.5', 'ruby-2.6', 'ruby-2.7'] 20 | os: [ubuntu-latest] 21 | steps: 22 | - uses: actions/checkout@v2 23 | - uses: ruby/setup-ruby@v1 24 | with: 25 | ruby-version: ${{ matrix.ruby }} 26 | bundler-cache: true 27 | - run: bundle install 28 | - run: bundle exec rspec spec 29 | ruby-mutant-minitest: 30 | name: Mutant Minitest 31 | runs-on: ${{ matrix.os }} 32 | timeout-minutes: 5 33 | strategy: 34 | fail-fast: false 35 | matrix: 36 | ruby: ['ruby-2.6', 'ruby-2.7'] 37 | os: [ubuntu-latest] 38 | steps: 39 | - uses: actions/checkout@v2 40 | - uses: ruby/setup-ruby@v1 41 | with: 42 | ruby-version: ${{ matrix.ruby }} 43 | bundler-cache: true 44 | - run: bundle install 45 | - run: bundle exec mutant run --use minitest -- 'AUOM*' 46 | ruby-mutant-rspec: 47 | name: Mutant RSpec 48 | runs-on: ${{ matrix.os }} 49 | timeout-minutes: 5 50 | strategy: 51 | fail-fast: false 52 | matrix: 53 | ruby: ['ruby-2.6', 'ruby-2.7'] 54 | os: [ubuntu-latest] 55 | steps: 56 | - uses: actions/checkout@v2 57 | - uses: ruby/setup-ruby@v1 58 | with: 59 | ruby-version: ${{ matrix.ruby }} 60 | bundler-cache: true 61 | - run: bundle install 62 | - run: bundle exec mutant run --use rspec -- 'AUOM*' 63 | -------------------------------------------------------------------------------- /config/reek.yml: -------------------------------------------------------------------------------- 1 | detectors: 2 | UncommunicativeParameterName: 3 | accept: [] 4 | exclude: [] 5 | enabled: true 6 | reject: 7 | - /^.$/ 8 | - /[0-9]$/ 9 | - /[A-Z]/ 10 | TooManyMethods: 11 | max_methods: 14 12 | enabled: true 13 | exclude: [] 14 | UncommunicativeMethodName: 15 | accept: [] 16 | exclude: [] 17 | enabled: true 18 | reject: 19 | - /^[a-z]$/ 20 | - /[0-9]$/ 21 | - /[A-Z]/ 22 | LongParameterList: 23 | max_params: 2 24 | exclude: 25 | - "AUOM::Unit#initialize" # 3 params 26 | - "AUOM::Unit#self.new" # 3 params 27 | - "AUOM::Unit#self.resolve" # 3 params 28 | enabled: true 29 | overrides: {} 30 | FeatureEnvy: 31 | exclude: [] 32 | enabled: true 33 | ClassVariable: 34 | exclude: [] 35 | enabled: true 36 | BooleanParameter: 37 | exclude: [] 38 | enabled: true 39 | IrresponsibleModule: 40 | exclude: [] 41 | enabled: true 42 | UncommunicativeModuleName: 43 | accept: [] 44 | exclude: [] 45 | enabled: true 46 | reject: 47 | - /^.$/ 48 | - /[0-9]$/ 49 | NestedIterators: 50 | ignore_iterators: [] 51 | exclude: 52 | - AUOM::Unit#initialize # 2 levels 53 | enabled: true 54 | max_allowed_nesting: 1 55 | TooManyStatements: 56 | max_statements: 6 57 | exclude: 58 | - AUOM::Inspection#pretty_unit # 7 statements 59 | - AUOM::Unit#initialize # 8 statements 60 | enabled: true 61 | DuplicateMethodCall: 62 | allow_calls: [] 63 | exclude: 64 | - "AUOM::ClassMethods#new" # invalid detection 65 | enabled: true 66 | max_calls: 1 67 | UtilityFunction: 68 | exclude: [] 69 | enabled: true 70 | Attribute: 71 | exclude: [] 72 | enabled: false 73 | UncommunicativeVariableName: 74 | accept: [] 75 | exclude: [] 76 | enabled: true 77 | reject: 78 | - /^.$/ 79 | - /[0-9]$/ 80 | - /[A-Z]/ 81 | RepeatedConditional: 82 | enabled: true 83 | max_ifs: 1 84 | DataClump: 85 | exclude: [] 86 | enabled: true 87 | max_copies: 1 88 | min_clump_size: 2 89 | ControlParameter: 90 | exclude: 91 | - AUOM::Unit#assert_same_unit 92 | enabled: true 93 | LongYieldList: 94 | max_params: 0 95 | exclude: [] 96 | enabled: true 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AUOM Algebra (for) Units of Measurement 2 | ======================================= 3 | 4 | [![Build Status](https://secure.travis-ci.org/mbj/auom.png?branch=master)](http://travis-ci.org/mbj/auom) 5 | [![Dependency Status](https://gemnasium.com/mbj/auom.png)](https://gemnasium.com/mbj/auom) 6 | [![Code Climate](https://codeclimate.com/github/mbj/auom.png)](https://codeclimate.com/github/mbj/auom) 7 | 8 | This is another unit system for ruby. 9 | It was created since I was not confident about the existing ones. 10 | 11 | Features: 12 | 13 | * No unit conversions 14 | * No core patches (Especially does not require mathn!) 15 | * Dependency free. 16 | * Functional implementation style. 17 | * No magic coercion from number like strings to numbers. 18 | * Will never loose precision (Uses rational as scalar internally) 19 | * Allows namespacing of unit systems via subclass. 20 | * Well tested (100% mutation coverage via [mutant](https://github.com/mbj/mutant)) 21 | 22 | The default set of predefined units is minimal as this library should be used in an application 23 | specific subclass. Override: ```AUOM::Unit.units``` 24 | 25 | Installation 26 | ------------ 27 | 28 | Install the gem `auom` via your preferred method. 29 | 30 | Examples 31 | -------- 32 | 33 | ``` ruby 34 | require 'auom' 35 | 36 | include AUOM 37 | 38 | u = Unit.new(1, :meter) # 39 | u * 100 # 40 | u / Unit.new(10, :meter) # 41 | u / Unit.new(10, [ :meter, :meter ]) # 42 | u * Unit.new(10, :meter) # 43 | u * Unit.new(1, :euro) # 44 | u - Unit.new(1, :meter) # 45 | u + Unit.new(1, :meter) # 46 | u + Unit.new(1, :euro) # raises error about incompatible units 47 | ``` 48 | 49 | Credits 50 | ------- 51 | 52 | Room for your name! 53 | 54 | Contributing 55 | ------------- 56 | 57 | * Fork the project. 58 | * Make your feature addition or bug fix. 59 | * Add tests for it. This is important so I don't break it in a 60 | future version unintentionally. 61 | * Commit, do not mess with Rakefile or version 62 | (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) 63 | * Send me a pull request. Bonus points for topic branches. 64 | 65 | Copyright 66 | --------- 67 | 68 | Copyright (c) 2014 Markus Schirp 69 | 70 | See `LICENSE` for details 71 | -------------------------------------------------------------------------------- /lib/auom/inspection.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AUOM 4 | # Inspection module for auom units 5 | module Inspection 6 | INSPECT_FORMAT = '<%s @scalar=%s%s>' 7 | SCALAR_FORMAT = '~%0.4f' 8 | UNIT_FORMAT = ' %s/%s' 9 | 10 | # Return inspectable representation 11 | # 12 | # @return [String] 13 | # 14 | # @api private 15 | # 16 | def inspect 17 | INSPECT_FORMAT % [self.class, pretty_scalar, pretty_unit] 18 | end 19 | 20 | # Return prettified units 21 | # 22 | # @param [Array] base 23 | # 24 | # @return [String] 25 | # 26 | # @api private 27 | # 28 | def self.prettify_unit_part(base) 29 | counts(base).map do |unit, length| 30 | length > 1 ? "#{unit}^#{length}" : unit 31 | end.join('*') 32 | end 33 | 34 | private 35 | 36 | # Return prettified scalar 37 | # 38 | # @return [String] 39 | # 40 | # @api private 41 | # 42 | def pretty_scalar 43 | if reminder? 44 | SCALAR_FORMAT % scalar 45 | else 46 | scalar.to_int 47 | end 48 | end 49 | 50 | # Return prettified unit part 51 | # 52 | # @return [String] 53 | # if there is a prettifiable unit part 54 | # 55 | # @return [nil] 56 | # otherwise 57 | # 58 | # @api private 59 | # 60 | def pretty_unit 61 | return if unitless? 62 | 63 | numerator = Inspection.prettify_unit_part(numerators) 64 | denominator = Inspection.prettify_unit_part(denominators) 65 | 66 | numerator = '1' if numerator.empty? 67 | return " #{numerator}" if denominator.empty? 68 | 69 | UNIT_FORMAT % [numerator, denominator] 70 | end 71 | 72 | # Test if scalar has and reminder in decimal representation 73 | # 74 | # @return [true] 75 | # if there is a reminder 76 | # 77 | # @return [false] 78 | # otherwise 79 | # 80 | # @api private 81 | # 82 | def reminder? 83 | !(scalar % scalar.denominator).zero? 84 | end 85 | 86 | # Return unit counts 87 | # 88 | # @param [Array] base 89 | # 90 | # @return [Hash] 91 | # 92 | # @api private 93 | # 94 | # rubocop:disable Metrics/MethodLength 95 | def self.counts(base) 96 | counts = base.each_with_object(Hash.new(0)) do |unit, hash| 97 | hash[unit] += 1 98 | end 99 | 100 | counts.sort do |left, right| 101 | result = right.last <=> left.last 102 | if result.equal?(0) 103 | left.first <=> right.first 104 | else 105 | result 106 | end 107 | end 108 | end 109 | private_class_method :counts 110 | # rubocop:enable Metrics/MethodLength 111 | 112 | end # Inspection 113 | end # AUOM 114 | -------------------------------------------------------------------------------- /lib/auom/algebra.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AUOM 4 | # The AUOM algebra 5 | module Algebra 6 | # Return addition result 7 | # 8 | # @param [Object] other 9 | # 10 | # @return [Unit] 11 | # 12 | # @example 13 | # 14 | # # unitless 15 | # Unit.new(1) + Unit.new(2) # => 16 | # 17 | # # with unit 18 | # Unit.new(1,:meter) + Unit.new(2,:meter) # => 19 | # 20 | # # incompatible unit 21 | # Unit.new(1,:meter) + Unit.new(2,:euro) # raises ArgumentError! 22 | # 23 | # @api public 24 | # 25 | def +(other) 26 | klass = self.class 27 | other = klass.convert(other) 28 | assert_same_unit(other) 29 | klass.new(other.scalar + scalar, numerators, denominators) 30 | end 31 | 32 | # Return subtraction result 33 | # 34 | # @param [Object] other 35 | # 36 | # @return [Unit] 37 | # 38 | # @example 39 | # 40 | # # unitless 41 | # Unit.new(2) - Unit.new(1) # => 42 | # 43 | # # with unit 44 | # Unit.new(2,:meter) - Unit.new(1,:meter) # => 45 | # 46 | # # incompatible unit 47 | # Unit.new(2,:meter) - Unit.new(1,:euro) # raises ArgumentError! 48 | # 49 | # @api public 50 | # 51 | def -(other) 52 | self + (other * -1) 53 | end 54 | 55 | # Return multiplication result 56 | # 57 | # @param [Object] other 58 | # 59 | # @return [Unit] 60 | # 61 | # @example 62 | # 63 | # # unitless 64 | # Unit.new(2) * Unit.new(1) # => 65 | # 66 | # # with unit 67 | # Unit.new(2, :meter) * Unit.new(1, :meter) # => 68 | # 69 | # # different units 70 | # Unit.new(2, :meter) * Unit.new(1, :euro) # => 71 | # 72 | # @api public 73 | # 74 | def *(other) 75 | klass = self.class 76 | other = klass.convert(other) 77 | 78 | klass.new( 79 | other.scalar * scalar, 80 | numerators + other.numerators, 81 | denominators + other.denominators 82 | ) 83 | end 84 | 85 | # Return division result 86 | # 87 | # @param [Object] other 88 | # 89 | # @return [Unit] 90 | # 91 | # @example 92 | # 93 | # # unitless 94 | # Unit.new(2) / Unit.new(1) # => 95 | # 96 | # # with unit 97 | # Unit.new(2, :meter) / Unit.new(1, :meter) # => 98 | # 99 | # # different units 100 | # Unit.new(2, :meter) / Unit.new(1, :euro) # => 101 | # 102 | # @api public 103 | # 104 | def /(other) 105 | klass = self.class 106 | other = klass.convert(other) 107 | 108 | self * klass.new( 109 | 1 / other.scalar, 110 | other.denominators, 111 | other.numerators 112 | ) 113 | end 114 | 115 | end # Algebra 116 | end # AUOM 117 | -------------------------------------------------------------------------------- /config/rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: ../.rubocop.yml 2 | 3 | # Avoid parameter lists longer than five parameters. 4 | ParameterLists: 5 | Max: 3 6 | CountKeywordArgs: true 7 | 8 | # Avoid more than `Max` levels of nesting. 9 | BlockNesting: 10 | Max: 3 11 | 12 | # Align with the style guide. 13 | CollectionMethods: 14 | Enabled: true 15 | PreferredMethods: 16 | collect: 'map' 17 | inject: 'reduce' 18 | find: 'detect' 19 | find_all: 'select' 20 | 21 | AccessModifierIndentation: 22 | Enabled: false 23 | 24 | # Limit line length 25 | LineLength: 26 | Max: 120 27 | 28 | # Disable documentation checking until a class needs to be documented once 29 | Documentation: 30 | Enabled: false 31 | 32 | # Permit 33 | # 34 | # boolean_check? or fail 35 | # 36 | # Reject 37 | # 38 | # if foo or bar 39 | # ... 40 | # end 41 | AndOr: 42 | EnforcedStyle: conditionals 43 | 44 | # Do not favor modifier if/unless usage when you have a single-line body 45 | IfUnlessModifier: 46 | Enabled: false 47 | 48 | # Allow case equality operator (in limited use within the specs) 49 | CaseEquality: 50 | Enabled: false 51 | 52 | # Constants do not always have to use SCREAMING_SNAKE_CASE 53 | ConstantName: 54 | Enabled: false 55 | 56 | # Not all trivial readers/writers can be defined with attr_* methods 57 | TrivialAccessors: 58 | Enabled: false 59 | 60 | # Allow empty lines around class body 61 | EmptyLinesAroundClassBody: 62 | Enabled: false 63 | 64 | # Allow empty lines around module body 65 | EmptyLinesAroundModuleBody: 66 | Enabled: false 67 | 68 | # Allow empty lines around block body 69 | EmptyLinesAroundBlockBody: 70 | Enabled: false 71 | 72 | # Allow multiple line operations to not require indentation 73 | MultilineOperationIndentation: 74 | Enabled: false 75 | 76 | # Prefer String#% over Kernel#sprintf 77 | FormatString: 78 | EnforcedStyle: percent 79 | 80 | # Use square brackets for literal Array objects 81 | PercentLiteralDelimiters: 82 | PreferredDelimiters: 83 | '%': '{}' 84 | '%i': '[]' 85 | '%q': () 86 | '%Q': () 87 | '%r': '{}' 88 | '%s': () 89 | '%w': '[]' 90 | '%W': '[]' 91 | '%x': () 92 | 93 | # Use %i[...] for arrays of symbols 94 | SymbolArray: 95 | Enabled: true 96 | 97 | # Prefer #kind_of? over #is_a? 98 | ClassCheck: 99 | EnforcedStyle: kind_of? 100 | 101 | # Do not prefer double quotes to be used when %q or %Q is more appropriate 102 | Style/RedundantPercentQ: 103 | Enabled: false 104 | 105 | # Allow a maximum ABC score 106 | Metrics/AbcSize: 107 | Max: 21.02 108 | 109 | Metrics/BlockLength: 110 | Exclude: 111 | - 'spec/**/*.rb' 112 | 113 | # Buggy cop, returns false positive for our code base 114 | NonLocalExitFromIterator: 115 | Enabled: false 116 | 117 | # To allow alignment of similar expressions we want to allow more than one 118 | # space around operators: 119 | # 120 | # let(:a) { bar + something } 121 | # let(:b) { foobar + something } 122 | # 123 | SpaceAroundOperators: 124 | Enabled: false 125 | 126 | # We use parallel assignments with great success 127 | ParallelAssignment: 128 | Enabled: false 129 | 130 | # Allow additional specs 131 | ExtraSpacing: 132 | AllowForAlignment: true 133 | 134 | # Buggy 135 | FormatParameterMismatch: 136 | Enabled: false 137 | 138 | # Different preference 139 | SignalException: 140 | EnforcedStyle: semantic 141 | 142 | # Do not use `alias` 143 | Alias: 144 | EnforcedStyle: prefer_alias_method 145 | 146 | # Do not waste my horizontal or vertical space 147 | Layout/FirstArrayElementIndentation: 148 | Enabled: false 149 | 150 | # Prefer 151 | # 152 | # some_receiver 153 | # .foo 154 | # .bar 155 | # .baz 156 | # 157 | # Over 158 | # 159 | # some_receiver.foo 160 | # .bar 161 | # .baz 162 | MultilineMethodCallIndentation: 163 | EnforcedStyle: indented 164 | 165 | # Prefer `public_send` and `__send__` over `send` 166 | Send: 167 | Enabled: true 168 | 169 | Layout/HashAlignment: 170 | EnforcedColonStyle: table 171 | EnforcedHashRocketStyle: table 172 | Layout/EmptyLineAfterGuardClause: 173 | Enabled: false 174 | Layout/SpaceInsideArrayLiteralBrackets: 175 | Enabled: false 176 | Lint/BooleanSymbol: 177 | Enabled: false 178 | Lint/InterpolationCheck: 179 | Enabled: false 180 | Lint/MissingCopEnableDirective: 181 | Enabled: false 182 | Lint/UnifiedInteger: 183 | Enabled: false 184 | Naming/FileName: 185 | Enabled: false 186 | Style/AccessModifierDeclarations: 187 | Enabled: false 188 | Style/CommentedKeyword: 189 | Enabled: false 190 | Style/MixinGrouping: 191 | Enabled: false 192 | Style/RaiseArgs: 193 | Enabled: false 194 | Style/RescueStandardError: 195 | Enabled: false 196 | Style/StderrPuts: 197 | Enabled: false 198 | # suggesting single letter variablesl bah 199 | Naming/RescuedExceptionsVariableName: 200 | Enabled: false 201 | # false positive on private keywords 202 | Layout/IndentationWidth: 203 | Enabled: false 204 | 205 | -------------------------------------------------------------------------------- /lib/auom/unit.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module AUOM 4 | # A scalar with units 5 | class Unit 6 | include Equalizer.new(:scalar, :numerators, :denominators) 7 | include Algebra 8 | include Inspection 9 | include Relational 10 | include Equalization 11 | 12 | # Return scalar 13 | # 14 | # @example 15 | # 16 | # include AUOM 17 | # m = Unit.new(1, :meter) 18 | # m.scalar # => Rational(1, 1) 19 | # 20 | # @return [Rational] 21 | # 22 | # @api public 23 | # 24 | attr_reader :scalar 25 | 26 | # Return numerators 27 | # 28 | # @example 29 | # 30 | # include AUOM 31 | # m = Unit.new(1, :meter) 32 | # m.numerators # => [:meter] 33 | # 34 | # @return [Rational] 35 | # 36 | # @api public 37 | # 38 | attr_reader :numerators 39 | 40 | # Return denominators 41 | # 42 | # @example 43 | # 44 | # include AUOM 45 | # m = Unit.new(1, :meter) 46 | # m.denominators # => [] 47 | # 48 | # @return [Rational] 49 | # 50 | # @api public 51 | # 52 | attr_reader :denominators 53 | 54 | # Return unit descriptor 55 | # 56 | # @return [Array] 57 | # 58 | # @api public 59 | # 60 | # @example 61 | # 62 | # u = Unit.new(1, [:meter, :meter], :euro) 63 | # u.unit # => [[:meter, :meter], [:euro]] 64 | # 65 | attr_reader :unit 66 | 67 | # These constants can easily be changed 68 | # by an application specific subclass that overrides 69 | # AUOM::Unit.units with an own hash! 70 | UNITS = { 71 | item: [1, :item], 72 | liter: [1, :liter], 73 | pack: [1, :pack], 74 | can: [1, :can], 75 | kilogramm: [1, :kilogramm], 76 | euro: [1, :euro], 77 | meter: [1, :meter], 78 | kilometer: [1000, :meter] 79 | }.freeze 80 | 81 | # Return built-in units symbols 82 | # 83 | # @return [Hash] 84 | # 85 | # @api private 86 | # 87 | def self.units 88 | UNITS 89 | end 90 | 91 | # Check for unitless unit 92 | # 93 | # @return [true] 94 | # return true if unit is unitless 95 | # 96 | # @return [false] 97 | # return false if unit is NOT unitless 98 | # 99 | # @example 100 | # 101 | # Unit.new(1).unitless? # => true 102 | # Unit.new(1, :meter).unitless ? # => false 103 | # 104 | # @api public 105 | # 106 | def unitless? 107 | numerators.empty? && denominators.empty? 108 | end 109 | 110 | # Test if units are the same 111 | # 112 | # @param [Unit] other 113 | # 114 | # @return [true] 115 | # if units are the same 116 | # 117 | # @return [false] 118 | # otherwise 119 | # 120 | # @example 121 | # 122 | # a = Unit.new(1) 123 | # b = Unit.new(1, :euro) 124 | # c = Unit.new(2, :euro) 125 | # 126 | # a.same_unit?(b) # => false 127 | # b.same_unit?(c) # => true 128 | # 129 | # @api public 130 | # 131 | def same_unit?(other) 132 | other.unit.eql?(unit) 133 | end 134 | 135 | # Instantiate a new unit 136 | # 137 | # @param [Rational] scalar 138 | # @param [Enumerable] numerators 139 | # @param [Enumerable] denominators 140 | # 141 | # @return [Unit] 142 | # 143 | # @example 144 | # 145 | # # A unitless unit 146 | # u = Unit.new(1) 147 | # u.unitless? # => true 148 | # u.scalar # => Rational(1, 1) 149 | # 150 | # # A unitless unit from string 151 | # u = Unit.new('1.5') 152 | # u.unitless? # => true 153 | # u.scalar # => Rational(3, 2) 154 | # 155 | # # A simple unit 156 | # u = Unit.new(1, :meter) 157 | # u.unitless? # => false 158 | # u.numerators # => [:meter] 159 | # u.scalar # => Rational(1, 1) 160 | # 161 | # # A complex unit 162 | # u = Unit.new(Rational(1, 3), :euro, :meter) 163 | # u.fractions? # => true 164 | # u.scalar # => Rational(1, 3) 165 | # u.inspect # => 166 | # u.unit # => [[:euro], [:meter]] 167 | # 168 | # @api public 169 | # 170 | # TODO: Move defaults coercions etc to .build method 171 | # 172 | def self.new(scalar, numerators = nil, denominators = nil) 173 | scalar = rational(scalar) 174 | 175 | scalar, numerators = resolve([*numerators], scalar, :*) 176 | scalar, denominators = resolve([*denominators], scalar, :/) 177 | 178 | super(scalar, *[numerators, denominators].map(&:sort)).freeze 179 | end 180 | 181 | # Assert units are the same 182 | # 183 | # @param [Unit] other 184 | # 185 | # @return [self] 186 | # 187 | # @api private 188 | # 189 | def assert_same_unit(other) 190 | fail ArgumentError, 'Incompatible units' unless same_unit?(other) 191 | 192 | self 193 | end 194 | 195 | # Return converted operand or raise error 196 | # 197 | # @param [Object] operand 198 | # 199 | # @return [Unit] 200 | # 201 | # @raise [ArgumentError] 202 | # raises argument error in case operand cannot be converted 203 | # 204 | # @api private 205 | # 206 | def self.convert(operand) 207 | converted = try_convert(operand) 208 | unless converted 209 | fail ArgumentError, "Cannot convert #{operand.inspect} to #{self}" 210 | end 211 | 212 | converted 213 | end 214 | 215 | # Return converted operand or nil 216 | # 217 | # @param [Object] operand 218 | # 219 | # @return [Unit] 220 | # return unit in case operand can be converted 221 | # 222 | # @return [nil] 223 | # return nil in case operand can NOT be converted 224 | # 225 | # @api private 226 | # 227 | def self.try_convert(operand) 228 | case operand 229 | when self 230 | operand 231 | when Integer, Rational 232 | new(operand) 233 | end 234 | end 235 | 236 | private 237 | 238 | # Initialize unit 239 | # 240 | # @param [Rational] scalar 241 | # @param [Enumerable] numerators 242 | # @param [Enumerable] denominators 243 | # 244 | # @api private 245 | # 246 | def initialize(scalar, numerators, denominators) 247 | @scalar = scalar 248 | 249 | [numerators, denominators].permutation do |left, right| 250 | left.delete_if { |item| right.delete_at(right.index(item) || right.length) } 251 | end 252 | 253 | @numerators = numerators.freeze 254 | @denominators = denominators.freeze 255 | 256 | @unit = [numerators, denominators].freeze 257 | end 258 | 259 | # Return rational converted from value 260 | # 261 | # @param [Object] value 262 | # 263 | # @return [Rational] 264 | # 265 | # @raise [ArgumentError] 266 | # raises argument error when cannot be converted to a rational 267 | # 268 | # @api private 269 | # 270 | def self.rational(value) 271 | case value 272 | when Rational 273 | value 274 | when Integer 275 | Rational(value) 276 | else 277 | fail ArgumentError, "#{value.inspect} cannot be converted to rational" 278 | end 279 | end 280 | 281 | private_class_method :rational 282 | 283 | # Resolve unit component 284 | # 285 | # @param [Enumerable] components 286 | # @param [Symbol] operation 287 | # 288 | # @return [Array] 289 | # 290 | # @api private 291 | # 292 | def self.resolve(components, scalar, operation) 293 | resolved = components.map do |component| 294 | scale, component = lookup(component) 295 | scalar = scalar.public_send(operation, scale) 296 | component 297 | end 298 | [scalar, resolved] 299 | end 300 | 301 | private_class_method :resolve 302 | 303 | # Return unit information 304 | # 305 | # @param [Symbol] value 306 | # the unit to search for 307 | # 308 | # @return [Array] 309 | # 310 | # @api private 311 | # 312 | def self.lookup(value) 313 | units.fetch(value) do 314 | fail ArgumentError, "Unknown unit #{value.inspect}" 315 | end 316 | end 317 | 318 | private_class_method :lookup 319 | end # Unit 320 | end # AUOM 321 | -------------------------------------------------------------------------------- /test/unit/auom_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | require 'mutant/minitest/coverage' 5 | 6 | $LOAD_PATH << 'lib' 7 | require 'auom' 8 | 9 | class AUOMTest < Minitest::Test 10 | cover 'AUOM*' 11 | 12 | private 13 | 14 | def unit(*arguments) 15 | AUOM::Unit.new(*arguments).tap do |unit| 16 | assert(unit.frozen?) 17 | assert(unit.numerators.frozen?) 18 | assert(unit.denominators.frozen?) 19 | assert(unit.unit.frozen?) 20 | end 21 | end 22 | 23 | class Binary < self 24 | 25 | private 26 | 27 | def object 28 | unit(*self.class::ARGUMENTS) 29 | end 30 | 31 | def apply(operand, expected) 32 | assert_equal( 33 | expected, 34 | object.public_send( 35 | self.class::METHOD, 36 | operand 37 | ) 38 | ) 39 | end 40 | 41 | def incompatible_apply(operand) 42 | exception = assert_raises(ArgumentError, message) do 43 | apply(operand, nil) 44 | end 45 | 46 | assert_equal('Incompatible units', exception.message) 47 | end 48 | 49 | class Add < self 50 | METHOD = :+ 51 | 52 | cover 'AUOM::Algebra#+' 53 | 54 | class Unitless < self 55 | ARGUMENTS = [1].freeze 56 | 57 | def test_integer 58 | apply(1, unit(2)) 59 | end 60 | 61 | def test_unitless 62 | apply(unit(1), unit(2)) 63 | end 64 | 65 | def test_unitful 66 | incompatible_apply(unit(2, :meter)) 67 | end 68 | end 69 | 70 | class Unitful < self 71 | ARGUMENTS = [1, :meter, :euro].freeze 72 | 73 | def test_integer 74 | incompatible_apply(2) 75 | end 76 | 77 | def test_incompatible_unit 78 | incompatible_apply(unit(2, :meter)) 79 | end 80 | 81 | def test_compatible_unit 82 | apply(unit(2, :meter, :euro), unit(3, :meter, :euro)) 83 | end 84 | end 85 | end 86 | 87 | class Subtract < self 88 | METHOD = :- 89 | 90 | cover 'AUOM::Algebra#-' 91 | 92 | class Unitless < self 93 | ARGUMENTS = [2].freeze 94 | 95 | def test_integer 96 | apply(1, unit(1)) 97 | end 98 | 99 | def test_unitless 100 | apply(unit(1), unit(1)) 101 | end 102 | 103 | def test_unitful 104 | incompatible_apply(unit(2, :meter)) 105 | end 106 | end 107 | 108 | class Unitful < self 109 | ARGUMENTS = [2, :meter, :euro].freeze 110 | 111 | def test_integer 112 | incompatible_apply(2) 113 | end 114 | 115 | def test_incompatible_unit 116 | incompatible_apply(unit(2, :meter)) 117 | end 118 | 119 | def test_compatible_unit 120 | apply(unit(1, :meter, :euro), unit(1, :meter, :euro)) 121 | end 122 | end 123 | end 124 | 125 | class Divide < self 126 | cover 'AUOM::Algebra#/' 127 | 128 | METHOD = :/ 129 | 130 | class Unitless < self 131 | ARGUMENTS = [4].freeze 132 | 133 | def test_integer 134 | apply(2, unit(2)) 135 | end 136 | 137 | def test_unitless 138 | apply(unit(2), unit(2)) 139 | end 140 | 141 | def test_unit 142 | apply(unit(2, :meter), unit(2, [], :meter)) 143 | end 144 | end 145 | 146 | class Unitful < self 147 | ARGUMENTS = [2, :meter].freeze 148 | 149 | def test_integer 150 | apply(1, object) 151 | end 152 | 153 | def test_unitless 154 | apply(unit(1), object) 155 | end 156 | 157 | def test_unit_remove_to_unitless 158 | apply(unit(1, :meter), unit(2)) 159 | end 160 | 161 | def test_unit_remove_denominator 162 | apply(unit(1, :meter, :euro), unit(2, :euro)) 163 | end 164 | 165 | def test_unit_add_to_numerator 166 | apply(unit(1, [], :euro), unit(2, %i[meter euro])) 167 | end 168 | end 169 | end 170 | 171 | class Multiply < self 172 | cover 'AUOM::Algebra#*' 173 | 174 | METHOD = :* 175 | 176 | class Unitless < self 177 | ARGUMENTS = [4].freeze 178 | 179 | def test_integer 180 | apply(5, unit(20)) 181 | end 182 | 183 | def test_unitless 184 | apply(unit(5), unit(20)) 185 | end 186 | 187 | def test_unit_numerator_addition 188 | apply(unit(5, :euro), unit(20, :euro)) 189 | end 190 | 191 | def test_unit_denominator_addition 192 | apply(unit(5, [], :euro), unit(20, [], :euro)) 193 | end 194 | end 195 | 196 | class Unitful < self 197 | ARGUMENTS = [4, :meter].freeze 198 | 199 | def test_integer 200 | apply(5, unit(20, :meter)) 201 | end 202 | 203 | def test_unitless 204 | apply(unit(5), unit(20, :meter)) 205 | end 206 | 207 | def test_unit_numerator_addition 208 | apply(unit(5, :meter), unit(20, %i[meter meter])) 209 | end 210 | 211 | def test_unit_denominator_addition 212 | apply(unit(5, [], :euro), unit(20, :meter, :euro)) 213 | end 214 | 215 | def test_unit_canelation 216 | apply(unit(5, [], :meter), unit(20)) 217 | end 218 | end 219 | 220 | class Unitfuller < self 221 | ARGUMENTS = [4, :meter, :euro].freeze 222 | 223 | def test_unit_denominator_union 224 | apply(unit(5, [], :euro), unit(20, :meter, [:euro, :euro])) 225 | end 226 | end 227 | end 228 | 229 | class Comparable < self 230 | cover 'AUOM::Relational#<=>' 231 | 232 | METHOD = :<=> 233 | 234 | class Unitless < self 235 | ARGUMENTS = [6].freeze 236 | 237 | def test_equal 238 | apply(unit(6), 0) 239 | end 240 | 241 | def test_smaller 242 | apply(unit(5), 1) 243 | end 244 | 245 | def test_bigger 246 | apply(unit(7), -1) 247 | end 248 | 249 | def test_unitful 250 | incompatible_apply(unit(7, :euro)) 251 | end 252 | end 253 | 254 | class Unitful < self 255 | ARGUMENTS = [7, :meter].freeze 256 | 257 | def test_equal 258 | apply(unit(7, :meter), 0) 259 | end 260 | 261 | def test_smaller 262 | apply(unit(6, :meter), 1) 263 | end 264 | 265 | def test_bigger 266 | apply(unit(8, :meter), -1) 267 | end 268 | 269 | def test_unitless 270 | incompatible_apply(unit(7)) 271 | end 272 | 273 | def test_incompatible_unit 274 | incompatible_apply(unit(7, :euro)) 275 | end 276 | end 277 | end 278 | 279 | class Equalization < self 280 | cover 'AUOM::Equalization#==' 281 | 282 | METHOD = :== 283 | 284 | class Unitless < self 285 | ARGUMENTS = [8].freeze 286 | 287 | def test_same_integer 288 | apply(8, true) 289 | end 290 | 291 | def test_different_integer 292 | apply(9, false) 293 | end 294 | 295 | def test_same_unit_same_scalar 296 | apply(unit(8), true) 297 | end 298 | 299 | def test_same_unit_different_scalar 300 | apply(unit(9), false) 301 | end 302 | 303 | def test_different_unit_same_scalar 304 | apply(unit(8, :meter), false) 305 | end 306 | end 307 | end 308 | 309 | class SameUnit < self 310 | cover 'AUOM::Unit#same_unit' 311 | METHOD = :same_unit? 312 | ARGUMENTS = [9, :meter].freeze 313 | 314 | def test_same_unit 315 | apply(unit(10, :meter), true) 316 | end 317 | 318 | def test_incompatible_unit 319 | apply(unit(10), false) 320 | end 321 | end 322 | 323 | class AssertSameUnit < self 324 | cover 'AUOM::Unit#assert_same_unit' 325 | 326 | METHOD = :assert_same_unit 327 | ARGUMENTS = [9, :meter].freeze 328 | 329 | def test_same_unit 330 | apply(unit(10, :meter), object) 331 | end 332 | 333 | def test_returns_self 334 | assert_equal(object, object.assert_same_unit(unit(10, :meter))) 335 | end 336 | 337 | def test_incompatible_unit 338 | incompatible_apply(unit(10)) 339 | end 340 | end 341 | end 342 | 343 | class Unary < self 344 | 345 | private 346 | 347 | def apply(object, expected) 348 | assert_equal( 349 | expected, 350 | object.public_send(self.class::METHOD) 351 | ) 352 | end 353 | 354 | class Inspect < self 355 | cover 'AUOM::Inspection#inspect' 356 | 357 | METHOD = :inspect 358 | 359 | def test_unitless 360 | apply(unit(1), '') 361 | end 362 | 363 | def test_unitless_fraction 364 | apply(unit(Rational(1, 2)), '') 365 | end 366 | 367 | def test_unit_only_numerator 368 | apply(unit(Rational(1, 3), :meter), '') 369 | end 370 | 371 | def test_unit_only_denominator 372 | apply( 373 | unit(Rational(1, 3), [], :meter), 374 | '' 375 | ) 376 | end 377 | 378 | def test_unit_nominator_denominator 379 | apply( 380 | unit(Rational(1, 3), :euro, :meter), 381 | '' 382 | ) 383 | end 384 | 385 | def test_complex_units 386 | apply( 387 | unit(1, %i[euro euro], :meter), 388 | '' 389 | ) 390 | 391 | apply( 392 | unit(1, %i[meter meter euro euro kilogramm], :meter), 393 | '' 394 | ) 395 | end 396 | end 397 | 398 | class Denomnators < self 399 | cover 'AUOM::Unit#denominators' 400 | 401 | METHOD = :denominators 402 | 403 | def test_unitless 404 | apply(unit(1), []) 405 | end 406 | 407 | def test_unit_with_numerators 408 | apply(unit(1, :meter), []) 409 | end 410 | 411 | def test_unit_with_denominators 412 | apply(unit(1, [], :euro), %i[euro]) 413 | end 414 | end 415 | 416 | class Numerators < self 417 | cover 'AUOM::Unit#numerators' 418 | 419 | METHOD = :numerators 420 | 421 | def test_unitless 422 | apply(unit(1), []) 423 | end 424 | 425 | def test_unit_with_numerators 426 | apply(unit(1, :meter), %i[meter]) 427 | end 428 | 429 | def test_unit_with_denominators 430 | apply(unit(1, [], :euro), []) 431 | end 432 | end 433 | 434 | class UnitlessPredicate < self 435 | cover 'AUOM::Unit#unitless?' 436 | 437 | METHOD = :unitless? 438 | 439 | def test_unitless 440 | apply(unit(1), true) 441 | end 442 | 443 | def test_numerator_unit 444 | apply(unit(1, :meter), false) 445 | end 446 | 447 | def test_denominator_unit 448 | apply(unit(1, [], :meter), false) 449 | end 450 | 451 | def test_numerator_denoninator_unit 452 | apply(unit(1, :euro, :meter), false) 453 | end 454 | end 455 | 456 | class Unit < self 457 | cover 'AUOM::Unit#unitless?' 458 | 459 | METHOD = :unit 460 | 461 | def test_unitless 462 | apply(unit(1), [[], []]) 463 | end 464 | 465 | def test_unit_with_numerators 466 | apply(unit(1, :meter), [%i[meter], []]) 467 | end 468 | 469 | def test_unit_with_denominators 470 | apply(unit(1, [], :euro), [[], %i[euro]]) 471 | end 472 | 473 | def test_unit_with_numerators_and_denominators 474 | apply(unit(1, :meter, :euro), [%i[meter], %i[euro]]) 475 | end 476 | end 477 | end 478 | 479 | class ClassMethods < self 480 | TARGET = AUOM::Unit 481 | 482 | private 483 | 484 | def apply(input, expected) 485 | public_send( 486 | expected.nil? ? :assert_nil : :assert_equal, 487 | expected, 488 | self.class::TARGET.public_send(self.class::METHOD, input) 489 | ) 490 | end 491 | 492 | class TryConvert < self 493 | cover 'AUOM::Unit.try_convert' 494 | 495 | METHOD = :try_convert 496 | 497 | def test_nil 498 | apply(nil, nil) 499 | end 500 | 501 | def test_integer 502 | apply(1, unit(1)) 503 | end 504 | 505 | def test_rational 506 | apply(Rational(2, 1), unit(Rational(2, 1))) 507 | end 508 | 509 | def test_object 510 | apply(Object.new, nil) 511 | end 512 | end 513 | 514 | class PrettifyUnitPart < self 515 | cover 'AUOM::Inspection.prettify_unit_part' 516 | 517 | TARGET = AUOM::Inspection 518 | METHOD = :prettify_unit_part 519 | 520 | def test_simple 521 | apply(%i[meter], 'meter') 522 | end 523 | 524 | def test_complex 525 | apply(%i[meter meter euro euro kilogramm], 'euro^2*meter^2*kilogramm') 526 | end 527 | end 528 | 529 | class Convert < self 530 | cover 'AUOM::Unit.convert' 531 | 532 | METHOD = :convert 533 | 534 | def incompatible_apply(input) 535 | exception = assert_raises(ArgumentError) do 536 | apply(input, nil) 537 | end 538 | 539 | assert_equal( 540 | "Cannot convert #{input.inspect} to AUOM::Unit", 541 | exception.message 542 | ) 543 | end 544 | 545 | def test_integer 546 | apply(1, unit(1)) 547 | end 548 | 549 | def test_rational 550 | apply(Rational(2, 1), unit(Rational(2, 1))) 551 | end 552 | 553 | def test_nil 554 | incompatible_apply(nil) 555 | end 556 | 557 | def test_object 558 | incompatible_apply(Object.new) 559 | end 560 | end 561 | 562 | class New < self 563 | cover 'AUOM::Unit.new' 564 | 565 | METHOD = :new 566 | 567 | def test_incompatible_scalar 568 | exception = assert_raises(ArgumentError, message) do 569 | unit(nil) 570 | end 571 | 572 | assert_equal('nil cannot be converted to rational', exception.message) 573 | end 574 | 575 | def test_unknown_unit 576 | exception = assert_raises(ArgumentError, message) do 577 | unit(1, :foo) 578 | end 579 | 580 | assert_equal('Unknown unit :foo', exception.message) 581 | end 582 | 583 | def test_integer 584 | assert_equal(1, unit(1).scalar) 585 | end 586 | 587 | def test_rational 588 | assert_equal(Rational(1), unit(Rational(1)).scalar) 589 | end 590 | 591 | def test_normalized_numerator_unit 592 | assert_equal(unit(1, :kilometer), unit(1000, :meter)) 593 | end 594 | 595 | def test_normalized_numerator_scalar 596 | assert_equal(Rational(1000), unit(1, :kilometer).scalar) 597 | end 598 | 599 | def test_normalized_denominator_unit 600 | assert_equal(unit(1, [], :kilometer), unit(Rational(1, 1000), [], :meter)) 601 | end 602 | 603 | def test_normalized_denominator_scalar 604 | assert_equal(Rational(1, 1000), unit(1, [], :kilometer).scalar) 605 | end 606 | 607 | def test_sorted_numerator 608 | assert_equal(%i[euro meter], unit(1, %i[meter euro]).numerators) 609 | end 610 | 611 | def test_sorted_denominator 612 | assert_equal(%i[euro meter], unit(1, [], %i[meter euro]).denominators) 613 | end 614 | 615 | def test_reduced_unit 616 | assert_equal([[], []], unit(1, :meter, :meter).unit) 617 | end 618 | end 619 | end 620 | end 621 | -------------------------------------------------------------------------------- /spec/unit/auom_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | describe AUOM do 4 | describe AUOM::Algebra do 5 | describe '#+' do 6 | subject { object + operand } 7 | 8 | let(:object) { AUOM::Unit.new(*arguments) } 9 | 10 | context 'when unit is unitless' do 11 | let(:arguments) { [1] } 12 | 13 | context 'and operand is an Integer' do 14 | let(:operand) { 2 } 15 | 16 | it_should_behave_like 'an operation' 17 | 18 | it { should eql(AUOM::Unit.new(3)) } 19 | end 20 | 21 | context 'and operand is a unitless unit' do 22 | let(:operand) { AUOM::Unit.new(2) } 23 | 24 | it { should eql(AUOM::Unit.new(3)) } 25 | end 26 | 27 | context 'and operand is a unitful unit' do 28 | let(:operand) { AUOM::Unit.new(2, :meter) } 29 | 30 | it_should_behave_like 'an incompatible operation' 31 | end 32 | end 33 | 34 | context 'when unit is unitful' do 35 | let(:arguments) { [1, :meter, :euro] } 36 | 37 | context 'and operand is an Integer' do 38 | let(:operand) { 2 } 39 | 40 | it_should_behave_like 'an incompatible operation' 41 | end 42 | 43 | context 'and operand is a unitless unit' do 44 | let(:operand) { AUOM::Unit.new(2) } 45 | 46 | it_should_behave_like 'an incompatible operation' 47 | end 48 | 49 | context 'and operand is a incompatible unit' do 50 | let(:operand) { AUOM::Unit.new(2, :euro) } 51 | 52 | it_should_behave_like 'an incompatible operation' 53 | end 54 | 55 | context 'and operand is a compatible unit' do 56 | let(:operand) { AUOM::Unit.new(2, :meter, :euro) } 57 | 58 | it { should eql(AUOM::Unit.new(3, :meter, :euro)) } 59 | end 60 | end 61 | end 62 | 63 | describe '#/' do 64 | subject { object / operand } 65 | 66 | let(:object) { AUOM::Unit.new(*arguments) } 67 | 68 | context 'with unitless unit' do 69 | let(:arguments) { [4] } 70 | 71 | context 'when operand is a fixnum' do 72 | let(:operand) { 2 } 73 | 74 | it_should_behave_like 'an operation' 75 | 76 | it { should eql(AUOM::Unit.new(2)) } 77 | end 78 | 79 | context 'when operand is a unitless unit' do 80 | let(:operand) { AUOM::Unit.new(2) } 81 | 82 | it_should_behave_like 'an operation' 83 | 84 | it { should eql(AUOM::Unit.new(2)) } 85 | end 86 | 87 | context 'when operand is a unitful unit' do 88 | let(:operand) { AUOM::Unit.new(2, :meter) } 89 | 90 | it_should_behave_like 'an operation' 91 | 92 | it { should eql(AUOM::Unit.new(2, [], :meter)) } 93 | end 94 | end 95 | 96 | context 'with unitful unit' do 97 | let(:arguments) { [2, :meter] } 98 | 99 | context 'when operand is a fixnum' do 100 | let(:operand) { 1 } 101 | 102 | it_should_behave_like 'an operation' 103 | 104 | it { should eql(AUOM::Unit.new(2, :meter)) } 105 | end 106 | 107 | context 'when operand is a unitless unit' do 108 | let(:operand) { AUOM::Unit.new(1) } 109 | 110 | it_should_behave_like 'an operation' 111 | 112 | it { should eql(AUOM::Unit.new(2, :meter)) } 113 | end 114 | 115 | context 'when operand is a unitful unit' do 116 | context 'and units get added to denominator' do 117 | let(:operand) { AUOM::Unit.new(1, :euro) } 118 | 119 | it_should_behave_like 'an operation' 120 | 121 | it { should eql(AUOM::Unit.new(2, :meter, :euro)) } 122 | end 123 | 124 | context 'and units get added to numerator' do 125 | let(:operand) { AUOM::Unit.new(1, nil, :euro) } 126 | 127 | it_should_behave_like 'an operation' 128 | 129 | it { should eql(AUOM::Unit.new(2, %i[euro meter])) } 130 | end 131 | 132 | context 'and units cancel each other' do 133 | let(:operand) { AUOM::Unit.new(1, :meter) } 134 | 135 | it_should_behave_like 'an operation' 136 | 137 | it { should eql(AUOM::Unit.new(2)) } 138 | end 139 | end 140 | end 141 | end 142 | 143 | describe '#*' do 144 | subject { object * operand } 145 | 146 | let(:object) { AUOM::Unit.new(*arguments) } 147 | 148 | context 'with unitless unit' do 149 | let(:arguments) { [2] } 150 | 151 | context 'when operand is a fixnum' do 152 | let(:operand) { 3 } 153 | 154 | it_should_behave_like 'an operation' 155 | 156 | it { should eql(AUOM::Unit.new(6)) } 157 | end 158 | 159 | context 'when operand is a unitless unit' do 160 | let(:operand) { AUOM::Unit.new(3) } 161 | 162 | it_should_behave_like 'an operation' 163 | 164 | it { should eql(AUOM::Unit.new(6)) } 165 | end 166 | 167 | context 'when operand is a unitful unit' do 168 | let(:operand) { AUOM::Unit.new(3, :meter) } 169 | 170 | it_should_behave_like 'an operation' 171 | 172 | it { should eql(AUOM::Unit.new(6, :meter)) } 173 | end 174 | end 175 | 176 | context 'with unitful unit' do 177 | let(:arguments) { [2, :meter, :kilogramm] } 178 | 179 | context 'when operand is a fixnum' do 180 | let(:operand) { 3 } 181 | 182 | it_should_behave_like 'an operation' 183 | 184 | it { should eql(AUOM::Unit.new(6, :meter, :kilogramm)) } 185 | end 186 | 187 | context 'when operand is a unitless unit' do 188 | let(:operand) { AUOM::Unit.new(3) } 189 | 190 | it_should_behave_like 'an operation' 191 | 192 | it { should eql(AUOM::Unit.new(6, :meter, :kilogramm)) } 193 | end 194 | 195 | context 'when operand is a unitful unit' do 196 | context 'and units get added to numerator' do 197 | let(:operand) { AUOM::Unit.new(3, :meter) } 198 | 199 | it_should_behave_like 'an operation' 200 | 201 | it { should eql(AUOM::Unit.new(6, %i[meter meter], :kilogramm)) } 202 | end 203 | 204 | context 'and units get added to denominator' do 205 | let(:operand) { AUOM::Unit.new(3, [], :euro) } 206 | 207 | it_should_behave_like 'an operation' 208 | 209 | it { should eql(AUOM::Unit.new(6, :meter, %i[euro kilogramm])) } 210 | end 211 | 212 | context 'and units cancel each other' do 213 | let(:operand) { AUOM::Unit.new(3, [], :meter) } 214 | 215 | it_should_behave_like 'an operation' 216 | 217 | it { should eql(AUOM::Unit.new(6, [], :kilogramm)) } 218 | end 219 | end 220 | end 221 | end 222 | 223 | describe '#-' do 224 | subject { object - operand } 225 | 226 | let(:object) { AUOM::Unit.new(*arguments) } 227 | 228 | context 'when unit is unitless' do 229 | let(:arguments) { [1] } 230 | 231 | context 'and operand is an Integer' do 232 | let(:operand) { 1 } 233 | 234 | it_should_behave_like 'an operation' 235 | 236 | it { should eql(AUOM::Unit.new(0)) } 237 | end 238 | 239 | context 'and operand is a unitless unit' do 240 | let(:operand) { AUOM::Unit.new(1) } 241 | 242 | it { should eql(AUOM::Unit.new(0)) } 243 | end 244 | 245 | context 'and operand is a unitful unit' do 246 | let(:operand) { AUOM::Unit.new(1, :meter) } 247 | 248 | it_should_behave_like 'an incompatible operation' 249 | end 250 | end 251 | 252 | context 'when unit is unitful' do 253 | let(:arguments) { [1, :meter] } 254 | 255 | context 'and operand is an Integer' do 256 | let(:operand) { 1 } 257 | 258 | it_should_behave_like 'an incompatible operation' 259 | end 260 | 261 | context 'and operand is a unitless unit' do 262 | let(:operand) { AUOM::Unit.new(1) } 263 | 264 | it_should_behave_like 'an incompatible operation' 265 | end 266 | 267 | context 'and operand is a incompatible unit' do 268 | let(:operand) { AUOM::Unit.new(1, :euro) } 269 | 270 | it_should_behave_like 'an incompatible operation' 271 | end 272 | 273 | context 'and operand is a compatible unit' do 274 | let(:operand) { AUOM::Unit.new(1, :meter) } 275 | 276 | it { should eql(AUOM::Unit.new(0, :meter)) } 277 | end 278 | end 279 | end 280 | end 281 | 282 | describe AUOM::Equalization, '#==' do 283 | subject { object == other } 284 | 285 | let(:object) { AUOM::Unit.new(1, :meter) } 286 | 287 | context 'when is other kind of object' do 288 | context 'that cannot be converted' do 289 | let(:other) { Object.new } 290 | 291 | it { should be(false) } 292 | end 293 | 294 | context 'that can be converted' do 295 | let(:other) { 1 } 296 | 297 | context 'and does not have the same values' do 298 | it { should be(false) } 299 | end 300 | 301 | context 'and does have the same values' do 302 | let(:other) { 1 } 303 | let(:object) { AUOM::Unit.new(1) } 304 | it { should be(true) } 305 | end 306 | end 307 | end 308 | 309 | context 'when is same kind of object' do 310 | let(:other) { AUOM::Unit.new(scalar, *unit) } 311 | 312 | let(:scalar) { 1 } 313 | let(:unit) { [:meter] } 314 | 315 | context 'and scalar and unit are the same' do 316 | it { should be(true) } 317 | end 318 | 319 | context 'and scalar is different' do 320 | let(:scalar) { 2 } 321 | it { should be(false) } 322 | end 323 | 324 | context 'and unit is different' do 325 | let(:unit) { [:euro] } 326 | it { should be(false) } 327 | end 328 | 329 | context 'and scalar and unit is different' do 330 | let(:scalar) { 2 } 331 | let(:unit) { [:euro] } 332 | it { should be(false) } 333 | end 334 | end 335 | end 336 | 337 | describe AUOM::Inspection do 338 | describe '#inspect' do 339 | subject { object.inspect } 340 | 341 | let(:object) { AUOM::Unit.new(scalar, *unit) } 342 | let(:unit) { [] } 343 | 344 | context 'when scalar is exact in decimal' do 345 | let(:scalar) { 1 } 346 | 347 | it { should eql('') } 348 | end 349 | 350 | context 'when scalar is NOT exact in decimal' do 351 | let(:scalar) { Rational(1, 2) } 352 | 353 | it { should eql('') } 354 | end 355 | 356 | context 'when has only numerator' do 357 | let(:scalar) { Rational(1, 3) } 358 | let(:unit) { [:meter] } 359 | 360 | it { should eql('') } 361 | end 362 | 363 | context 'when has only denominator' do 364 | let(:scalar) { Rational(1, 3) } 365 | let(:unit) { [[], :meter] } 366 | 367 | it { should eql('') } 368 | end 369 | 370 | context 'when has numerator and denominator' do 371 | let(:scalar) { Rational(1, 3) } 372 | let(:unit) { %i[euro meter] } 373 | 374 | it { should eql('') } 375 | end 376 | end 377 | 378 | describe '.prettify_unit_part' do 379 | subject { object.prettify_unit_part(part) } 380 | 381 | let(:object) { AUOM::Inspection } 382 | 383 | context 'with simple part' do 384 | let(:part) { [:meter] } 385 | 386 | it { should eql('meter') } 387 | end 388 | 389 | context 'with mixed part' do 390 | let(:part) { %i[meter euro] } 391 | 392 | it { should eql('euro*meter') } 393 | end 394 | 395 | context 'with complex part' do 396 | let(:part) { %i[meter meter euro] } 397 | 398 | it { should eql('meter^2*euro') } 399 | end 400 | 401 | context 'with very complex part' do 402 | let(:part) { %i[meter meter euro euro kilogramm] } 403 | 404 | it { should eql('euro^2*meter^2*kilogramm') } 405 | end 406 | end 407 | end 408 | 409 | describe AUOM::Relational do 410 | describe '#<=>' do 411 | subject { object <=> other } 412 | 413 | let(:object) { AUOM::Unit.new(1, :meter) } 414 | let(:other) { AUOM::Unit.new(scalar, unit) } 415 | 416 | context 'when operand unit is the same' do 417 | let(:unit) { :meter } 418 | 419 | context 'and operand scalar is less than receiver scalar' do 420 | let(:scalar) { 0 } 421 | 422 | it { should be(1) } 423 | end 424 | 425 | context 'and operand scalar is equal to receiver scalar' do 426 | let(:scalar) { 1 } 427 | 428 | it { should be(0) } 429 | end 430 | 431 | context 'and operand scalar is greater than receiver scalar' do 432 | let(:scalar) { 2 } 433 | 434 | it { should be(-1) } 435 | end 436 | end 437 | 438 | context 'when operand unit is not the same' do 439 | let(:scalar) { 1 } 440 | let(:unit) { :euro } 441 | 442 | it_should_behave_like 'an incompatible operation' 443 | end 444 | end 445 | 446 | describe 'Comparable inclusion' do 447 | specify do 448 | expect(AUOM::Unit < Comparable).to be(true) 449 | end 450 | end 451 | end 452 | 453 | describe AUOM::Unit do 454 | describe '#assert_same_unit' do 455 | subject { object.assert_same_unit(other) } 456 | 457 | let(:object) { described_class.new(1, *unit) } 458 | let(:unit) { [%i[meter], %i[euro]] } 459 | 460 | context 'when units are the same' do 461 | let(:other) { AUOM::Unit.new(2, :meter, :euro) } 462 | 463 | it { should be(object) } 464 | end 465 | 466 | context 'when units are not the same' do 467 | let(:other) { AUOM::Unit.new(2, :meter) } 468 | 469 | it_should_behave_like 'an incompatible operation' 470 | end 471 | end 472 | 473 | describe '#denominators' do 474 | subject { object.denominators } 475 | 476 | let(:object) { described_class.new(1, *unit) } 477 | let(:unit) { [[:meter], [:euro]] } 478 | 479 | it 'should return denominators' do 480 | should eql([:euro]) 481 | end 482 | 483 | it { should be_frozen } 484 | 485 | it_should_behave_like 'an idempotent method' 486 | end 487 | 488 | describe '#numerators' do 489 | subject { object.numerators } 490 | 491 | let(:object) { described_class.new(1, *unit) } 492 | let(:unit) { [[:meter], [:euro]] } 493 | 494 | it 'should return numerators' do 495 | should eql([:meter]) 496 | end 497 | 498 | it { should be_frozen } 499 | 500 | it_should_behave_like 'an idempotent method' 501 | end 502 | 503 | describe '#same_unit?' do 504 | subject { object.same_unit?(other) } 505 | 506 | let(:object) { described_class.new(1, *unit) } 507 | let(:unit) { [[:meter], [:euro]] } 508 | 509 | context 'when units are the same' do 510 | let(:other) { AUOM::Unit.new(2, :meter, :euro) } 511 | 512 | it { should be(true) } 513 | end 514 | 515 | context 'when units are not the same' do 516 | let(:other) { AUOM::Unit.new(2, :meter) } 517 | 518 | it { should be(false) } 519 | end 520 | end 521 | 522 | describe '#denominators' do 523 | subject { object.scalar } 524 | 525 | let(:object) { described_class.new(scalar) } 526 | let(:scalar) { Rational(1, 2) } 527 | 528 | it 'should return scalar' do 529 | should eql(scalar) 530 | end 531 | 532 | it { should be_frozen } 533 | 534 | it_should_behave_like 'an idempotent method' 535 | end 536 | 537 | describe '#unitless?' do 538 | subject { object.unitless? } 539 | 540 | let(:object) { described_class.new(1, *unit) } 541 | 542 | context 'when unit is unitless' do 543 | let(:unit) { [] } 544 | it { should be(true) } 545 | end 546 | 547 | context 'when unit has no denominator' do 548 | let(:unit) { [:meter] } 549 | 550 | it { should be(false) } 551 | end 552 | 553 | context 'when unit has no numerator' do 554 | let(:unit) { [[], :meter] } 555 | 556 | it { should be(false) } 557 | end 558 | end 559 | 560 | describe '#unit' do 561 | subject { object.unit } 562 | 563 | let(:object) { described_class.new(1, *unit) } 564 | let(:unit) { [[:meter], [:euro]] } 565 | 566 | it 'should return unit of unit instance' do 567 | should eql(unit) 568 | end 569 | 570 | it { should be_frozen } 571 | 572 | it_should_behave_like 'an idempotent method' 573 | end 574 | 575 | describe '.convert' do 576 | subject { object.convert(value) } 577 | 578 | let(:object) { AUOM::Unit } 579 | 580 | context 'with nil' do 581 | let(:value) { nil } 582 | 583 | it 'should raise error' do 584 | expect { subject }.to raise_error(ArgumentError, 'Cannot convert nil to AUOM::Unit') 585 | end 586 | end 587 | 588 | context 'with fixnum' do 589 | let(:value) { 1 } 590 | 591 | it { should eql(AUOM::Unit.new(1)) } 592 | end 593 | 594 | context 'with rational' do 595 | let(:value) { Rational(2, 1) } 596 | 597 | it { should eql(AUOM::Unit.new(2)) } 598 | end 599 | 600 | context 'with Object' do 601 | let(:value) { Object.new } 602 | 603 | it 'should raise error' do 604 | expect { subject }.to raise_error(ArgumentError, "Cannot convert #{value.inspect} to AUOM::Unit") 605 | end 606 | end 607 | end 608 | 609 | describe '.lookup' do 610 | subject { object.__send__(:lookup, value) } 611 | 612 | let(:object) { described_class } 613 | 614 | context 'with existing symbol' do 615 | let(:value) { :meter } 616 | 617 | it { should eql([1, :meter]) } 618 | end 619 | 620 | context 'with inexistent symbol' do 621 | let(:value) { :foo } 622 | 623 | it 'should raise error' do 624 | expect { subject }.to raise_error(ArgumentError, 'Unknown unit :foo') 625 | end 626 | end 627 | end 628 | 629 | describe '.new' do 630 | let(:object) { described_class } 631 | 632 | subject do 633 | described_class.new(*arguments) 634 | end 635 | 636 | shared_examples_for 'invalid unit' do 637 | it 'should raise ArgumentError' do 638 | expect { subject }.to raise_error(ArgumentError) 639 | end 640 | end 641 | 642 | let(:expected_scalar) { 1 } 643 | let(:expected_numerators) { [] } 644 | let(:expected_denominators) { [] } 645 | 646 | shared_examples_for 'valid unit' do 647 | it { should be_frozen } 648 | 649 | its(:scalar) { should == expected_scalar } 650 | its(:scalar) { should be_frozen } 651 | its(:numerators) { should == expected_numerators } 652 | its(:numerators) { should be_frozen } 653 | its(:denominators) { should == expected_denominators } 654 | its(:denominators) { should be_frozen } 655 | end 656 | 657 | describe 'without arguments' do 658 | let(:arguments) { [] } 659 | it_should_behave_like 'invalid unit' 660 | end 661 | 662 | describe 'with one scalar argument' do 663 | let(:arguments) { [argument] } 664 | 665 | context 'when scalar is a string' do 666 | let(:argument) { '10.31' } 667 | it 'should raise error' do 668 | expect { subject }.to raise_error(ArgumentError, '"10.31" cannot be converted to rational') 669 | end 670 | end 671 | 672 | context 'when argument is an Integer' do 673 | let(:argument) { 1 } 674 | 675 | it_should_behave_like 'unitless unit' 676 | it_should_behave_like 'valid unit' 677 | end 678 | 679 | context 'when argument is a Rational' do 680 | let(:argument) { Rational(1, 1) } 681 | 682 | it_should_behave_like 'unitless unit' 683 | it_should_behave_like 'valid unit' 684 | end 685 | 686 | context 'when argument is something else' do 687 | let(:argument) { 1.0 } 688 | 689 | it_should_behave_like 'invalid unit' 690 | end 691 | end 692 | 693 | describe 'with scalar and numerator argument' do 694 | let(:arguments) { [1, argument] } 695 | 696 | context 'when argument is a valid unit' do 697 | let(:argument) { :kilogramm } 698 | let(:expected_numerators) { [:kilogramm] } 699 | 700 | it_should_behave_like 'valid unit' 701 | end 702 | 703 | context 'when argument is a valid unit alias' do 704 | let(:argument) { :kilometer } 705 | let(:expected_numerators) { [:meter] } 706 | let(:expected_scalar) { 1000 } 707 | 708 | it_should_behave_like 'valid unit' 709 | end 710 | 711 | context 'when argument is an array of valid units' do 712 | let(:argument) { %i[kilogramm meter] } 713 | let(:expected_numerators) { %i[kilogramm meter] } 714 | 715 | it_should_behave_like 'valid unit' 716 | end 717 | 718 | context 'when argument is an array with invalid unit' do 719 | let(:argument) { %i[kilogramm nonsense] } 720 | 721 | it_should_behave_like 'invalid unit' 722 | end 723 | 724 | context 'when argument is an invalid unit' do 725 | let(:argument) { :nonsense } 726 | 727 | it_should_behave_like 'invalid unit' 728 | end 729 | end 730 | 731 | describe 'with scalar, numerator and denominator argument' do 732 | let(:arguments) { [1, :kilogramm, argument] } 733 | let(:expected_numerators) { %i[kilogramm] } 734 | 735 | context 'when argument is a valid unit' do 736 | let(:argument) { :meter } 737 | let(:expected_denominators) { [:meter] } 738 | 739 | it_should_behave_like 'valid unit' 740 | end 741 | 742 | context 'when argument is a valid unit alias' do 743 | let(:argument) { :kilometer } 744 | 745 | let(:expected_denominators) { [:meter] } 746 | let(:expected_scalar) { Rational(1, 1000) } 747 | 748 | it_should_behave_like 'valid unit' 749 | end 750 | 751 | context 'when argument is an array of valid units' do 752 | let(:argument) { %i[euro meter] } 753 | let(:expected_denominators) { %i[euro meter] } 754 | 755 | it_should_behave_like 'valid unit' 756 | end 757 | 758 | context 'when argument is an array with invalid unit' do 759 | let(:argument) { %i[euro nonsense] } 760 | 761 | it_should_behave_like 'invalid unit' 762 | end 763 | 764 | context 'when argument is an invalid unit' do 765 | let(:argument) { :nonsense } 766 | 767 | it_should_behave_like 'invalid unit' 768 | end 769 | end 770 | 771 | context 'when numerators and denominators overlap' do 772 | let(:arguments) { [1, numerators, denominators] } 773 | let(:numerators) { %i[kilogramm meter euro] } 774 | let(:denominators) { %i[meter meter] } 775 | let(:expected_numerators) { %i[euro kilogramm] } 776 | let(:expected_denominators) { [:meter] } 777 | 778 | it_should_behave_like 'valid unit' 779 | end 780 | end 781 | 782 | describe '.try_convert' do 783 | subject { object.try_convert(value) } 784 | 785 | let(:object) { AUOM::Unit } 786 | 787 | context 'with unit' do 788 | let(:value) { AUOM::Unit.new(1) } 789 | 790 | it { should be(value) } 791 | end 792 | 793 | context 'with nil' do 794 | let(:value) { nil } 795 | 796 | it { should be(nil) } 797 | end 798 | 799 | context 'with fixnum' do 800 | let(:value) { 1 } 801 | 802 | it { should eql(AUOM::Unit.new(1)) } 803 | end 804 | 805 | context 'with rational' do 806 | let(:value) { Rational(2, 1) } 807 | 808 | it { should eql(AUOM::Unit.new(2)) } 809 | end 810 | 811 | context 'with Object' do 812 | let(:value) { Object.new } 813 | 814 | it { should be(nil) } 815 | end 816 | end 817 | 818 | describe '.units' do 819 | subject { object.units } 820 | 821 | let(:object) { described_class } 822 | 823 | it { should be_a(Hash) } 824 | 825 | it_should_behave_like 'an idempotent method' 826 | end 827 | end 828 | end 829 | --------------------------------------------------------------------------------