Programming ≈ Fun

Written by Krešimir Bojčić

Let It Be

Lately pet peeve of mine is how to make small methods look more elegant. One idea was to use “indentation” for implicit end of small methods. Big problem with this is that this is only a wish. I was forced to just cram the method to one line and curse the destiny. For a while I was torn. I loved cramming small methods and hated the impact on readability it had.

Couple of days ago it came to my attention that you can use define_method to get a job done. If we start with this:

def stupid?
  @pretty ? "NO" : "YES"
end

It can be written even uglier like this (bear with me):

define_method :stupid? do
  @pretty ? "NO" : "YES"
end

BUT if we create alias for define_method (called something short like ‘let’) and use {} instead of “do end” we end up with this:

let(:stupid?) { @pretty ? "NO" : "YES" }
I find this great. I was using define_method before for “meta programming” but it never occurred to me that this is possible use.

This:

def numbered?(line)
  line_numbering_style  == :all_lines || significant?(line)
end

def significant?(line)
  line_numbering_style == :significant_lines && !blank?(line)
end

can become this:

let(:numbered?) { |line| line_numbering_style == :all_lines || significant?(line) }
let(:significant?) { |line| line_numbering_style == :significant_lines && !blank(line) }

I did a bit of usability testing yesterday( read - refactoring of a medium sized class to the death) and I really like the results. Class lines went from 145 to 78 while readability was increased, and I even added lines is some places where I felt that they increase readability.

(before.rb) download
#endcoding : utf-8
class Kvar < ActiveRecord::Base

  searchable do
   text :napomena, :naselje_naziv, :ulica_naziv, :ulica_sluzbeni_naziv, :izvodjac_naziv,
     :otklonio_naziv, :datum_prijave, :mjesec_prijave, :razlog_prijave_opis, :uzrok_kvara_opis, :status, :opis_kvara, :godina_prijave, :gradska_cetvrt_naziv, :oznaka, :kategorija_prijave_naziv, :broj_naljepnice
  end

  solr_info 'naselje#naziv', 'ulica#naziv' ,'ulica#sluzbeni_naziv' ,'izvodjac#naziv' ,'gradska_cetvrt#naziv' ,'otklonio#naziv' ,'razlog_prijave#opis' ,'uzrok_kvara#opis'
  default_value_for :vrijeme_prijave  do
    RadnoVrijeme.zavrseno? ? RadnoVrijeme.pocetak_sutra : DateTime.now
  end

  has_many :obavljeni_rads
  has_many :zamijenjeni_dijelovi, :class_name => 'ZamijenjeniDio'
  belongs_to :rasvjetno_mjesto
  belongs_to :naselje
  belongs_to :razlog_prijave
  belongs_to :ulica
  belongs_to :gradska_cetvrt
  belongs_to :izvodjac
  belongs_to :otklonio, :class_name => "NositeljEkipe"
  belongs_to :uzrok_kvara
  validate :mora_biti_unesena_lokacija
  validates_presence_of :vrijeme_prijave, :gradska_cetvrt_id
  validate :mora_biti_tko_je_otklonio_kada_je_kvar_otklonjen
  validate :mora_biti_broj_naljepnice_kada_je_kvar_otklonjen
  validate :datum_otklona_mora_biti_nakon_datuma_prijave
  validate :oznaka_rasvjetnog_mjesta_mora_biti_kod_otklona_osim_ako_nije_numerirana
  validate :mora_postojati_dnevnik_rada_za_unos_u_proslosti_preko_tjedan_dana
  validates_uniqueness_of :broj_naljepnice, :allow_nil => true

  scope :aktivni,  where('kvarovi.datum_storna is NULL')
  scope :gradska_cetvrt, lambda { |gradska_cetvrt_id| where('gradska_cetvrt_id = ? ', gradska_cetvrt_id ) }

  @povratna_prijava = false
  attr_accessor :povratna_prijava

  def mora_postojati_dnevnik_rada_za_unos_u_proslosti_preko_tjedan_dana
    if (vrijeme_prijave + 7.days < DateTime.now) || (vrijeme_prijave - 7.days > DateTime.now)
      if !RadniNalog.postoji_dnevnik_rada(datum_prijave)
          errors.add(:vrijeme_prijave, "nije ispravno, ne postoji dnevnik rada otvoren za taj dan")
      end
    end
  end

  def oznaka_rasvjetnog_mjesta_mora_biti_kod_otklona_osim_ako_nije_numerirana
    if is_otklonjen == true  && !povratna_prijava
     if  oznaka_rasvjetnog_mjesta.blank?  && !rasvjetno_mjesto_nije_numerirano
      errors.add :oznaka_rasvjetnog_mjesta, "mora biti odredjena kad je kvar otklonjen osim kada rasvjetno mjesto nije numerirano"
     end
    end
  end

  def mora_biti_tko_je_otklonio_kada_je_kvar_otklonjen
    if is_otklonjen == true && otklonio.nil?
        errors.add :otklonio_id, "mora biti odredjen nositelj ekipe koja je otklonila kvar"
    end
  end

  def mora_biti_broj_naljepnice_kada_je_kvar_otklonjen
    if is_otklonjen == true && !povratna_prijava
        errors.add(:broj_naljepnice, "mora biti upisan" ) if broj_naljepnice.nil? || broj_naljepnice.blank?
    end
  end

  def datum_otklona_mora_biti_nakon_datuma_prijave
    if is_otklonjen ==  true && !datum_otklona.nil? && (vrijeme_prijave.to_date > datum_otklona)
      errors.add(:datum_otklona, "mora biti nakon datuma prijave")
    end
  end


  def lokacija
    return string.blank if ulica.nil?
    ret = "#{ulica.lokacija} #{kucni_broj}"
    return "#{ret} - #{gradska_cetvrt.naziv}" unless gradska_cetvrt.nil?
    return ret
  end

  def lokacija_bez_gradske_cetvrti
    return string.blank if ulica.nil?
    ret = "#{ulica.lokacija} #{kucni_broj}"
  end

  def oznaka
    "RN #{oznaka_short}"
  end

  def oznaka_short
    result = "#{broj_dnevnika}/#{redni_broj}"
    result = result + "-#{redni_broj_unutar_naloga}" if !redni_broj_unutar_naloga.blank?
    return result
  end


  def mora_biti_unesena_lokacija
    errors[:base] <<  I18n.translate(:lokacija_mora_biti_popunjena) if ulica_id.nil?
  end

  def datum_prijave
    vrijeme_prijave.strftime("%d.%m.%Y")
  end

  def datum_otklona_ispis
    datum_otklona.strftime("%d.%m.%Y") unless datum_otklona.nil?
  end

  def mjesec_prijave
    vrijeme_prijave.strftime("%m.%Y")
  end

  def godina_prijave
    vrijeme_prijave.strftime("G%Y")
  end

  def status
    if datum_storna.nil?
      is_otklonjen == true ? 'otklonjen' : 'neotklonjen'
    else
      'storniran'
    end
  end

  def sekvenca_za_redni_broj_unutar_radnog_naloga
    "RN#{broj_dnevnika}/#{redni_broj}-#{vrijeme_prijave.strftime("%Y")}"
  end

  def self.smjene
    Settings.smjene
  end

  def self.kategorije_prijave
   Settings.kategorije_prijave
  end

  def kategorija_prijave_naziv
    Kvar.kategorije_prijave[kategorija_prijave]
  end

  def self.total_on(month)
    self.aktivni.count(:conditions => ["month(vrijeme_prijave) = ? and year(vrijeme_prijave) = '#{DateTime.now.year}'", month])
  end

end
(after.rb) download
#endcoding : utf-8
require 'kernel'
class Kvar < ActiveRecord::Base
  searchable do
   text :napomena, :naselje_naziv, :ulica_naziv, :ulica_sluzbeni_naziv,
        :izvodjac_naziv, :otklonio_naziv, :datum_prijave, :mjesec_prijave,
        :razlog_prijave_opis, :uzrok_kvara_opis, :status, :opis_kvara,
        :godina_prijave, :gradska_cetvrt_naziv, :oznaka, :kategorija_prijave_naziv, :broj_naljepnice
  end
  solr_info 'naselje#naziv', 'ulica#naziv', 'ulica#sluzbeni_naziv', 'izvodjac#naziv', 'gradska_cetvrt#naziv', 'otklonio#naziv',
            'razlog_prijave#opis' ,'uzrok_kvara#opis'

  default_value_for :vrijeme_prijave,  RadnoVrijeme.zavrseno? ? RadnoVrijeme.pocetak_sutra : DateTime.now

  has_many :obavljeni_rads
  has_many :zamijenjeni_dijelovi, :class_name => 'ZamijenjeniDio'
  belongs_to :rasvjetno_mjesto
  belongs_to :naselje
  belongs_to :razlog_prijave
  belongs_to :ulica
  belongs_to :gradska_cetvrt
  belongs_to :izvodjac
  belongs_to :otklonio, :class_name => "NositeljEkipe"
  belongs_to :uzrok_kvara
  validate :mora_biti_unesena_lokacija
  validate :mora_biti_tko_je_otklonio_kada_je_kvar_otklonjen
  validate :mora_biti_broj_naljepnice_kada_je_kvar_otklonjen
  validate :datum_otklona_mora_biti_nakon_datuma_prijave
  validate :oznaka_rasvjetnog_mjesta_mora_biti_kod_otklona_osim_ako_nije_numerirana
  validate :mora_postojati_dnevnik_rada_za_unos_u_proslosti_preko_tjedan_dana
  validates_uniqueness_of :broj_naljepnice, :allow_nil => true
  validates_presence_of :vrijeme_prijave, :gradska_cetvrt_id

  scope :aktivni,  where('kvarovi.datum_storna is NULL')
  scope :gradska_cetvrt, lambda{ |gradska_cetvrt_id| where('gradska_cetvrt_id = ? ', gradska_cetvrt_id ) }
  attr_accessor :povratna_prijava

  def mora_postojati_dnevnik_rada_za_unos_u_proslosti_preko_tjedan_dana
    errors.add(:vrijeme_prijave, "nije ispravno, ne postoji dnevnik rada otvoren za taj dan") if unutar_perioda? &&  !RadniNalog.postoji_dnevnik_rada(datum_prijave)
  end

  def oznaka_rasvjetnog_mjesta_mora_biti_kod_otklona_osim_ako_nije_numerirana
    errors.add :oznaka_rasvjetnog_mjesta, "mora biti odredjena kad je kvar otklonjen osim kada rasvjetno mjesto nije numerirano" if treba_oznaka? && otklonjena_normalna_prijava?
  end

  def mora_biti_tko_je_otklonio_kada_je_kvar_otklonjen
    errors.add :otklonio_id, "mora biti odredjen nositelj ekipe koja je otklonila kvar" if is_otklonjen == true && otklonio.nil?
  end

  def datum_otklona_mora_biti_nakon_datuma_prijave
    errors.add(:datum_otklona, "mora biti nakon datuma prijave") if is_otklonjen ==  true && !datum_otklona.nil? && (vrijeme_prijave.to_date > datum_otklona)
  end

  let(:mora_biti_broj_naljepnice_kada_je_kvar_otklonjen) { errors.add(:broj_naljepnice, "mora biti upisan" ) if otklonjena_normalna_prijava? && !postoji_broj_naljepnice? }
  let(:mora_biti_unesena_lokacija) { errors[:base] <<  I18n.translate(:lokacija_mora_biti_popunjena) if ulica_id.nil? }

  let(:unutar_perioda?) { (vrijeme_prijave + 7.days < DateTime.now) || (vrijeme_prijave - 7.days > DateTime.now) }
  let(:postoji_broj_naljepnice?) { !broj_naljepnice.nil? && !broj_naljepnice.blank? }
  let(:otklonjena_normalna_prijava?) { is_otklonjen == true && !povratna_prijava }
  let(:treba_oznaka?) { oznaka_rasvjetnog_mjesta.blank?  && !rasvjetno_mjesto_nije_numerirano }
  let(:datum_prijave) { vrijeme_prijave.strftime("%d.%m.%Y") }
  let(:datum_otklona_ispis)  { datum_otklona.strftime("%d.%m.%Y") unless datum_otklona.nil? }
  let(:mjesec_prijave) { vrijeme_prijave.strftime("%m.%Y") }
  let(:godina_prijave) { vrijeme_prijave.strftime("G%Y") }
  let(:sekvenca_za_redni_broj_unutar_radnog_naloga)  { "RN#{broj_dnevnika}/#{redni_broj}-#{vrijeme_prijave.strftime("%Y")}" }
  let(:oznaka) { "RN #{oznaka_short}" }
  let(:redni_broj_dnevnika) { "#{broj_dnevnika}/#{redni_broj}"  }
  let(:oznaka_short) {  redni_broj_dnevnika +  (redni_broj_unutar_naloga.blank? ? string.blank :  "-#{redni_broj_unutar_naloga}") }
  let(:lokacija) { lokacija_bez_gradske_cetvrti + (gradska_cetvrt.nil? ? string.blank : "#{gradska_cetvrt.naziv}") }
  let(:lokacija_bez_gradske_cetvrti) { ulica.nil? ? string.blank : "#{ulica.lokacija} #{kucni_broj}" }
  let(:otklonjen_opis) { is_otklonjen == true ? 'otklonjen' : 'neotklonjen' }
  let(:status) { datum_storna.nil? ? otklonjen_opis : 'storniran' }
  let(:kategorija_prijave_naziv) { Kvar.kategorije_prijave[kategorija_prijave] }

  let_self(:smjene) {  Settings.smjene }
  let_self(:kategorije_prijave) { Settings.kategorije_prijave }
  let_self(:total_on) { |month| Kvar.aktivni.count(:conditions => ["month(vrijeme_prijave) = ? and year(vrijeme_prijave) = '#{DateTime.now.year}'", month]) }
end

This is my modest Kernel patch in guerrilla_patch, @garybernhardt did it “properly” in his Cls without monkey patching.

module Kernel
   def let(name, &block)
     define_method(name, &block)
   end

   def let_self(name, &block)
     define_singleton_method(name, &block)
   end
end