Class: UOM::Unit

Inherits:
Object
  • Object
show all
Defined in:
lib/uom/unit.rb

Overview

A Unit demarcates a standard magnitude on a Measurement Dimension. A base unit is an unscaled dimensional Unit, e.g. METER. A derived unit is composed of other units. The derived unit includes a required dimensional axis unit and an optional scalar Factor, e.g. the millimeter unit is derived from the meter axis and the milli scaling factor.

The axis is orthogonal to the scalar. There is a distinct unit for each axis and scalar. The base unit specifies the permissible scalar factors.

Direct Known Subclasses

CompositeUnit

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*params, &converter) ⇒ Unit

Creates the Unit with the given label and parameters. The params include the following:

  • a unit label followed by zero, one or more Symbol unit abbreviations

  • one or more Dimension objects for a basic unit

  • an optional axis Unit for a derived unit

  • an optional scaling Factor for a derived unit

  • an optional normalization multiplier which converts this unit to the axis

For example, a second is defined as:

SECOND = Unit.new(:second, :sec, Dimension::TIME, MILLI, MICRO, NANO, PICO, FEMTO)

and a millisecond is defined as:

Unit.new(MILLI, UOM::SECOND)

In most cases, a derived unit does not need to define a label or abbreviation since these are inferred from the axis and factor.

Raises:



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/uom/unit.rb', line 37

def initialize(*params, &converter)
  # this long initializer ensures that every unit is correct by construction
  # the first symbol is the label
  labels = params.select { |param| Symbol === param }
  @label = labels.first
  # the optional Factor parameters are the permissible scaling factors
  factors = params.select { |param| Factor === param }.to_set
  # a convertable unit must have a unique factor
  if converter and factors.size > 1 then
    raise MeasurementError.new("Derived unit #{label} can have at most one scalar: #{factors.to_a.join(', ')}")
    @permissible_factors = []
  else
    @permissible_factors = factors
  end
  # a Numeric parameter indicates a conversion multiplier instead of a converter block
  multiplier = params.detect { |param| Numeric === param }
  if multiplier then
    # there can't be both a converter and a multiplier
    if converter then
      raise MeasurementError.new("Derived unit #{label} specifies both a conversion multiplier constant and a converter block")
    end
    # make the converter block from the multiplier
    converter = lambda { |n| n * multiplier }
  end
  # the optional single Unit parameter is the axis for a derived unit
  axes = params.select { |param| Unit === param }
  raise MeasurementError.new("Unit #{label} can have at most one axis: #{axes.join(', ')}") if axes.size > 1
  @axis = axes.first
  # validate that a convertable unit has an axis; the converter argument is an axis quantity
  raise MeasurementError.new("Derived unit #{label} has a converter but does not have an axis unit") if @default_converter and @axis.nil?
  # the axis of an underived base unit is the unit itself
  @axis ||= self
  # validate that there is not a converter on self
  raise MeasurementError.new("Unit #{label} specifies a converter but not a conversion unit") unless converter.nil? if @axis == self
  # the default converter for a derived unit is identity
  converter ||= lambda { |n| n } unless @axis == self
  # the scalar is the first specified factor, or UNIT if there are multiple permissible factors
  @scalar = @permissible_factors.size == 1 ? @permissible_factors.to_a.first : UNIT
  # validate the scalar
  if @axis == self then
    #a derived unit cannot have a scalar factor
    raise MeasurementError.new("Base unit #{label} cannot have a scalar value - #{@scalar}") unless @scalar == UNIT
  elsif @scalar != UNIT and not @axis.permissible_factors.include?(@scalar) then
    # a derived unit scalar factor must be in the axis permissible factors
    raise MeasurementError.new("Derived unit #{label} scalar #{scalar} not a #{@axis} permissible factor #{@axis.permissible_factors.to_a.join(', ')}")
  end
  # if a scalar is defined, then adjust the converter
  scaled_converter = @scalar == UNIT ? converter : lambda { |n| @scalar.as(@axis.scalar) * converter.call(n) } 
  # add the axis converter to the converters hash
  @converters = {}
  @converters[@axis] = scaled_converter if converter
  # define the multiplier converter inverse
  @axis.add_converter(self) { |n| 1.0 / scaled_converter.call(1.0 / n) } unless @scalar.nil? and multiplier.nil?
  # make the label from the scalar and axis
  @label ||= create_label
  # validate label existence
  raise MeasurementError.new("Unit does not have a label") if self.label.nil?
  # validate label uniqueness
  if Unit.extent.association.has_key?(@label) then
    raise MeasurementError.new("Unit label #{@label} conflicts with existing unit #{Unit.extent.association[@label].inspect}")
  end
  # get the dimension
  dimensions = params.select { |param| Dimension === param }
  if dimensions.empty? then
    # a base unit must have a dimension
    raise MeasurementError.new("Base unit #{label} is missing a dimension") if @axis == self
    # a derived unit dimension is the axis dimension
    @dimension = axis.dimension
  elsif dimensions.size > 1 then
    # there can be at most one dimension
    raise MeasurementError.new("Unit #{label} can have at most one dimension")
  else
    # the sole specified dimension
    @dimension = dimensions.first
  end
  # the remaining symbols are abbreviations
  @abbreviations = labels.size < 2 ? [] : labels[1..-1]
  # validate abbreviation uniqueness
  conflict = @abbreviations.detect { |abbrev| Unit.extent.association.has_key?(abbrev) }
  raise MeasurementError.new("Unit label #{@label} conflicts with an existing unit") if conflict
  # add this Unit to the extent
  Unit << self
end

Instance Attribute Details

#abbreviationsObject (readonly)

Returns the value of attribute abbreviations.



23
24
25
# File 'lib/uom/unit.rb', line 23

def abbreviations
  @abbreviations
end

#axisObject (readonly)

Returns the value of attribute axis.



23
24
25
# File 'lib/uom/unit.rb', line 23

def axis
  @axis
end

#dimensionObject (readonly)

Returns the value of attribute dimension.



23
24
25
# File 'lib/uom/unit.rb', line 23

def dimension
  @dimension
end

#labelObject (readonly)

Returns the value of attribute label.



23
24
25
# File 'lib/uom/unit.rb', line 23

def label
  @label
end

#permissible_factorsObject (readonly)

Returns the value of attribute permissible_factors.



23
24
25
# File 'lib/uom/unit.rb', line 23

def permissible_factors
  @permissible_factors
end

#scalarObject (readonly)

Returns the value of attribute scalar.



23
24
25
# File 'lib/uom/unit.rb', line 23

def scalar
  @scalar
end

Instance Method Details

#*(other) ⇒ Object

Returns a product CompositeUnit consisting of this unit and the other unit, e.g.:

(Unit.for(:pound) * Unit.for(:inch)).label #=> foot_pound


151
152
153
# File 'lib/uom/unit.rb', line 151

def *(other)
  CompositeUnit.for(self, other, :*)
end

#/(other) ⇒ Object

Returns a division CompositeUnit consisting of this unit and the other unit, e.g.:

(Unit.for(:gram) / Unit.for(:liter)).label #=> gram_per_liter


145
146
147
# File 'lib/uom/unit.rb', line 145

def /(other)
  CompositeUnit.for(self, other, :/)
end

#add_abbreviation(abbrev) ⇒ Object



133
134
135
136
# File 'lib/uom/unit.rb', line 133

def add_abbreviation(abbrev)
  @abbreviations << abbrev.to_sym
  Unit.extent.association[abbrev] = self
end

#add_converter(other, &converter) ⇒ Object

Defines a conversion from this unit to the other unit.



139
140
141
# File 'lib/uom/unit.rb', line 139

def add_converter(other, &converter)
  @converters[other] = converter
end

#as(quantity, unit) ⇒ Object

Returns the given quantity converted from this Unit into the given unit.



156
157
158
159
160
161
162
# File 'lib/uom/unit.rb', line 156

def as(quantity, unit)
  begin
    convert(quantity, unit)
  rescue MeasurementError => e
    raise MeasurementError.new("No conversion path from #{self} to #{unit} - #{e}")
  end
end

#basic?Boolean

Returns whether this unit’s axis is the unit itself.

Returns:

  • (Boolean)


129
130
131
# File 'lib/uom/unit.rb', line 129

def basic?
  self == axis
end

#basisObject

Returns the Unit which is the basis for a derived unit. If this unit’s axis is the axis itself, then that is the basis. Otherwise, the basis is this unit’s axis basis.



124
125
126
# File 'lib/uom/unit.rb', line 124

def basis
  basic? ? self : axis.basis
end

#inspectObject



168
169
170
# File 'lib/uom/unit.rb', line 168

def inspect
  "#{self.class.name}@#{self.object_id}[#{([label] + abbreviations).join(', ')}]"
end

#to_s(quantity = nil) ⇒ Object



164
165
166
# File 'lib/uom/unit.rb', line 164

def to_s(quantity=nil)
  (quantity.nil? or quantity == 1) ? label.to_s : label.to_s.pluralize
end