Skip to content

Commit

Permalink
Issue335-Unit-format-if-numerator-is-1-in-unit-faction (#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
olbrich authored Sep 1, 2024
1 parent 1c67c6c commit d25259d
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 47 deletions.
3 changes: 2 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
PATH
remote: .
specs:
ruby-units (4.0.3)
ruby-units (4.1.0)


GEM
remote: https://rubygems.org/
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,14 +296,15 @@ Configuration options can be set like:

```ruby
RubyUnits.configure do |config|
config.format = :rational
config.separator = false
end
```

Currently there is only one configuration you can set:

1. separator (true/false): should a space be used to separate the scalar from
the unit part during output.
| Option | Description | Valid Values | Default |
|-----------|------------------------------------------------------------------------------------------------------------------------|---------------------------|-------------|
| format | Only used for output formatting. `:rational` is formatted like `3 m/s^2`. `:exponential` is formatted like `3 m*s^-2`. | `:rational, :exponential` | `:rational` |
| separator | Use a space separator for output. `true` is formatted like `3 m/s`, `false` is like `3m/s`. | `true, false` | `true` |

### NOTES

Expand Down
28 changes: 28 additions & 0 deletions lib/ruby_units/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def self.configuration
@configuration ||= Configuration.new
end

# Reset the configuration to the default values
def self.reset
@configuration = Configuration.new
end
Expand All @@ -27,16 +28,43 @@ class Configuration
# Used to separate the scalar from the unit when generating output. A value
# of `true` will insert a single space, and `false` will prevent adding a
# space to the string representation of a unit.
#
# @!attribute [rw] separator
# @return [Boolean] whether to include a space between the scalar and the unit
attr_reader :separator

# The style of format to use by default when generating output. When set to `:exponential`, all units will be
# represented in exponential notation instead of using a numerator and denominator.
#
# @!attribute [rw] format
# @return [Symbol] the format to use when generating output (:rational or :exponential) (default: :rational)
attr_reader :format

def initialize
self.format = :rational
self.separator = true
end

# Use a space for the separator to use when generating output.
#
# @param value [Boolean] whether to include a space between the scalar and the unit
# @return [void]
def separator=(value)
raise ArgumentError, "configuration 'separator' may only be true or false" unless [true, false].include?(value)

@separator = value ? ' ' : nil
end

# Set the format to use when generating output.
# The `:rational` style will generate units string like `3 m/s^2` and the `:exponential` style will generate units
# like `3 m*s^-2`.
#
# @param value [Symbol] the format to use when generating output (:rational or :exponential)
# @return [void]
def format=(value)
raise ArgumentError, "configuration 'format' may only be :rational or :exponential" unless %i[rational exponential].include?(value)

@format = value
end
end
end
47 changes: 30 additions & 17 deletions lib/ruby_units/unit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -667,8 +667,10 @@ def to_base
# @note Rational scalars that are equal to an integer will be represented as integers (i.e, 6/1 => 6, 4/2 => 2, etc..)
# @param [Symbol] target_units
# @param [Float] precision - the precision to use when converting to a rational
# @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format
#
# @return [String]
def to_s(target_units = nil, precision: 0.0001)
def to_s(target_units = nil, precision: 0.0001, format: RubyUnits.configuration.format)
out = @output[target_units]
return out if out

Expand Down Expand Up @@ -696,26 +698,26 @@ def to_s(target_units = nil, precision: 0.0001)
when /(%[-+.\w#]+)\s*(.+)*/ # format string like '%0.2f in'
begin
if Regexp.last_match(2) # unit specified, need to convert
convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1))
convert_to(Regexp.last_match(2)).to_s(Regexp.last_match(1), format: format)
else
"#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units}".strip
"#{Regexp.last_match(1) % @scalar}#{separator}#{Regexp.last_match(2) || units(format: format)}".strip
end
rescue StandardError # parse it like a strftime format string
(DateTime.new(0) + self).strftime(target_units)
end
when /(\S+)/ # unit only 'mm' or '1/mm'
convert_to(Regexp.last_match(1)).to_s
convert_to(Regexp.last_match(1)).to_s(format: format)
else
raise 'unhandled case'
end
else
out = case @scalar
when Complex
"#{@scalar}#{separator}#{units}"
"#{@scalar}#{separator}#{units(format: format)}"
when Rational
"#{@scalar == @scalar.to_i ? @scalar.to_i : @scalar}#{separator}#{units}"
"#{@scalar == @scalar.to_i ? @scalar.to_i : @scalar}#{separator}#{units(format: format)}"
else
"#{'%g' % @scalar}#{separator}#{units}"
"#{'%g' % @scalar}#{separator}#{units(format: format)}"
end.strip
end
@output[target_units] = out
Expand Down Expand Up @@ -1266,8 +1268,10 @@ def as_json(*)
# Returns the 'unit' part of the Unit object without the scalar
#
# @param with_prefix [Boolean] include prefixes in output
# @param format [Symbol] Set to :exponential to force all units to be displayed in exponential format
#
# @return [String]
def units(with_prefix: true)
def units(with_prefix: true, format: nil)
return '' if @numerator == UNITY_ARRAY && @denominator == UNITY_ARRAY

output_numerator = ['1']
Expand All @@ -1289,15 +1293,24 @@ def units(with_prefix: true)
output_denominator = definitions.map { _1.map(&:display_name).join }
end

on = output_numerator
.uniq
.map { [_1, output_numerator.count(_1)] }
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
od = output_denominator
.uniq
.map { [_1, output_denominator.count(_1)] }
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
"#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip
on = output_numerator
.uniq
.map { [_1, output_numerator.count(_1)] }
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }

if format == :exponential
od = output_denominator
.uniq
.map { [_1, output_denominator.count(_1)] }
.map { |element, power| (element.to_s.strip + (power > 0 ? "^#{-power}" : '')) }
(on + od).join('*').strip
else
od = output_denominator
.uniq
.map { [_1, output_denominator.count(_1)] }
.map { |element, power| (element.to_s.strip + (power > 1 ? "^#{power}" : '')) }
"#{on.join('*')}#{od.empty? ? '' : "/#{od.join('*')}"}".strip
end
end

# negates the scalar of the Unit
Expand Down
2 changes: 1 addition & 1 deletion lib/ruby_units/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module RubyUnits
class Unit < Numeric
VERSION = '4.0.3'.freeze
VERSION = '4.1.0'.freeze
end
end
56 changes: 40 additions & 16 deletions spec/ruby_units/configuration_spec.rb
Original file line number Diff line number Diff line change
@@ -1,28 +1,52 @@
require 'spec_helper'

describe RubyUnits::Configuration do
describe '.separator is true' do
it 'has a space between the scalar and the unit' do
expect(RubyUnits::Unit.new('1 m').to_s).to eq '1 m'
describe '.separator' do
context 'when set to true' do
it 'has a space between the scalar and the unit' do
expect(RubyUnits::Unit.new('1 m').to_s).to eq '1 m'
end
end

context 'when set to false' do
around do |example|
RubyUnits.configure do |config|
config.separator = false
end
example.run
RubyUnits.reset
end

it 'does not have a space between the scalar and the unit' do
expect(RubyUnits::Unit.new('1 m').to_s).to eq '1m'
expect(RubyUnits::Unit.new('14.5 lbs').to_s(:lbs)).to eq '14lbs 8oz'
expect(RubyUnits::Unit.new('220 lbs').to_s(:stone)).to eq '15stone 10lbs'
expect(RubyUnits::Unit.new('14.2 ft').to_s(:ft)).to eq %(14'2-2/5")
expect(RubyUnits::Unit.new('1/2 cup').to_s).to eq '1/2cu'
expect(RubyUnits::Unit.new('123.55 lbs').to_s('%0.2f')).to eq '123.55lbs'
end
end
end

describe '.separator is false' do
around do |example|
RubyUnits.configure do |config|
config.separator = false
describe '.format' do
context 'when set to :rational' do
it 'uses rational notation' do
expect(RubyUnits::Unit.new('1 m/s^2').to_s).to eq '1 m/s^2'
end
example.run
RubyUnits.reset
end

it 'does not have a space between the scalar and the unit' do
expect(RubyUnits::Unit.new('1 m').to_s).to eq '1m'
expect(RubyUnits::Unit.new('14.5 lbs').to_s(:lbs)).to eq '14lbs 8oz'
expect(RubyUnits::Unit.new('220 lbs').to_s(:stone)).to eq '15stone 10lbs'
expect(RubyUnits::Unit.new('14.2 ft').to_s(:ft)).to eq %(14'2-2/5")
expect(RubyUnits::Unit.new('1/2 cup').to_s).to eq '1/2cu'
expect(RubyUnits::Unit.new('123.55 lbs').to_s('%0.2f')).to eq '123.55lbs'
context 'when set to :exponential' do
around do |example|
RubyUnits.configure do |config|
config.format = :exponential
end
example.run
RubyUnits.reset
end

it 'uses exponential notation' do
expect(RubyUnits::Unit.new('1 m/s^2').to_s).to eq '1 m*s^-2'
end
end
end
end
19 changes: 11 additions & 8 deletions spec/ruby_units/unit_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2315,14 +2315,17 @@
end
end

describe 'Unit Output formatting' do
context RubyUnits::Unit.new('10.5 m/s^2') do
specify { expect(subject.to_s).to eq('10.5 m/s^2') }
specify { expect(subject.to_s('%0.2f')).to eq('10.50 m/s^2') }
specify { expect(subject.to_s('%0.2e km/s^2')).to eq('1.05e-02 km/s^2') }
specify { expect(subject.to_s('km/s^2')).to eq('0.0105 km/s^2') }
specify { expect(subject.to_s(STDOUT)).to eq('10.5 m/s^2') }
specify { expect { subject.to_s('random string') }.to raise_error(ArgumentError, "'random' Unit not recognized") }
describe '#to_s (Unit Output formatting)' do
describe '10.5 m/s^2' do
subject(:unit) { RubyUnits::Unit.new('10.5 m/s^2') }

it { expect(unit.to_s).to eq('10.5 m/s^2') }
it { expect(unit.to_s(format: :exponential)).to eq('10.5 m*s^-2') }
it { expect(unit.to_s('%0.2f')).to eq('10.50 m/s^2') }
it { expect(unit.to_s('%0.2e km/s^2')).to eq('1.05e-02 km/s^2') }
it { expect(unit.to_s('km/s^2')).to eq('0.0105 km/s^2') }
it { expect(unit.to_s(STDOUT)).to eq('10.5 m/s^2') }
it { expect { unit.to_s('random string') }.to raise_error(ArgumentError, "'random' Unit not recognized") }
end

context 'for a unit with a custom display_name' do
Expand Down

0 comments on commit d25259d

Please sign in to comment.