Skip to content

Instantly share code, notes, and snippets.

@krisleech
Last active September 11, 2019 15:34
Show Gist options
  • Select an option

  • Save krisleech/cd5a148f48ef5f30e49dae443d79c056 to your computer and use it in GitHub Desktop.

Select an option

Save krisleech/cd5a148f48ef5f30e49dae443d79c056 to your computer and use it in GitHub Desktop.
Integers with units in Ruby

Often we have Integer's but don't have a clue what the unit is unless we encode it in a varible name. How can we prevent adding two numbers which are of different or incompatible units...

class IntegerWithUnit < SimpleDelegator
  attr_reader :unit
  
  def initialize(integer, unit)   
    @unit = unit.to_sym.freeze
    super(integer)
  end 
  
  def unit?(unit)
    @unit == unit.to_sym
  end
  
  def to_int
    self
  end
  
  def to_s
    super + " " + unit.to_s
  end
  
  def to_str
    to_s
  end
  
  def inspect
    to_s
  end
end
num = IntegerWithUnit.new(1, :byte)
# => 1 byte

"Remaining: #{num}"
# => "Remaining: 1 byte"

num + 2
# => 3

(num + 2).class
# => Integer
# oops, we need to return an IntegerWithUnit

IntegerWithUnit.new(1, :byte) == IntegerWithUnit.new(1, :mile)
# => true
# oops, no it doesn't...
class Byte < IntegerWithUnit
  def initialize(number)
    super(number, :byte)
  end
end

We can then allow certain types to be involved in arithmetic, e.g.

n1 = Byte.new(1)
n2 = Megabyte.new(2)

n1 + n2 
# => 1000001 byte

Do we need to add pluralization, so:

Byte.new(2)
# => 2 bytes

This would need an inflection library, or it is an optional arguments, e.g.:

IntegerWithUnit.new(2, [:byte, :bytes])

What about Floats?

FloatWithUnit.new(2.0, :miles)
# => 2.0 mile

IntegerWithUnit.new(2, :bytes).to_f 
#=> 2.0 byte

And sugar:

Byte.new(2).to_megabyte
Mile.new(2).to_kilometers

Where do we store the conversion ratios between units?

In a chosen base unit, e.g. Byte has all conversions to Megabyte, Gigabyte etc. What about going from say kilometers to miles...

Maybe we have to convert to a base unit, do addition, and then back to the target unit. Each class has a conversion to the base unit, so Byte::ToByte = 1, Kilobyte::ToByte = 1000 etc.

If this where to be gemified then we would want to wrap all class in a module instead of poluting the root namespace. In which case we could have:

module Unit
  class Integer < SimpleDelegator
    attr_reader :unit
  
    def initialize(integer, unit)   
      @unit = unit.to_sym.freeze
      super(integer)
    end 
    
    # ...
  end
  
  class Byte < Integer
  end
end

But then does a Unit::Byte inherit from Unit::Integer or Unit::Float?

Would we need a base class, ValueWithUnit, which picks the correct class, Integer versus Float... Is it even needed since we are delegating to the passed in value...

IntegerWithUnit.new(1.2, :byte)
# => 1.2 byte

IntegerWithUnit.new({}, :byte)
# => {} byte

So what about:

module Unit
  class Object < SimpleDelegator
    attr_reader :unit
  
    def initialize(obj, unit)   
      @unit = unit.to_sym.freeze
      super(obj)
    end 
    
    # ...
  end
  
  class Byte < Object
    def initialize(number)   
      super(number, :byte)
    end
  end
end

There are so many different units they might need to be in seperate gems, e.g. bitcoin denominations.

From here it is proberbly best to write a README showing the desired API and then copy the examples to form the basis of the feature specs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment