From ccae37dd54cab89cd974b7cb487c1b8c240a872a Mon Sep 17 00:00:00 2001 From: Kevin Olbrich Date: Thu, 2 Nov 2023 07:21:25 -0400 Subject: [PATCH] Allow for underscores in unit names and aliases. (#331) Fixes #330 --- lib/ruby_units/unit.rb | 4 +- spec/ruby_units/definition_spec.rb | 12 +- spec/ruby_units/parsing_spec.rb | 179 ++++++++++++++++------------- 3 files changed, 112 insertions(+), 83 deletions(-) diff --git a/lib/ruby_units/unit.rb b/lib/ruby_units/unit.rb index 77a8c53c..0cbff933 100644 --- a/lib/ruby_units/unit.rb +++ b/lib/ruby_units/unit.rb @@ -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] diff --git a/spec/ruby_units/definition_spec.rb b/spec/ruby_units/definition_spec.rb index b9ab7e38..5242e632 100644 --- a/spec/ruby_units/definition_spec.rb +++ b/spec/ruby_units/definition_spec.rb @@ -1,9 +1,9 @@ -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 @@ -11,31 +11,37 @@ 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]) } + + 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('', '', '') } 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/parsing_spec.rb b/spec/ruby_units/parsing_spec.rb index 35c40c43..fb8d711b 100644 --- a/spec/ruby_units/parsing_spec.rb +++ b/spec/ruby_units/parsing_spec.rb @@ -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[], + 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