1

I've run into a problem where I have virtual attributes within models that are very similar. In short, they are acting as "converters" for some of the attributes. Here is an example of some of these virtual attributes:

class Setting < ActiveRecord::Base
  validates :overtime, presence: true, numericality: { greater_than_or_equal_to: 0 }
  validates :shift_drop_cut_off, presence: true, numericality: { greater_than_or_equal_to: 0 }

  def overtime_hrs
    return 0 unless self.overtime.present?
    (self.overtime / 3600)
  end

  def overtime_hrs=(overtime_hrs)
    return 0 unless overtime_hrs.present?
    self.overtime = overtime_hrs.to_i * 3600
  end

  def shift_drop_cut_off_hrs
    return 0 unless self.shift_drop_cut_off.present?
    (self.shift_drop_cut_off / 3600)
  end

  def shift_drop_cut_off_hrs=(shift_drop_cut_off_hrs)
    return 0 unless shift_drop_cut_off_hrs.present?
    self.shift_drop_cut_off = shift_drop_cut_off_hrs.to_i * 3600
  end
end

In this case I have two columns named "overtime" and "shift_drop_cutoff". Both of these columns are integers that represent time in seconds. However, I don't want to display these attributes to the user in seconds. Instead, I want to convert them into hours. Hence, this is the purpose of the virtual attributes.

As you can see these virtual attribute getter/setters are nearly identical. Does anyone have tips on how I can refactor this?

3 Answers3

1

Metaprogramming ftw!

module ByHours
  extend ActiveSupport::Concern
  module ClassMethods
    def by_hours(name, base)
      define_method name do
        (send(base) || 0) / 3600
      end
      define_method "#{name}=" do |val|
        send("#{base}=", val * 3600)
      end
    end
  end
end

Then in your Setting class:

class Setting
  by_hours :overtime_hrs, :overtime
  by_hours :shift_drop_cut_off_hrs, :shift_drop_cut_off
end
Slicedpan
  • 4,995
  • 2
  • 18
  • 33
1

You can define class handling time conversation and use it in your model:

class Duration
  attr_reader :hours, :seconds

  def self.from_hours(hours)
    hours ||= 0
    seconds = hours * 3600

    new(seconds)
  end

  def self.from_seconds(seconds)
    seconds ||= 0

    new(seconds)
  end

  def initialize(seconds)
    @seconds = seconds
    @hours = @seconds / 3600
  end
end

And then in your model:

def overtime_hrs
  Duration.from_seconds(self.overtime).hours
end

def overtime_hrs=(overtime_hrs)
  self.overtime = Duration.from_hours(overtime_hrs).seconds
end

def shift_drop_cut_off_hrs
  Duration.from_seconds(self.shift_drop_cut_off).hours
end

def shift_drop_cut_off_hrs=(shift_drop_cut_off_hrs)
  self.overtime = Duration.from_hours(shift_drop_cut_off_hrs).seconds
end
Michał Młoźniak
  • 5,466
  • 2
  • 22
  • 38
0

Try to make some investigation on: method_missing, define_method and send in ruby.

here is a nice tutorial that may help you

bkdir
  • 998
  • 7
  • 12