Skip to content

Commit

Permalink
Allow for underscores in unit names and aliases. (#331)
Browse files Browse the repository at this point in the history
Fixes #330
  • Loading branch information
olbrich authored Nov 2, 2023
1 parent e90fc78 commit ccae37d
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 83 deletions.
4 changes: 3 additions & 1 deletion lib/ruby_units/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1581,7 +1581,9 @@ def parse(passed_unit_string = '0')
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', '_' => '', ',' => '')
unit_string.gsub!(/(\d)[_,](\d)/, '\1\2') # remove underscores and commas in numbers

unit_string.gsub!(/[%'"#]/, '%' => 'percent', "'" => 'feet', '"' => 'inch', '#' => 'pound')
if unit_string.start_with?(COMPLEX_NUMBER)
match = unit_string.match(COMPLEX_REGEX)
real = Float(match[:real]) if match[:real]
Expand Down
12 changes: 9 additions & 3 deletions spec/ruby_units/definition_spec.rb
Original file line number Diff line number Diff line change
@@ -1,41 +1,47 @@
require File.dirname(__FILE__) + '/../spec_helper'
require_relative '../spec_helper'

describe "Unit::Definition('eV')" do
subject do
Unit::Definition.new('eV') do |ev|
ev.aliases = ['eV', 'electron-volt']
ev.aliases = %w[eV electron-volt 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('<eV>') }
end

describe '#aliases' do
subject { super().aliases }
it { is_expected.to eq(%w[eV electron-volt]) }

it { is_expected.to eq(%w[eV electron-volt 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('<kilogram>', '<meter>', '<meter>') }
end

describe '#denominator' do
subject { super().denominator }

it { is_expected.to include('<second>', '<second>') }
end

describe '#display_name' do
subject { super().display_name }

it { is_expected.to eq('electron-volt') }
end
end
179 changes: 100 additions & 79 deletions spec/ruby_units/parsing_spec.rb
Original file line number Diff line number Diff line change
@@ -1,92 +1,113 @@
require 'spec_helper'

RSpec.describe 'Number parsing' do
context 'with Integers' do
it { expect(RubyUnits::Unit.new('1')).to have_attributes(scalar: 1) }
it { expect(RubyUnits::Unit.new('-1')).to have_attributes(scalar: -1) }
it { expect(RubyUnits::Unit.new('+1')).to have_attributes(scalar: 1) }
it { expect(RubyUnits::Unit.new('01')).to have_attributes(scalar: 1) }
it { expect(RubyUnits::Unit.new('1,000')).to have_attributes(scalar: 1000) }
it { expect(RubyUnits::Unit.new('1_000')).to have_attributes(scalar: 1000) }
end
RSpec.describe 'Parsing' do
describe 'Parsing numbers' do
context 'with Integers' do
it { expect(RubyUnits::Unit.new('1')).to have_attributes(scalar: 1) }
it { expect(RubyUnits::Unit.new('-1')).to have_attributes(scalar: -1) }
it { expect(RubyUnits::Unit.new('+1')).to have_attributes(scalar: 1) }
it { expect(RubyUnits::Unit.new('01')).to have_attributes(scalar: 1) }
it { expect(RubyUnits::Unit.new('1,000')).to have_attributes(scalar: 1000) }
it { expect(RubyUnits::Unit.new('1_000')).to have_attributes(scalar: 1000) }
end

context 'with Decimals' do
# NOTE: that since this float is the same as an integer, the integer is returned
it { expect(RubyUnits::Unit.new('1.0').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('-1.0').scalar).to be(-1) }
context 'with Decimals' do
# NOTE: that since this float is the same as an integer, the integer is returned
it { expect(RubyUnits::Unit.new('1.0').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('-1.0').scalar).to be(-1) }

it { expect(RubyUnits::Unit.new('1.1').scalar).to be(1.1) }
it { expect(RubyUnits::Unit.new('-1.1').scalar).to be(-1.1) }
it { expect(RubyUnits::Unit.new('+1.1').scalar).to be(1.1) }
it { expect(RubyUnits::Unit.new('0.1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-0.1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+0.1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('.1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-.1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+.1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('1.1').scalar).to be(1.1) }
it { expect(RubyUnits::Unit.new('-1.1').scalar).to be(-1.1) }
it { expect(RubyUnits::Unit.new('+1.1').scalar).to be(1.1) }
it { expect(RubyUnits::Unit.new('0.1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-0.1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+0.1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('.1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-.1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+.1').scalar).to be(0.1) }

it { expect { RubyUnits::Unit.new('0.1.') }.to raise_error(ArgumentError) }
it { expect { RubyUnits::Unit.new('-0.1.') }.to raise_error(ArgumentError) }
it { expect { RubyUnits::Unit.new('+0.1.') }.to raise_error(ArgumentError) }
end
it { expect { RubyUnits::Unit.new('0.1.') }.to raise_error(ArgumentError) }
it { expect { RubyUnits::Unit.new('-0.1.') }.to raise_error(ArgumentError) }
it { expect { RubyUnits::Unit.new('+0.1.') }.to raise_error(ArgumentError) }
end

context 'with Fractions' do
it { expect(RubyUnits::Unit.new('1/1').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('-1/1').scalar).to be(-1) }
it { expect(RubyUnits::Unit.new('+1/1').scalar).to be(1) }
context 'with Fractions' do
it { expect(RubyUnits::Unit.new('1/1').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('-1/1').scalar).to be(-1) }
it { expect(RubyUnits::Unit.new('+1/1').scalar).to be(1) }

# NOTE: eql? is used here because two equivalent Rational objects are not the same object, unlike Integers
it { expect(RubyUnits::Unit.new('1/2').scalar).to eql(1/2r) }
it { expect(RubyUnits::Unit.new('-1/2').scalar).to eql(-1/2r) }
it { expect(RubyUnits::Unit.new('+1/2').scalar).to eql(1/2r) }
it { expect(RubyUnits::Unit.new('(1/2)').scalar).to eql(1/2r) }
it { expect(RubyUnits::Unit.new('(-1/2)').scalar).to eql(-1/2r) }
it { expect(RubyUnits::Unit.new('(+1/2)').scalar).to eql(1/2r) }
# NOTE: eql? is used here because two equivalent Rational objects are not the same object, unlike Integers
it { expect(RubyUnits::Unit.new('1/2').scalar).to eql(1/2r) }
it { expect(RubyUnits::Unit.new('-1/2').scalar).to eql(-1/2r) }
it { expect(RubyUnits::Unit.new('+1/2').scalar).to eql(1/2r) }
it { expect(RubyUnits::Unit.new('(1/2)').scalar).to eql(1/2r) }
it { expect(RubyUnits::Unit.new('(-1/2)').scalar).to eql(-1/2r) }
it { expect(RubyUnits::Unit.new('(+1/2)').scalar).to eql(1/2r) }

# improper fractions
it { expect(RubyUnits::Unit.new('1 1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('-1 1/2').scalar).to eql(-3/2r) }
it { expect(RubyUnits::Unit.new('+1 1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('1-1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('-1-1/2').scalar).to eql(-3/2r) }
it { expect(RubyUnits::Unit.new('+1-1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('1 2/2').scalar).to be(2) } # weird, but not wrong
it { expect(RubyUnits::Unit.new('1 3/2').scalar).to eql(5/2r) } # weird, but not wrong
it { expect { RubyUnits::Unit.new('1.5 1/2') }.to raise_error(ArgumentError, 'Improper fractions must have a whole number part') }
it { expect { RubyUnits::Unit.new('1.5/2') }.to raise_error(ArgumentError, 'invalid value for Integer(): "1.5"') }
it { expect { RubyUnits::Unit.new('1/2.5') }.to raise_error(ArgumentError, 'invalid value for Integer(): "2.5"') }
end
# improper fractions
it { expect(RubyUnits::Unit.new('1 1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('-1 1/2').scalar).to eql(-3/2r) }
it { expect(RubyUnits::Unit.new('+1 1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('1-1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('-1-1/2').scalar).to eql(-3/2r) }
it { expect(RubyUnits::Unit.new('+1-1/2').scalar).to eql(3/2r) }
it { expect(RubyUnits::Unit.new('1 2/2').scalar).to be(2) } # weird, but not wrong
it { expect(RubyUnits::Unit.new('1 3/2').scalar).to eql(5/2r) } # weird, but not wrong
it { expect { RubyUnits::Unit.new('1.5 1/2') }.to raise_error(ArgumentError, 'Improper fractions must have a whole number part') }
it { expect { RubyUnits::Unit.new('1.5/2') }.to raise_error(ArgumentError, 'invalid value for Integer(): "1.5"') }
it { expect { RubyUnits::Unit.new('1/2.5') }.to raise_error(ArgumentError, 'invalid value for Integer(): "2.5"') }
end

context 'with Scientific Notation' do
it { expect(RubyUnits::Unit.new('1e0').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('-1e0').scalar).to be(-1) }
it { expect(RubyUnits::Unit.new('+1e0').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('1e1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('-1e1').scalar).to be(-10) }
it { expect(RubyUnits::Unit.new('+1e1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('1e-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-1e-1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+1e-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('1E+1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('-1E+1').scalar).to be(-10) }
it { expect(RubyUnits::Unit.new('+1E+1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('1E-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-1E-1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+1E-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('1.0e2').scalar).to be(100) }
it { expect(RubyUnits::Unit.new('.1e2').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('0.1e2').scalar).to be(10) }
it { expect { RubyUnits::Unit.new('0.1e2.5') }.to raise_error(ArgumentError) }
context 'with Scientific Notation' do
it { expect(RubyUnits::Unit.new('1e0').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('-1e0').scalar).to be(-1) }
it { expect(RubyUnits::Unit.new('+1e0').scalar).to be(1) }
it { expect(RubyUnits::Unit.new('1e1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('-1e1').scalar).to be(-10) }
it { expect(RubyUnits::Unit.new('+1e1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('1e-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-1e-1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+1e-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('1E+1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('-1E+1').scalar).to be(-10) }
it { expect(RubyUnits::Unit.new('+1E+1').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('1E-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('-1E-1').scalar).to be(-0.1) }
it { expect(RubyUnits::Unit.new('+1E-1').scalar).to be(0.1) }
it { expect(RubyUnits::Unit.new('1.0e2').scalar).to be(100) }
it { expect(RubyUnits::Unit.new('.1e2').scalar).to be(10) }
it { expect(RubyUnits::Unit.new('0.1e2').scalar).to be(10) }
it { expect { RubyUnits::Unit.new('0.1e2.5') }.to raise_error(ArgumentError) }
end

context 'with Complex numbers' do
it { expect(RubyUnits::Unit.new('1+1i').scalar).to eql(Complex(1, 1)) }
it { expect(RubyUnits::Unit.new('1i').scalar).to eql(Complex(0, 1)) }
it { expect(RubyUnits::Unit.new('-1i').scalar).to eql(Complex(0, -1)) }
it { expect(RubyUnits::Unit.new('-1+1i').scalar).to eql(Complex(-1, 1)) }
it { expect(RubyUnits::Unit.new('+1+1i').scalar).to eql(Complex(1, 1)) }
it { expect(RubyUnits::Unit.new('1-1i').scalar).to eql(Complex(1, -1)) }
it { expect(RubyUnits::Unit.new('-1.23-4.5i').scalar).to eql(Complex(-1.23, -4.5)) }
it { expect(RubyUnits::Unit.new('1+0i').scalar).to be(1) }
end
end

context 'with Complex numbers' do
it { expect(RubyUnits::Unit.new('1+1i').scalar).to eql(Complex(1, 1)) }
it { expect(RubyUnits::Unit.new('1i').scalar).to eql(Complex(0, 1)) }
it { expect(RubyUnits::Unit.new('-1i').scalar).to eql(Complex(0, -1)) }
it { expect(RubyUnits::Unit.new('-1+1i').scalar).to eql(Complex(-1, 1)) }
it { expect(RubyUnits::Unit.new('+1+1i').scalar).to eql(Complex(1, 1)) }
it { expect(RubyUnits::Unit.new('1-1i').scalar).to eql(Complex(1, -1)) }
it { expect(RubyUnits::Unit.new('-1.23-4.5i').scalar).to eql(Complex(-1.23, -4.5)) }
it { expect(RubyUnits::Unit.new('1+0i').scalar).to be(1) }
describe 'Unit parsing' do
before do
RubyUnits::Unit.define('m2') do |m2|
m2.definition = RubyUnits::Unit.new('meter^2')
m2.aliases = %w[m2 meter2 square_meter square-meter]
end
end

it {
expect(RubyUnits::Unit.new('m2')).to have_attributes(scalar: 1,
numerator: %w[<m2>],
denominator: ['<1>'],
kind: :area)
}

# make sure that underscores in the unit name are handled correctly
it { expect(RubyUnits::Unit.new('1_000 square_meter')).to eq(RubyUnits::Unit.new('1000 m^2')) }
end
end

0 comments on commit ccae37d

Please sign in to comment.