Subclassing Float to create Length and Volume classes


#1

Hi all!

I’m trying to implement a Volume and an Area class much the same way as Length is already implemented. However I’m not sure how to solve the inheritance since yiy can’t subclass Float. Does anyone know how Length is currently implemented?

Here’s my current code, designed specifically for consistency with the Length implementation.

class Numeric

  %i{cm feet inch km m mile mm yard}.each do |unit|

    # The <unit>2 methods are used to convert a number, written in any of the
    # supported units, to an Area.
    #
    # Examples
    #
    #   number = 42
    #   area = number.m2 # If we know the area is written in m^2.
    #
    # Returns an Area.
    #
    # Signature
    #
    #   <unit>2
    #
    # unit - cm, feet, inch, km, m, mile, mm or yard.
    define_method("#{unit}2".to_sym) do
      send(unit).send(unit).to_area
    end

    # The <unit>3 methods are used to convert a number, written in any of the
    # supported units, to an Volume.
    #
    # Examples
    #
    #   number = 16
    #   volume = number.m3 # If know the volume is written in m^3.
    #
    # Returns a Volume.
    #
    # Signature
    #
    #   <unit>3
    #
    # unit - cm, feet, inch, km, m, mile, mm or yard.
    define_method("#{unit}3".to_sym) do
      send(unit).send(unit).send(unit).to_volume
    end

    # The to_<unit>2 methods are used to convert an area to a number specified
    # in any of the supported units.
    #
    # Examples
    #
    #   area = 0.5.m2
    #   number = area.to_cm2
    #
    # Returns a Float.
    #
    # Signature
    #
    #   to_<unit>2
    #
    # unit - cm, feet, inch, km, m, mile, mm or yard.
    define_method("to_#{unit}2".to_sym) do
      method = "to_#{unit}".to_sym
      send(method).send(method).to_f
    end

    # The to_<unit>3 methods are used to convert a volume to a number specified
    # in any of the supported units.
    #
    # Examples
    #
    #   volume = 1.cm3
    #   number = volume.to_mm3
    #
    # Returns a Float.
    #
    # Signature
    #
    #   to_<unit>3
    #
    # unit - cm, feet, inch, km, m, mile, mm or yard.
    define_method("to_#{unit}3".to_sym) do
      method = "to_#{unit}".to_sym
      send(method).send(method).send(method).to_f
    end

  end

  # The to_area is used to convert from a number to an area.
  #
  # Number is interpreted as square inches.
  #
  # Examples
  #
  #   number = 5
  #   area = number.to_area
  #
  # Returns Area.
  def to_area
    # TODO: Create an instance of Area somehow, despite it being a subclass of Numeric.
    to_f
  end

  # The to_volume is used to convert from a number to a volume.
  #
  # Number is interpreted as cubic inches.
  #
  # Examples
  #
  #   number = 5
  #   volume = number.to_volume
  #
  # Returns Volume.
  def to_volume
    # TODO: Create an instance of Volume somehow, despite it being a subclass of Numeric.
    to_f
  end

end

# REVIEW: Merge the identical code in these two classes somehow? Maybe mixin a custom module?
class Area < Float

  # The == method is used to see if one area is equal to another area.
  #
  # The equality comparison on Area values uses the default tolerance that SketchUp uses for comparing lengths.
  #
  # Returns Boolean.
  def ==(other)

    # HACK: Rely on Length to access SketchUp's internal precision.
    to_l == other

  end

  # Format an Area as a String using the current units formatting settings for the model.
  # (So if the user's model is set to feet, this method will return a nicely formatted area in square feet.)
  #
  # Examples
  #
  #   # (If model uses cm as unit with 2 decimal precision and system uses comma as decimal separator)
  #   100.mm2.to_s #=> "1,00cm²"
  #
  # Returns String.
  def to_s

    options = Sketchup.active_model.options["UnitsOptions"]

    # REVIEW: Try not to hard code relation between unit CONSTANT and method names.
    unit = %i{inch feet mm cm m}[options["LengthUnit"]]

    # HACK: Rely on the Length class to format the number to the preferred
    # precision and decimal separator, as well as adding the unit string
    # representation (e.g. '"' for inch).
    s = send("to_#{unit}2".to_sym).inch.to_l.to_s

    s << "²" unless options["SuppressUnitsDisplay"]

    s

  end

end

class Volume < Float

  # The == method is used to see if one volume is equal to another volume.
  #
  # The equality comparison on Volume values uses the default tolerance that SketchUp uses for comparing lengths.
  #
  # Returns Boolean.
  def ==(other)

    # HACK: Rely on Length to access SketchUp's internal precision.
    to_l == other

  end

  # Format a Volume as a String using the current units formatting settings for the model.
  # (So if the user's model is set to feet, this method will return a nicely formatted area in cubic feet.)
  #
  # Examples
  #
  #   # (If model uses cm as unit with 2 decimal precision and system uses comma as decimal separator)
  #   1000.mm3.to_s #=> "1,00cm³"
  #
  # Returns String.
  def to_s

    options = Sketchup.active_model.options["UnitsOptions"]

    # REVIEW: Try not to hard code relation between unit CONSTANT and method names.
    unit = %i{inch feet mm cm m}[options["LengthUnit"]]

    # HACK: Rely on the Length class to format the number to the preferred
    # precision and decimal separator, as well as adding the unit string
    # representation (e.g. '"' for inch).
    s = send("to_#{unit}3".to_sym).inch.to_l.to_s

    s << "³" unless options["SuppressUnitsDisplay"]

    s

  end

end

#2

This is “reinventing the wheel.”

Thomas already has an implementation of these classes posted in his GitHub repos.

ADD: In his code, you’d rename the outermost module “Example” to some mixin module name (perhaps within your toplevel author namespace,) … and then mix it into particular plugin sub-modules using include().

This way the class names become available to your plugin sub-modules (or classes) without affecting anyone else’s namespaces.