Class: Shipping::Attribute

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
app/services/shipping/package.rb

Overview

Base class for typed physical measurements (length, mass, etc.).

Direct Known Subclasses

Length, Mass

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(amount, unit) ⇒ Attribute

Returns a new instance of Attribute.

Parameters:

  • amount (Numeric)

    the measured amount

  • unit (Symbol, String)

    the unit of measurement

Raises:

  • (ArgumentError)

    if amount is not numeric



227
228
229
230
231
232
# File 'app/services/shipping/package.rb', line 227

def initialize(amount, unit)
  raise ArgumentError, "amount must be a Numeric" unless amount.is_a?(Numeric)

  @amount = amount
  @unit = unit.to_sym
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args) ⇒ Attribute, Object

Supports arithmetic against other Attributes and delegates to Numeric otherwise.

Parameters:

  • meth (Symbol)

    the missing method name

  • args (Array)

    method arguments

Returns:

  • (Attribute, Object)

    result of the operation



278
279
280
281
282
283
284
285
# File 'app/services/shipping/package.rb', line 278

def method_missing(meth, *args)
  if args.size == 1 && instance_of?((other = args.first).class)
    other_amount_in_self_units = self.class.convert(other.amount, other.unit, unit)
    self.class.new(amount.send(meth, other_amount_in_self_units), unit)
  else
    amount.send(meth, *args)
  end
end

Instance Attribute Details

#amountObject (readonly)

Returns the value of attribute amount.



222
223
224
# File 'app/services/shipping/package.rb', line 222

def amount
  @amount
end

#unitObject (readonly)

Returns the value of attribute unit.



222
223
224
# File 'app/services/shipping/package.rb', line 222

def unit
  @unit
end

Class Method Details

.add_conversion(multiple_unit, other_unit, multiple) ⇒ void

This method returns an undefined value.

Stores a bidirectional conversion factor between two units.

Parameters:

  • multiple_unit (Symbol)

    the derived unit

  • other_unit (Symbol)

    the base unit

  • multiple (Numeric)

    how many base units equal one derived unit



448
449
450
451
452
453
# File 'app/services/shipping/package.rb', line 448

def self.add_conversion(multiple_unit, other_unit, multiple)
  conversions[multiple_unit] ||= {}
  conversions[multiple_unit][other_unit] = multiple
  conversions[other_unit] ||= {}
  conversions[other_unit][multiple_unit] = (1.0 / multiple)
end

.add_conversion_method_for(sym, _options = {}) ⇒ void

This method returns an undefined value.

Defines instance conversion methods for the given unit.

Parameters:

  • sym (Symbol)

    the unit to define conversion methods for

  • _options (Hash) (defaults to: {})

    unused options (reserved for add_methods_for interface)



486
487
488
489
490
491
492
493
494
495
496
# File 'app/services/shipping/package.rb', line 486

def self.add_conversion_method_for(sym, _options = {})
  unit_name = sym.to_s
  class_eval do
    define_method("to_#{unit_name}") do
      return self if unit_name == unit.to_s

      self.class.new(self.class.convert(amount, unit, unit_name), unit_name)
    end
    alias_method("in_#{unit_name}", "to_#{unit_name}")
  end
end

.add_methods_for(sym, options = {}) ⇒ void

This method returns an undefined value.

Parameters:

  • sym (Symbol)

    unit name to define conversion/numeric methods for

  • options (Hash) (defaults to: {})

    method generation options

Options Hash (options):

  • add_numeric_methods (Boolean)

    whether to add the Numeric helper methods (defaults to the class-level setting)



471
472
473
474
475
476
477
478
479
# File 'app/services/shipping/package.rb', line 471

def self.add_methods_for(sym, options = {})
  add_conversion_method_for(sym, options)
  add_numeric_method = if options.key?(:add_numeric_methods)
                         options[:add_numeric_methods]
                       else
                         add_numeric_methods
                       end
  add_numeric_method_for(sym.to_s, options) if add_numeric_method
end

.add_numeric_method_for(unit_name, _options = {}) ⇒ void

This method returns an undefined value.

Defines a Numeric helper method that builds an Attribute of this unit.

Parameters:

  • unit_name (Symbol, String)

    the unit to expose on Numeric

  • _options (Hash) (defaults to: {})

    unused options (reserved for add_methods_for interface)

Raises:

  • (ArgumentError)

    if the unit is not registered



504
505
506
507
508
509
510
511
512
513
514
# File 'app/services/shipping/package.rb', line 504

def self.add_numeric_method_for(unit_name, _options = {})
  unit_name = unit_name.to_sym
  raise ArgumentError, "#{unit_name.inspect} is not a unit in #{name}" unless units.include?(unit_name)

  klass = self
  Numeric.class_eval do
    define_method(unit_name) do
      klass.new(self, unit_name.to_sym)
    end
  end
end

.add_numeric_methods?Boolean

Returns whether numeric helper methods should be generated.

Returns:

  • (Boolean)

    whether numeric helper methods should be generated



347
348
349
# File 'app/services/shipping/package.rb', line 347

def self.add_numeric_methods?
  add_numeric_methods
end

.add_to_system(unit_sym) ⇒ Symbol?

Associates a unit with the currently active measurement system.

Parameters:

  • unit_sym (Symbol)

    the unit to register

Returns:

  • (Symbol, nil)

    the system the unit was added to, or nil if no system is active



397
398
399
400
401
402
403
404
405
# File 'app/services/shipping/package.rb', line 397

def self.add_to_system(unit_sym)
  return unless current_system

  units_to_systems[unit_sym] ||= begin
    sys_ary = systems_to_units[current_system] ||= []
    sys_ary << unit_sym
    current_system
  end
end

.conversion_rate(from, to) ⇒ Float?

Returns the conversion factor from one unit to another.

Parameters:

  • from (Symbol)

    the source unit

  • to (Symbol)

    the target unit

Returns:

  • (Float, nil)

    the conversion rate, or nil when either unit is unknown

Raises:

  • (StandardError)

    when no conversion path exists between the units



293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'app/services/shipping/package.rb', line 293

def self.conversion_rate(from, to)
  return nil unless conversions[from] && conversions[to]

  # rubocop:disable Lint/NoReturnInBeginEndBlocks
  conversions[from][to] ||=
    (1.0 / conversions[to][from] if conversions[to][from]) || begin
      shared_conversions = conversions[from].keys & conversions[to].keys
      if shared_conversions.any?
        primitive = shared_conversions.first
        conversions[from][primitive] * (1.0 / conversions[to][primitive])
      else
        conversions[from].each do |conversion_unit, multiple|
          return multiple * conversion_rate(conversion) * (1.0 / conversions[to][conversion_unit]) if conversions[to].include?(conversion_unit)
        end
        from_primitive = (conversions[from].keys & primitives).first
        to_primitive = (conversions[to].keys & primitives).first
        if (from_primitive_to_primitive_multiple = conversion_rate(from_primitive, to_primitive))
          return conversions[from][from_primitive] * from_primitive_to_primitive_multiple * (1.0 / conversions[to][to_primitive])
        end

        raise StandardError, "No conversion path from #{from} to #{to}"
      end
    end
  # rubocop:enable Lint/NoReturnInBeginEndBlocks
end

.convert(amount, from, to) ⇒ Numeric

Converts an amount from one unit to another.

Parameters:

  • amount (Numeric)

    the amount to convert

  • from (Symbol)

    the source unit

  • to (Symbol)

    the target unit

Returns:

  • (Numeric)

    the converted amount



461
462
463
464
465
# File 'app/services/shipping/package.rb', line 461

def self.convert(amount, from, to)
  from = from.to_sym
  to = to.to_sym
  amount * conversion_rate(from, to)
end

.non_primitivesArray<Symbol>

Returns the non-primitive (derived) units for this attribute class.

Returns:

  • (Array<Symbol>)

    the non-primitive (derived) units for this attribute class



337
338
339
# File 'app/services/shipping/package.rb', line 337

def self.non_primitives
  conversions.keys
end

.numeric_methods(*args) ⇒ Array<Symbol>

Enables numeric helper methods for the given units.

Parameters:

  • args (Array<Symbol, String>)

    units to expose on Numeric

Returns:

  • (Array<Symbol>)

    the unit names that were registered



355
356
357
358
359
# File 'app/services/shipping/package.rb', line 355

def self.numeric_methods(*args)
  args.each do |arg|
    add_numeric_method_for(arg.to_sym)
  end
end

.one(sym, options = {}) ⇒ void

This method returns an undefined value.

Parameters:

  • sym (Symbol)

    singular unit name

  • options (Hash) (defaults to: {})

    unit registration options (see add_methods_for)

Options Hash (options):

  • plural (String, Symbol)

    plural unit name (defaults to the pluralized singular)

  • is (Measured::Measurable)

    the base unit this unit is one of



412
413
414
415
416
417
# File 'app/services/shipping/package.rb', line 412

def self.one(sym, options = {})
  unit_sym = (options[:plural] || sym.to_s.pluralize).to_sym
  add_to_system(unit_sym)
  register_unit(unit_sym, options[:is].unit, options[:is].amount)
  add_methods_for(unit_sym, options)
end

.primitive(sym, options = {}) ⇒ void

This method returns an undefined value.

Parameters:

  • sym (Symbol)

    singular unit name

  • options (Hash) (defaults to: {})

    unit registration options (see add_methods_for)

Options Hash (options):

  • plural (String, Symbol)

    plural unit name (defaults to the pluralized singular)



386
387
388
389
390
391
# File 'app/services/shipping/package.rb', line 386

def self.primitive(sym, options = {})
  unit_sym = (options[:plural] || sym.to_s.pluralize).to_sym
  primitives << unit_sym
  add_to_system(unit_sym)
  add_methods_for(unit_sym, options)
end

.primitivesArray<Symbol>

Returns the base (primitive) units for this attribute class.

Returns:

  • (Array<Symbol>)

    the base (primitive) units for this attribute class



332
333
334
# File 'app/services/shipping/package.rb', line 332

def self.primitives
  read_inheritable_attribute(:primitives).dup
end

.register_unit(multiple_unit, other_unit, multiple) ⇒ void

This method returns an undefined value.

Records a conversion relationship between a derived unit and its base unit.

Parameters:

  • multiple_unit (Symbol)

    the derived unit

  • other_unit (Symbol)

    the base unit

  • multiple (Numeric)

    how many base units equal one derived unit



425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
# File 'app/services/shipping/package.rb', line 425

def self.register_unit(multiple_unit, other_unit, multiple)
  multiple_unit = multiple_unit.to_sym
  other_unit = other_unit.to_sym
  conversions[multiple_unit] ||= {}
  conversions[other_unit] ||= {}

  if primitives.include?(multiple_unit) || primitives.include?(other_unit)
    add_conversion(multiple_unit, other_unit, multiple)
  else
    [multiple_unit, other_unit].each do |this_unit|
      conversions[this_unit].each do |this_other_unit, this_multiple|
        add_conversion(multiple_unit, this_other_unit, multiple * this_multiple) if primitives.include?(this_other_unit)
      end
    end
  end
end

.system(system_name) { ... } ⇒ Object

Sets the current measurement system while registering units.

Parameters:

  • system_name (Symbol, String)

    the measurement system to activate

Yields:

  • block run with the system activated

Returns:

  • (Object)

    the block's return value



375
376
377
378
379
380
# File 'app/services/shipping/package.rb', line 375

def self.system(system_name, &)
  old_system = current_system
  self.current_system = system_name.to_sym
  yield
  self.current_system = old_system
end

.systemsArray<Symbol>

Returns the registered measurement systems.

Returns:

  • (Array<Symbol>)

    the registered measurement systems



342
343
344
# File 'app/services/shipping/package.rb', line 342

def self.systems
  systems_to_units.keys
end

.units(system = nil) ⇒ Array<Symbol>

Returns all registered units, optionally filtered by measurement system.

Parameters:

  • system (Symbol, String, nil) (defaults to: nil)

    the measurement system to filter by

Returns:

  • (Array<Symbol>)

    the registered units



323
324
325
326
327
328
329
# File 'app/services/shipping/package.rb', line 323

def self.units(system = nil)
  if system
    systems_to_units[system.to_sym].dup
  else
    read_inheritable_attribute(:primitives) | conversions.keys
  end
end

Instance Method Details

#<=>(other) ⇒ Integer

Returns -1, 0, or 1 based on comparison.

Parameters:

  • other (Attribute, Object)

    the object to compare

Returns:

  • (Integer)

    -1, 0, or 1 based on comparison



260
261
262
263
264
265
266
# File 'app/services/shipping/package.rb', line 260

def <=>(other)
  if instance_of?(other.class)
    self.class.convert(amount, unit, other.unit) <=> other.amount
  else
    amount <=> other
  end
end

#==(other) ⇒ Boolean

Returns whether the values are equal after unit conversion.

Parameters:

  • other (Attribute, Object)

    the object to compare

Returns:

  • (Boolean)

    whether the values are equal after unit conversion



246
247
248
249
250
# File 'app/services/shipping/package.rb', line 246

def ==(other)
  (BigDecimal(amount&.to_s) == BigDecimal(other&.amount&.to_s) && unit == other&.unit) || BigDecimal(self.class&.convert(amount, unit, other&.unit)&.to_s) == BigDecimal(other&.amount&.to_s)
rescue NoMethodError
  amount == other
end

#eql?(other) ⇒ Boolean

Returns whether class, amount, and unit are identical.

Parameters:

  • other (Attribute)

    the object to compare

Returns:

  • (Boolean)

    whether class, amount, and unit are identical



254
255
256
# File 'app/services/shipping/package.rb', line 254

def eql?(other)
  self.class == other.class && BigDecimal(amount.to_s) == BigDecimal(other.amount.to_s) && unit == other.unit
end

#inspectString

Returns developer-facing inspection string.

Returns:

  • (String)

    developer-facing inspection string



240
241
242
# File 'app/services/shipping/package.rb', line 240

def inspect
  "#<#{self.class.name}: #{amount} #{unit}>"
end

#systemSymbol?

Returns the measurement system this unit belongs to.

Returns:

  • (Symbol, nil)

    the measurement system this unit belongs to



269
270
271
# File 'app/services/shipping/package.rb', line 269

def system
  self.class.units_to_systems[unit]
end

#to_sString

Returns human-readable amount and unit.

Returns:

  • (String)

    human-readable amount and unit



235
236
237
# File 'app/services/shipping/package.rb', line 235

def to_s
  "#{amount} #{unit}"
end