воскресенье, 29 марта 2015 г.

Примеры упрощения кода

Упрощать код очень важно, т.к. это увеличивает его читаемость. Одну и ту же задачу можно сделать одной, так и десятью строчками кода. Чем код компактнее, тем его легче поддерживать. Разумеется, не нужно доходить до фанатизма, пытаясь уместить все в одну строчку, т.к. это может привести к ухудшению его читаемости.

  1. lots.map { |lot| lot.start_date }.compact.min
    
    # Упростили
    lots.map(&:start_date).compact.min
    
  2. if age.present?
      I18n.t('firm.regular')
    else
      I18n.t('firm.new')
    end
    
    # Упростили:
    I18n.t("firm.#{age.present? ? :regular : :new}")
    
  3. if company.type_id == nil || company.type_id == CompanyType.online.id
      return false
    end
    
    # Упростили
    return false if [nil, CompanyType.online.id].include? company.type_id
    
  4. if companies.select { |company| company.store_type == company.online_type }.empty?
      return false
    end
    
    # Упростили
    return false if companies.none? { |company| company.store_type == company.online_type }
    

пятница, 27 марта 2015 г.

Rails: undefined method `to_datetime' for nil:NilClass

В Rails может возникнуть ошибка undefined method `to_datetime' for nil:NilClass при выполнении следующего кода:
filtered_products = products.select do |product|
  product.start_date && product.end_date &&
  (product.start_date <= start_date_ago && start_date_ago <= product.end_date) ||
  (start_date_ago <= product.start_date && product.start_date < first_start_date)
end

Здесь берутся товары и отбираются те из них, что попадают в нужный диапазон дат.
product.start_date - дата начала периода публикации товара
product.end_date - дата конца периода публикации товара
В начале мы проверяем, что у товара есть эти параметры, а уже после этого сравниваем их с датами first_start_date и start_date_ago, которые определены где-то выше по коду. Это позволяет нам избежать ошибки:
NoMethodError: undefined method `<' for nil:NilClass.

Почему же в приведенном выше коде возникает ошибка undefined method `to_datetime' for nil:NilClass ?

Это связано с тем, что мы по невнимательности нарушили расстановку скобок. Сначала нам нужно убедиться в наличии параметров product.start_date и product.end_date, а уже потом выполнять остальные сравнения:
filtered_products = products.select do |product|
  product.start_date && product.end_date &&
  ((product.start_date <= start_date_ago && start_date_ago <= product.end_date) ||
  (start_date_ago <= product.start_date && product.start_date < first_start_date))
end




вторник, 10 марта 2015 г.

ArgumentError: comparison of String with Date failed

Что не так со следующим кодом?
# здесь custom_lot_attributes - колонка типа 'hstore' в таблице lots
start_date = lot.custom_lot_attributes['start_date']
if start_date && start_date < Date.today
  # ...
end

При выполнии этого участка кода возникает ошибка:
ArgumentError: comparison of String with Date failed
Это связано с тем, что мы пытаемся сравнить дату со строкой. Из hstore мы получаем строку, поэтому в переменной start_date содержится дата в виде строки, к примеру "2015-03-10".
Для устранения ошибки нужно преобразовать строку в дату. Для этого в Rails есть метод to_date.
Теперь ошибки возникать не будет:

start_date = lot.custom_lot_attributes['start_date'].to_date
if start_date && start_date < Date.today
  # ...
end

А сможете ли вы найти ошибку в этом коде?
start_date = lot.custom_lot_attributes['start_date'].to_date
if start_date && start_date < Date.today
  lot.custom_lot_attributes['start_date'] = Date.today
  lot.custom_lot_attributes['end_date'] = 
    lot.custom_lot_attributes['end_date'] +
    (Date.today - lot.custom_lot_attributes['start_date'])
end

Данный код будет вызывать ошибку:
TypeError: can't convert Rational into String
Причина аналогична предыдущей, мы забыли преобразовать строковое значение, полученное из hstore, в дату.
Вот как должно быть:
start_date = lot.custom_lot_attributes['start_date'].to_date
if start_date && start_date < Date.today
  lot.custom_lot_attributes['start_date'] = Date.today
  lot.custom_lot_attributes['end_date'] = 
    lot.custom_lot_attributes['end_date'].to_date +
    (Date.today - lot.custom_lot_attributes['start_date'].to_date)
end


вторник, 10 февраля 2015 г.

NoMethodError: undefined method `[]' for nil:NilClass

Данная ошибка может возникать в Ruby при попытке обратиться к несуществующему свойству объекта, который почему-то стал nil.
Рассмотрим пример, запустив консоль Rials (rails c):
hash = {a: 1, b: {c: 2}}
=> {:a=>1, :b=>{:c=>2}}

# Получаем элемент с индексом :a
hash[:a]
=> 1

# Получаем элемент с индексом :b
hash[:b]
=> {:c=>2}

# Получаем элемент с индексом :b, а для него - с индексом :c
hash[:b][:c]
=> 2

# Пытаемся получить эл-нт с индексом :zzz, а для него - с индексом :c
hash[:zzz][:c]
NoMethodError: undefined method `[]' for nil:NilClass

Как видно, при попытке получить элемент с индексом :zzz мы получаем nil, и уже для nil мы пытаемся найти элемент с индексом :c, что приводит к ошибке, ведь у nil нет метода [].

Решение:

hash[:zzz].try(:[], :c)

Обратите внимание, что этот способ работает только в Rails. В чистом Ruby он не поможет, там можно использовать трюк с if:
hash[:zzz][:c] if hash[:zzz]
 

пятница, 30 января 2015 г.

Шаблонизатор haml вместо erb

В веб-фреймворке Ruby on Rails есть отличная возможность подключить новый шаблонизатор haml вместо имеющегося стандартного erb-шаблонизатора. Для этого достаточно в Gemfile проекта добавить строчку:  
gem "haml"

Так выглядит erb-шаблон для отображения товаров - index.html.erb:
<h1>Listing products</h1>
<% if notice %>
  <p id="notice"><%= notice %></p>
<% end %>

<table class="products">
<% @products.each do |product| %>
  <tr class="<%= cycle('list_line_odd', 'list_line_even') %>">
    <td>
      <%= image_tag(product.image_url, class: 'list_image') %>
    </td>
    <td class="list_description">
      <dl>
        <dt><%= product.title %></dt>
        <dd><%= truncate(strip_tags(product.description),
               length: 80) %></dd>
      </dl>
    </td>
    <td class="list_actions">
      <%= link_to 'Показать', product %><br/>
      <%= link_to 'Ред.', edit_product_path(product) %><br/>
      <%= link_to 'Удалить', product, method: :delete,
                  data: { confirm: I18n.t('app.delete_confirm') } %>
    </td>
  </tr>
<% end %>
</table>
<br />
<%= link_to 'New product', new_product_path %>

А вот тот же шаблон с использованием haml - index.html.haml:
%h1 Listing products
- if notice
  %p#notice= notice
%table.products
  - @products.each do |product|
    %tr{class: "#{cycle('list_line_odd', 'list_line_even')}"}
      %td
        %p= image_tag(product.image_url, class: 'list_image')
      %td.list_description
        %dl>
          %dt= product.title
          %dd= truncate(strip_tags(product.description), length: 80)
      %td.list_actions
        = link_to 'Показать', product
        %br/
        = link_to 'Ред.', edit_product_path(product)
        %br/
        = link_to 'Удалить', product, method: :delete, data: { confirm: I18n.t('app.delete_confirm') }
%p= link_to 'New product', new_product_path

Если вы используете в работе Ruby on Rails, использование шаблонизатора haml позволит вам сократить количество кода в ваших шаблонах и сделает их более читаемыми. Поначалу может показаться, что нет смысл учить чью-то придумку, ведь есть старый добрый html. Однако в больших проектах кода много и его читаемость можно улучшить применив этот шаблонизатор. Кроме того, правила haml-шаблонизатора весьма просты, освоить их можно минут за 20, проделав самостоятельно примеры, приведенные на официальном сайте шаблонизатора haml.

Также есть аналогичный шаблонизатор - slim, синтаксис которого еще лучше. Однако, у нас в проекте используется haml, поскольку "так исторически сложилось".

понедельник, 1 декабря 2014 г.

Определить первый родительский класс

В языке программирования Ruby каждый объект имеет как минимум один родительский класс. Однако большинство объектов имеют в цепочке наследования несколько классов. Каждый дочерний класс может переопределить метод, определенный в родительском.
Требуется написать метод
def location_in_hierarchy(object, method)
...
end
который будет принимать объект и метод и возвращать класс, в котором данный метод был впервые определен.

Сериализация и десериализация объекта класса в Ruby

Объект в языке Ruby можно представить в виде строки. Также можно преобразовать строку обратно в объект. Для этого необходимо подключить библиотеку yaml.
# encoding: utf-8
require "yaml"

class Car
  def initialize(model, speed)
    @model = model
    @speed = speed
  end

  # Десереализуем в объект
  def self.deserialize(yaml_string)
    YAML::load(yaml_string)
  end
  
  # Объект сериализуем
  def serialize
    YAML::dump(self)
  end

  def to_s
    "Car: [model = #{@model}, speed = #{@speed}]"
  end
end

audi = Car.new('Audi', 400)
yaml = audi.serialize
puts "yaml выглядит так:"
p yaml
puts "Yaml можно обратно преобразовать в объект\n: #{Car.deserialize(yaml)}"
puts

bmw = Car.new('BMW', 600)
mazda = Car.new('Mazda', 120)
cars = [bmw, mazda]

puts "Мы можем даже сериализовать массивы: #{cars}"
Программа выведет:
yaml выглядит так:
"--- !ruby/object:Car\nmodel: Audi\nspeed: 400\n"
Yaml можно обратно преобразовать в объект
: Car: [model = Audi, speed = 400]

Мы можем даже сериализовать массивы: [Car: [model = BMW, speed = 600], Car: [model = Mazda, speed = 120]]