Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 52 additions & 36 deletions lib/ruby_units/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -785,27 +785,23 @@ def ==(other)
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
# Check to see if units are compatible, ignoring the scalar part. This check is done by comparing unit signatures
# for performance reasons. If passed a string, this 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)
x =~ y
rescue ArgumentError
false
end
end
return signature == other.signature if other.is_a?(Unit)

x, y = coerce(other)
x =~ y
rescue ArgumentError # return false when `other` cannot be converted to a [Unit]
false
end

alias compatible? =~
Expand Down Expand Up @@ -955,27 +951,50 @@ def /(other)
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]
# Returns the remainder when one unit is divided by another
#
# @param [Unit] other
# @return [Unit]
# @raise [ArgumentError] if units are not compatible
def remainder(other)
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other)

self.class.new(base_scalar.remainder(other.to_unit.base_scalar), to_base.units).convert_to(self)
end

# Divide two units and return quotient and remainder
#
# @param [Unit] other
# @return [Array(Integer, Unit)]
# @raise [ArgumentError] if units are not compatible
def divmod(other)
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless self =~ other
return scalar.divmod(other.scalar) if units == other.units
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other)

to_base.scalar.divmod(other.to_base.scalar)
[quo(other).to_base.floor, self % other]
end

# perform a modulo on a unit, will raise an exception if the units are not compatible
# @param [Object] other
# Perform a modulo on a unit, will raise an exception if the units are not compatible
#
# @param [Unit] other
# @return [Integer]
# @raise [ArgumentError] if units are not compatible
def %(other)
divmod(other).last
raise ArgumentError, "Incompatible Units ('#{self}' not compatible with '#{other}')" unless compatible_with?(other)

self.class.new(base_scalar % other.to_unit.base_scalar, to_base.units).convert_to(self)
end
alias modulo %

# @param [Object] other
# @return [Unit]
# @raise [ZeroDivisionError] if other is zero
def quo(other)
self / other
end
alias fdiv quo

# Exponentiation. 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.
# 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
Expand Down Expand Up @@ -1428,19 +1447,16 @@ def from(time_point)
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
# Automatically coerce objects to [Unit] when possible. If an object defines a '#to_unit' method, it will be coerced
# using that method.
#
# @param other [Object, #to_unit]
# @return [Array]
# @return [Array(Unit, Unit)]
# @raise [ArgumentError] when `other` cannot be converted to a [Unit]
def coerce(other)
return [other.to_unit, self] if other.respond_to? :to_unit
return [other.to_unit, self] if other.respond_to?(:to_unit)

case other
when Unit
[other, self]
else
[self.class.new(other), self]
end
[self.class.new(other), self]
end

# returns a new unit that has been scaled to be more in line with typical usage.
Expand Down
41 changes: 30 additions & 11 deletions spec/ruby_units/unit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1949,13 +1949,15 @@
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
describe 'modulo (%)' do
it { expect(RubyUnits::Unit.new('2 m') % RubyUnits::Unit.new('1 m')).to eq(0) }
it { expect(RubyUnits::Unit.new('2 m') % RubyUnits::Unit.new('1 m')).to eq(RubyUnits::Unit.new('0 m')) }
it { expect(RubyUnits::Unit.new('5 m') % RubyUnits::Unit.new('2 m')).to eq(RubyUnits::Unit.new('1 m')) }
it { expect(RubyUnits::Unit.new('5 m') % RubyUnits::Unit.new('-2 m')).to eq(RubyUnits::Unit.new('-1 m')) }
it { expect(RubyUnits::Unit.new(127) % 2).to eq(1) }
it { expect(RubyUnits::Unit.new(127) % -2).to eq(-1) }

it 'raises and exception with incompatible units' 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
Expand Down Expand Up @@ -2160,10 +2162,27 @@
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')") }
describe '#remainder' do
it { expect { RubyUnits::Unit.new('5 mm').remainder(2) }.to raise_error(ArgumentError, "Incompatible Units ('5 mm' not compatible with '2')") }
it { expect { RubyUnits::Unit.new('5 mm').remainder(RubyUnits::Unit.new('2 kg')) }.to raise_error(ArgumentError, "Incompatible Units ('5 mm' not compatible with '2 kg')") }
it { expect(RubyUnits::Unit.new('5 mm').remainder(RubyUnits::Unit.new('2 mm'))).to eq(RubyUnits::Unit.new('1 mm')) }
it { expect(RubyUnits::Unit.new('5 mm').remainder(RubyUnits::Unit.new('-2 mm'))).to eq(RubyUnits::Unit.new('1 mm')) }
it { expect(RubyUnits::Unit.new('5 cm').remainder(RubyUnits::Unit.new('1 in'))).to eq(RubyUnits::Unit.new('2.46 cm')) }
it { expect(RubyUnits::Unit.new(127).remainder(2)).to eq(1) }
it { expect(RubyUnits::Unit.new(127).remainder(-2)).to eq(1) }
end

describe '#divmod' do
it { expect(RubyUnits::Unit.new('5 mm').divmod(RubyUnits::Unit.new('2 mm'))).to eq([2, RubyUnits::Unit.new('1 mm')]) }
it { expect(RubyUnits::Unit.new('5 mm').divmod(RubyUnits::Unit.new('-2 mm'))).to eq([-3, RubyUnits::Unit.new('-1 mm')]) }
it { expect(RubyUnits::Unit.new('1 km').divmod(RubyUnits::Unit.new('2 m'))).to eq([500, RubyUnits::Unit.new('0 mm')]) }
it { 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

describe '#quo' do
it { expect(RubyUnits::Unit.new('5 mm').quo(RubyUnits::Unit.new('2 mm'))).to eq(2.5) }
it { expect(RubyUnits::Unit.new('1 km').quo(RubyUnits::Unit.new('2 s'))).to eq(RubyUnits::Unit.new('1/2 km/s')) }
it { expect { RubyUnits::Unit.new('1 km').quo(RubyUnits::Unit.new('0 s'))}.to raise_error(ZeroDivisionError) }
end

context '#div' do
Expand Down