TopperH
TopperH

Reputation: 2223

DRY way to define multiple virtual attributes in rails

I'm writing an app that acts as a tailor measure form.

The customers model has a lot of attributes that are stored in the database as integers in millimeters. Since this app will be used both in Europe and in the US I'll use virtual attributes for showing the user a inches and a centimeter version of the data.

For example for the customer height I have this in my model:

  def height_in_cm
    height / 10
  end

  def height_in_cm=(height)
    self.height = height.to_f  * 10
  end

  def height_in_in
    height * 0.039370
  end

  def height_in_in=(height)
    self.height = height.to_f / 0.039370
  end

And this in my _form view:

  <% if @customer.measure_unit.eql? "imperial" %>
    <%= f.input :height_in_in %></br>
  <% else %>      
    <% if @customer.measure_unit.eql? "metric" %>
      <%= f.input :height_in_cm %></br>
    <% end %>     
  <% end %>

Since as I said I have many attributes my customer model file is becoming extremely long and very error prone.

Is there a dry way to shorten it up?

Upvotes: 1

Views: 206

Answers (2)

Unixmonkey
Unixmonkey

Reputation: 18784

Encapsulate that logic something like this:

def display_height
  case measure_unit
  when 'imperial' then height_in_in
  when 'metric'   then height_in_cm
  else height_in_in
  end
end

Then your view can just be like this:

<%= @customer.display_height %>

If you are using these same conversion methods in lots of models, extract it out to a module like so:

module HeightConversions

  def height_in_cm
    height / 10
  end

  def height_in_in
    height * 0.039370
  end

  def display_height
    case measure_unit
    when 'imperial' then height_in_in
    when 'metric'   then height_in_cm
    else height_in_in
    end
  end

end

And include it like so in the models where needed:

class Customer
  include HeightConversions
end

EDIT:

Ok, perhaps you need something more like this:

  %w(neck waist arm).each do |name|
    self.class_eval do

      define_method :"#{name}_in_cm" do
        self.send(name) / 10
      end

      define_method :"#{name}_in_cm=" do |n|
        self.send("#{name}=", (n.to_f * 10))
      end

      define_method :"#{name}_in_in" do
        self.send(name) * 0.03
      end

      define_method :"#{name}_in_in=" do |n|
        self.send("#{name}=", (n.to_f / 0.039370))
      end

    end
  end

This is usually called metaprogramming. Here's a nice concise article on different ways to do it in Rails: http://www.trottercashion.com/2011/02/08/rubys-define_method-method_missing-and-instance_eval.html

Upvotes: 1

Archonic
Archonic

Reputation: 5362

I would let either a gem or javascript do the conversion. A gem such as ruby-units might be overkill but provides a lasting solution for any conversion you could want.

For letting javascript do the conversion, here's something you could add to your application.js. You could also use the "baseline" concept to DRY your method in rails.

function convert_units(input, input_unit, output_unit) {
// Used to convert all units to similar intermediary unit. I used meters.
var inches_per_meter = 39.3701;
var sixteenths_per_meter = 629.9216;
var cm_per_meter = 100;
var mm_per_meter = 1000;

var intermediary = 0; //in meters

switch(input_unit)
{
  case: "mm": intermediary = input * mm_per_meter;
    break;
  case: "cm": intermediary = input * cm_per_meter;
    break;
  case: "meter": intermediary = input;
    break;
  case: "inches": intermediary = input * inches_per_meter;
    break;
  case: "sixteenths": intermediary = input * sixteenths_per_meter;
    break;
  default: return "Input unit not correctly accomodated";
}

switch(output_unit)
{
  case: "mm": return intermediary / mm_per_meter;
    break;
  case: "meter": return intermediary;
    break;
  case: "inches": return intermediary / inches_per_meter;
    break;
  case: "sixteenths": return intermediary / sixteenths_per_meter;
    break;
  case: "cm": return intermediary / cm_per_meter;
    break;
  default: return "Output unit not correctly accomodated";
}
}

Then in your views

convert_units(@customer.height, 'inches', 'cm');

Upvotes: 0

Related Questions