Sinatra и Whois gem: маленькое приложение

Сегодня речь пойдет о Ruby, но я не буду вдаваться в подробное описание языка, поэтому ограничимся базовой функциональностью замечательного фреймворка Sinatra.

Вступление

Мой отпуск подходит плавно к концу, но чтобы не зря тратить время решил я посвятить его изучению нового для меня языка программирования. Почему Ruby, возможно назреет у вас вопрос. Отвечу так: я долго шел к смене языка программировани, нет, это не из-за того, что мне не нравится PHP, отнюдь, но захотелось чего-то другого во время отпуска, тем более, что время позволяет. Прежде чем остановиться на Ruby, мною было прочитано много килобайт материала, просмотрено много мегабайт видео презентаций и скринкастов, и я решил более не задаваться глупыми вопросами, а может Python, может еще что-нибудь, решил просто попробовать.



Почему Sinatra, а не Rails? Все просто: чтобы понять как работает такой большой веб-фреймворк, как Rails, понадобится намного больше времени, да и понять его без хорошего знания Ruby, я не рискну, а Sinatra в этом отношении в разы полегче, можно сказать, что это чистый Ruby.

Постановка задачи

Задача будет проста до безумия: у нас будет иметься одна страница с одним текстовым полем. Если мы введем туда корректное имя домена, приложение ответит нам доступен ли домен для регистрации и срок истечения, если домен зарегистрирован.

Используемый инструментарий

В первую очередь нам, конечно же, понадобится сервер с установленным на нем Ruby, локальный вполне подойдет. В этой короткой заметке я не буду рассказывать как его [сервер] установить, в интернете полным-полно подобных мануалов и раскрывают они тему исчерпывающе. В дополнение к гему sinatra, нам также понадобятся: whois (http://goo.gl/0jEJA) и гем json (им мы будем отдавать ответ).

Реализация

Приложение у нас будет состоять из двух экшнов: первый это главная страница, на которой и будет отображаться текстовый элемент и экшн, который будет отвечать за прием данных по средством AJAX, отправку запроса серверу whois и возвращению ответ в формате JSON.
Шаблонизатором у нас будет выступать HAML, очень удобная и простая штука, так что приведу код шаблона главной страницы в формате haml.

!!!
%html
  %head
    %meta{:content => "text/html; charset=UTF-8", "http-equiv" => "content-type"}/
    %script{:type => 'text/javascript', :src => 'http://yandex.st/jquery/1.7.0/jquery.min.js'}
    %script{:type => 'text/javascript', :src => '/javascripts/default.js'}
    %title Маленькое приложение на Руби (Sinatra, Whois, Json)
  %body
    %div
      %p Впишите какой-нибудь, желательно, корректный домен и нажмите Enter
      %form{:method => 'post', :action => '/ajax.json'}
        %input{:type => "text", :name => "domain"}
      %ul{:id => 'info'}

Также приведу исходных код экшна главной страницы, который занимает всего 1 строчку. Его задача только отобразить шаблон.
get '/' do
   haml :index
end

Наш AJAX экшн будет срабатывать по POST запросу при отсылке данных по адресу /ajax.json
  post '/ajax.json' do  
    domain = params[:domain]

    begin
      info = Whois.query(domain)
    rescue Whois::ServerNotFound
    end 
    content_type :json
    
    { :domain => domain, :info => {:available => info.available?, :registered => info.registered?, :expires => info.expires_on} }.to_json unless info.nil?
  end

В первой строке которого мы получаем имя домена через форму. Далее получаем необходимую информацию о домене, для того чтобы не сыпались в приложении исключения я оборачиваю вызов кода, который легко может выбросить исключения в обертку аля PHP try-catch. content_type json, как вы догадались, устанавливает формат ответа. В последней строке мы формируем JSON. Что удобно в Ruby, так это то, что не нужно возвращать значения, данный язык делает это сам, т.е. последняя строка в методе возвращает значение.

Осталось только связать нашу форму на главной странице и AJAX экшн. Для этих целей очень хорошо подойдет несколько строк на замечательном JavaScript-фреймворке jQuery. Задача — отправить AJAX запрос методом POST, дождаться ответа от сервера и заполнить список полями из полученного JSON.
$(function() {
  $('form').submit(function() {
    $.post('/ajax.json', $(this).serialize(), function(data) {
      if (!data) {
        alert('Что-то пошло не так (');
        return;
      }   
      $('ul#info > *').remove();
      $.each(data.info, function(field, value) {
        $('ul#info').append('<li><strong>' + field + '</strong> ' + value + '</li>')
      }); 
      console.log(data);
    }, 'json');
    return false;
  }); 
});

Исходный код

Исходный код вы можете посмотреть на гитхабе, здесь.

Заключение

Это была моя первая заметка на тему Ruby, надеюсь, мой блог и дальше будет пополняться подобными постами, потому что язык мне очень нравится, нравится своей гибкостью и необычностью (как программист на PHP говорю). Спасибо зв внимание и до новых встреч.

Добавлено: 21 Сентября 2013 10:31:25 Добавил: Андрей Ковальчук

Один вопрос и несколько ответов



Давно что-то я не писал, а тут выдался свободный денек, чтобы рассказать еще об одной плюшке, которую я узнал. Все приведенное ниже не претендует на ортодоксальную правильность, это лишь мое мнение и мое решение.

Имеется, к примеру, у нас вопросы и ответы и мы, вместо того, чтобы создавать сначала вопрос и потом привязывать к нему по одному ответы, будем делать это все на одной странице.


Решение
В решении нам помогут gem simple_form, который в разы облегчает работу с формами и coffescript, который хоть и транслируется в javascript, но по синтаксису очень похож на руби со своим «сахаром».

Модели
Как ясно из название, у нас будут две модели: Question и Answer, в них нет ничего сложного

Вопрос

class Question < ActiveRecord::Base
  attr_accessible :name, :answers_attributes

  has_many :answers

  accepts_nested_attributes_for :answers

  validates :name, :presence => true
end

Ответ
class Answer < ActiveRecord::Base
  attr_accessible :name, :question_id

  belongs_to :question

  validates :name, :presence => true
end

Единственное, что может показаться странным с первого взгляда, так это строка accepts_nested_attributes. Которая говорит, что модель Question может принимать атрибуты и для ответа, т.е. один post запрос может нам и вопрос создать и ответы к нему.

Я для экономии времени использовал twitter-bootstrap-rails gem и scaffold, поэтому все получилось почти готовое. Лишь небольшие правки я внес в форму app/views/questions/_form.html.erb
<%= f.input :name, :label => 'Текст вопроса' %>

<div class="answers">
  <%= f.simple_fields_for :answers do |answers_form| %>
    <div class="answer">
      <%= answers_form.input :name, :label => 'Текст ответа' %>

      <%= link_to '#', :class => 'remove_answer' do %>
        <i class="icon-minus-sign"></i>
      <% end %>
    </div>
  <% end %>
</div>

<%= link_to '#', :id => 'add_answer' do %>
  <i class="icon-plus-sign"></i>
<% end %>

Я обернул ответы на вопрос классом .answers, чтобы они визуально отличались, добавил вложенную форму для вопросов с помощью simple_form и две кнопки: «+» — для добавления ответа и «-» — для его удаления

Сейчас задача сводится к тому, чтобы динамически добавлять/удалять ответы на странице без ее перезагрузки, взглянем на генерируемый html
<input class="string required" id="question_answers_attributes_0_name" name="question[answers_attributes][0][name]" size="50" type="text">

При добавлении нового элемента возрастает цифра в айдишнике и в имени текстового поля. Для того, чтобы создать новый ответы мы будем действовать так: склонируем последний ответ на странице с помощью $.clone(), поменяем все элементы name и id, заменив у них цифру в середине. Заменять будем на new Date().getTime(), который генирует случайное число, хоть и большое, но оно нам подходит. Также нам нужно будет не забыть о лейблах, а то получится, что мы кликаем на лейбл, а в фокус попадает совсем не соответствующий элемент. Итак, поехали.
$ ->
  # обрабатываем клик по кнопке добавления
  $('#add_answer').click ->
    $last = $('.answer:last') # последний ответ на странице

    $cloned = $last.clone() # клонируем его

    # получаем рандомное число
    randValue = new Date().getTime()

    # пробегаемся по всем элементам, у которых есть атрибут name или for
    $('[name], [for]', $cloned).each (index, item) ->
      $item = $(item)

      # если это текстовое поле, заменяем id и name
      if $item[0].nodeName is 'INPUT'
        $item.attr 'id', $item.attr('id').replace(/\d+/, randValue)
        $item.attr 'name', $item.attr('name').replace(/\d+/, randValue)

      # если это лейбл, то заменяем атрибут for
      $item.attr 'for', $item.attr('for').replace(/\d+/, randValue)  if $item[0].nodeName is 'LABEL'

    # скрываем добавляемый ответ, чтобы красиво появиться
    $cloned.hide()
    $cloned.appendTo $last.parent()
    $cloned.slideDown()

    false

Осталось добавить обработчик события удаления ответа
# при клике на удаление ответа, удаляем его
$('body').delegate '.remove_answer', 'click', ->
  $(this).closest($('.answer')).slideUp ->
    $(this).remove()

  false

Метод $.closest находит ближайший элемент с классом .answer, поднимаясь вверх по дереву DOM. Находим его, скрываем, а после проигрыша анимации удаляем.

Что можно сделать лучше

Добавить валидацию на количество ответов в модели, к примеру, validates_length_of :answers, :minimum => 1
Добавить проверку, чтобы не удалить последний ответ, после удаления которого мы не сможем добавить новый — клонировать некого icon smile Один вопрос и несколько ответов
Не использовать скаффолд, который генерирует много ненужного мусора, по крайней мере, для данной задачи
Да и вообще предела-то совершенству нет icon smile Один вопрос и несколько ответов
Надеюсь, данная неидеальная реализация кому-нибудь в жизни пригодится, вопросы, советы? — велком в комментарии.

Добавлено: 21 Сентября 2013 10:28:10 Добавил: Андрей Ковальчук

Rails Полиморфная связь и TokenInput jQuery

Представьте ситуацию, что у вас имеется некая модель, к которой вы хотите добавлять по средствам связей другие модели. Хорошо, когда данную связь можно описать обычным has_many, другое дело, когда нужно привязать разнородные модели, к примеру, у вас имеется модель вопросов, к которой нужно привязать город или страну (не очень наглядный пример, вряд ли он вам в жизни встретится, но я поделюсь своим опытом).

Инструментарий

Как вы, наверное, догадались, основным инструментом будет выступать ruby an rails. Из гемов будем использовать simple_form для удобного создания форм, inherited_resources, чтобы писать намного меньше кода в контроллерах (сейчас не представляю без этого гема жизни).

Также нам понадобится плагин для jQuery tokenInput, который будет отображать выпадающий список с autoComplete’ом, из которого мы сможем выбрать одно значение из стран или городов (список будет один).

Реализация

Начнем с контроллера QuestionableController (не очень, конечно, удачное название), который будет отдавать солянку из городов и стран с возможностю поиска.

class QuestionableController < ApplicationController
  def index
    countries = Country.find(:all, conditions: ['name LIKE(?)', "%#{ params[:q] 
    cities = City.find(:all, conditions: ['name LIKE(?)', "%#{ params[:q] }%"])

    @questionable = countries + cities

    respond_to do |format|
      format.json { render }
    end  
  end  
end

Лучше вынести всю логику в модель, но данный пример создан исключительно для демонстрации. Что мы делаем в этом контроллере? Ищем страны и города, удовлетворяющих параметру q и заполняем инстанс переменную @questionable

Также давайте заполним нашу вьюшку index.json.erb
<%= sanitize @questionable.to_json(methods: [:id_with_class_name], only: [:id, :name]) %>

в которой мы рендерим наш json, т.к. нам не нужны все поля модели мы используем только необходимые (only: [:id, :name]), ключ methods: указывает на то, что мы помимо физических свойст, будем использовать метод модели id_with_class_name.

Пока не забыл, давайте пропишем пару строк в наш config/routes.rb
resources :questions # для того, чтобы создавать/показывать/удалять вопросы
resources :questionable, only: :index # для того, чтобы отображать json

Перейдем к моделям:

models/question.rb
class Question < ActiveRecord::Base
  attr_accessor :location

  belongs_to :questionable, polymorphic: true
  attr_accessible :name, :questionable_id, :questionable_type

  before_save do
    return unless self.location =~ /_/
    location_id, location_type = self.location.split('_')
    self.questionable_id   = location_id   unless location_id.nil?
    self.questionable_type = location_type unless location_type.nil?
  end
end

Самая интересная строчка — это та, которая начинается с belongs_to, в ней мы объявляем полиморфную связь questionable. Дальше идет метод, который мы
вызываем каждый раз перед сохранением в базу данных. В нем мы парсим строку из autoComplete’а, которая будет иметь вид id_ModelName и сохраняем отдельно id и ModelName.

Модель city.rb
class City < ActiveRecord::Base
  has_many :questions, as: :questionable
  attr_accessible :name

  def id_with_class_name
    "#{ id }_#{ self.class.name }"
  end
end

В ней объявляется связь has_many через questionable, который мы обявили в модели question полиморфной. Далее идем метод, который возвращает id записи и название класса модели (чтобы можно было отделить зерно от плевел).

Модель country.rb почти такая же
class Country < ActiveRecord::Base
  has_many :questions, as: :questionable
  attr_accessible :name
  
  def id_with_class_name
    "#{ id }_#{ self.class.name }"
  end
end

Связь обявлена также как и в предыдущей модели.

Наш маленький контроллер QuestionController
class QuestionsController < InheritedResources::Base

end

где остальное спросите вы. Это все. Это все, что нам нужно для того, чтобы создавать, удалять, редактировать вопросы. Как вы, наверное, заметили наш контроллер наследуется не от ApplicationController’а, а от inherited_resources. Что это значит? А это значит, если абстрагироваться от 80% возможностей этого гема, то, что гем берет всю «грязную» работу на себя, предоставляя нам в пользование переменные resource и collection, хранятся в которых ссылка на текущую запись (resource) и список всех записей (collection) соответственно. Для того, чтобы мы смогли создать свой первый вопрос нам необходимы лишь вьюшки. Начнем с _form.html.erb, которую мы будем подключать и в создании вопроса, и в редактировании.

app/views/questions/_form.html.erb
<%= simple_form_for resource do |f| %>
  <% pre = if resource.questionable %>
    <% [resource.questionable].to_json(only: [:id, :name]) %>
  <% end %>
  <%= f.input :name %>
  <%= f.input :location, input_html: { 'data-pre' => pre, class: 'token-input-questionable' } %>
  <%= f.submit nil %>
<% end %>

Как видите, почти никаких различий с нативным form_for нет. Во второй строке мы заполняем переменную pre json’ом выбранного города или страны, в предпоследней создаем инпут с атрибутом data-pre, равным json’у и классом, чтобы мы могли найти этот элемент без проблем.

new.html.erb и edit.html.erb абсолютно одинаковы и представляют собой следующее:
<%= render 'form' %>

в них мы отрисовываем только форму.

Далее кинем наш скачанный jquery.tokeninput.js в папку app/assets/javascripts и допишем в application.js следующее:
$(function() {
  var $input = $('.token-input-questionable');
   $input.tokenInput('/questionable.json', {
      tokenLimit: 1,
      tokenValue: 'id_with_class_name',
      prePopulate: $input.data('pre')
    });
});

Первым параметром к tokenInput мы указываем откуда брать данные, tokenLimit:1 указывает на то, что одной записи нам будет достаточно (одного элемента из выпадающего списка), tokenValue — откуда мы будем брать имя, которое будем отображать из json’а, prePopulate используем для того, чтобы заполнить элемент, если мы его уже выбрали (редактирование).



Сумбурным получилось изложения материала, потому что для меня это в новинку и писалось это для того, чтобы не забыть полезную «плюшку» в дальнейшем. Если заметка кому-нибудь еще пригодится, я буду очень рад.

P.S.: Если данную заметку читает Александр, мало ли, то, надеюсь, он будет не против, что я позаимствовал его идею.

Демо проект «лежит» на heroku, репозиторий, как всегда на github (там же лежат и миграции, нужные для запуска проекта).

Удачи вам в любых начинаниях и до новых встреч

Добавлено: 21 Сентября 2013 10:24:08 Добавил: Андрей Ковальчук

Введение в TDD с Ruby и Rspec

Здравствуйте, уважаемые разработчики, сегодня я коснусь немного новой для себя методологии, а именно TDD, что означает test driven development (разработка через тестирование).

Что же представляет собой эта методология, что за разработка через тестирование? Это методика, которая с ног на голову переворачивает стандартный ход дел: продумывание архитектуры, проектирование, написание кода и в заключении написание модульных и приемочных тестов. По задумке TDD мы сначала продумываем архитектуру, потом пишем модульные они же юнит тесты, потом непосредственно код. С одной небольшой поправкой: юнит тесты пишутся очень маленькими, после этого пишется код, который позволяет тесту закраситься в зеленый цвет и так дальше — короткие итерации, которые сводят к минимуму функциональные и логические ошибки в приложениях.

Так как я в данный момент очень поглащен изучением замечательного языка ruby, с его основ, то на нем и будем проводить наши с вами эксперименты. Чтобы не усложнять и без того сначала непростую к пониманию информацию, я попробую рассказать про данную методологию на простом примере класса калькулятора. И для дальнейшего, но уже самостоятельного закрепления материала и изучения, я очень рекомендую найти в интернете пример, наглядно показывающий TDD при разработке класса, который подсчитывает игровые очки в боулинге, учитывая особенности игры.

Итак, поехали! Для начала нам понадобится gem rspec, который и будет нашим тестирующим инструментом, почему он, если есть аналоги? Потому что он первый попался мне на глаза, может есть и лучше, я пока не вникал в эту сторону вопроса, как известно — лучшее — враг хорошего.

gem install rspec

Команда установит нам необходимый гем со всеми необходимыми зависимостями.

Далее создаем папку проект (.). Дерево нашего проекта будет иметь вид:
.
├── _
├── calculator.rb
└── spec
    └── calculator_spec.rb

calculator.rb — наше приложение, spec/calculator_spec.rb — наш файл с тестами.

Создайте, если у вас еще не созданы, эти файлы, calculator.rb оставьте пустым. Начнем разработку, как уже говорилось вначале заметки с тестов, но перед этим давайте напишем небольшой план по разработке.

* Должен существовать класс калькулятора
* Калькулятор должен иметь метод add (сумма)
* Метод add должен возвращать сумму двух своих аргументов
* Должен иметь метод sub (разность)
* Также и для методов mul (произведение) и div (частное)
* Метод div должен кидать исключение, когда пытаемся делить на 0
* (не относящийся к делу пункт) Класс Calculator должен быть проивзодным от Calculator

Напишем первый тест
require '../calculator'
describe 'Calculator' do
   it 'should exists' do
      Calculator.new
   end
end

Что же мы получаем, в первой строке все просто — подключаем файл нашего приложени, ведь над ним будем проводить наши эксперименты (без него никак). Со слова descibe начинается контекст теста, входящая в него конструкция (it ‘should exists’) непосредственно сам тест. Остается только запустить тест и посмотреть на результат:
rspec ./spec/calculator_spec.rb

Введение в TDD с Ruby и Rspec
Чтобы написать минимальный код, который бы прошел данный тест, нужно две строчки, а именно создать класс Calculator, так и запишем в ./calculator.rb
class Calulcator

end

Запускаем наш тест еще раз и убеждаемся, что тест пройден. Напишем новый тест, заключим его в тот же describe, что и предыдущий. У нас получится следующее:
describe 'Calculator' do
   it 'should exists' do
      Calculator.new
   end
   it 'should have method add(a,b)' do
      Calculator.new.add 2,3
   end
end

Введение в TDD с Ruby и Rspec
Тест говорит следующее: «Калькулятор должен иметь метод add, который принимает 2 параметра», запускаем наш тест и видим, что !!! тест провален, потому что у нас метода add, тем более принимающего два параметра, так в чем же дело, давайте его напишем, ведь это всего две строки кода.
class Calculator

def add(a, b)

end

end

Теперь наш тест проходит и мы можем двигаться дальше. Давайте сделаем так, чтобы метод add возвращал сумму своих двух аргументов. Начнем, конечно же, с написания теста.
Введение в TDD с Ruby и Rspec
   it 'add() method should add two numbers' do
      Calculator.new.add(2, 2).should_not eq(5)
      Calculator.new.add(2, 2).should eq(4)
   end

И, собственно, код, данный тест проходящий:
def add(a, b)
   a+b
end

Остальные тесты я приведу вам без кода приложения, чтобы они выполнялись правильно вам потребуется код написать самим, благо, он совсем простой и займет всего пару минут.
require '../calculator'

describe 'Calculator' do
   it 'should exists' do
      Calculator.new
   end
   it 'should have method add(a,b)' do
      Calculator.new.add 2,3
   end

   it 'add() method should add two numbers' do
      Calculator.new.add(2, 2).should_not eq(5)
      Calculator.new.add(2, 2).should eq(4)
   end

   it 'sub() method should return subtraction' do
      Calculator.new.sub(5, 3).should eq(2)
   end

   it 'mul() method should return multiple' do
      Calculator.new.mul(2, 7).should eq(14)
   end

   it 'div() method should return division' do
      Calculator.new.div(10, 2).should eq(5)
   end

   it 'div() method should raise exception by division by zero' do
      expect { Calculator.new.div(2, 0) }.to raise_error(ZeroDivisionError)
   end
end

Заключение

В данной заметке мы очень поверхностно познакомились с очень интересной методологией разработки TDD (test driven development) и разобрали небольшой пример, чтобы немного закрепить знания. Если вы будете применять данную методику в своих проектах, то количество ошибок в них будет асимптотически приближаться к нулю, это ли не счастье программиста?

Добавлено: 21 Сентября 2013 10:20:22 Добавил: Андрей Ковальчук

Скачиваем Torrent файлы с помощью ruby и transmission



Сегодня я расскажу вам как я решил несложную, но очень акутальную для меня задачу, а именно скачивание файлов с торрентов. Я приследовал в первую очередь цель скачивать файлы с помощью айпеда, в котором очень сложно с торрент клиентами. Задача свелась к тому, что с айпеда отправляю задание на скачку с торрентов, жду некоторое время и забирая скаченный файл. Сначала хотел реализовать вариант сложнее, а именно, чтобы скаченный файл отправлялся в дропбокс (ничего сложного тут нет, есть удобный и понятный гем), но понял что для меня задача решена и вместо того, чтобы делать какие-то «плюшки», займусь чем-нибудь другим.

Инструментарий

Нам понадобится дешевенький vps сервер, я взял у reg.ru за 128 рублей в месяц с 128 мегабайтами оперативной памяти и 2 гигабайтами жестким и хорошим интернет каналом на XEN. Для данной задачи за глаза хватит такого слабенького сервера.

Демон transmission-daemon для быстрого и удобного скачивания файлов через торрент. Самое сложное тут будет правильно настроить конфиг (но и это просто).

Rails бекенд (unicorn, nginx) для сообщения transmission, что пока заканчивать прохлаждаться и начинать скачивать файлы.

Поэтапная реализация

В данной заметке я не буду рассказывать вам, как устанавливать rvm (в данном проекте я применял его), настраивать nginx + unicorn + capistrano, мануалов полно в инете, я лишь заострю внимание на ключевых моментах. Допустим приложение, чтобы не путаться в путях у вас также как у меня находится в папке /home/deployer/apps/my_site_current.

Давайте разберемся с конфигом transmission-daemon’а, который будет лежать в ~/.config/transmission-daemon/settings.json.

{
    "dht-enabled": true, 
    "download-dir": "/home/deployer/apps/my_site/current/public/attachment/complete-downloads", 
    "download-queue-enabled": true, 
    "download-queue-size": 5, 
    "encryption": 1, 
    "idle-seeding-limit": 30, 
    "idle-seeding-limit-enabled": false, 
    "incomplete-dir": "/home/deployer/apps/my_site/current/public/attachment/incomplete-downloads", 
    "watch-dir": "/home/deployer/apps/my_site/current/public/attachment/incomplete-dir", 
    "watch-dir-enabled": true,
    "incomplete-dir-enabled": true, 
    "lpd-enabled": false, 
    "message-level": 2, 
    "peer-congestion-algorithm": "", 
    "script-torrent-done-enabled": false, 
    "script-torrent-done-filename": "", 
    "start-added-torrents": true, 
    "trash-original-torrent-files": false, 
    "umask": 18, 
    "upload-slots-per-torrent": 14, 
    "utp-enabled": true
}

Большинство опций использунется по умолчанию, но есть некоторые которые стоит пояснить.

«download-dir» — указывает в какую папку сохранять скаченные файлы
«incomplete-dir» — куда будет качать
«incomplete-dir-enabled» чтобы отделить котлеты от мух (скачиваемое и скаченное будут лежать отдельно, дабы не путаться)
«watch-dir» — за какой папкой будем следить, т.е. если в этой папке появляются торрент файлы, то автоматически начинаем их скачивать
«watch-dir-enabled» — включаем опцию «слежения»
Для эксперимента будем скачивать торрент файл с tfile.ru, чтобы без регистрации. Чтобы скачать файл, необходимо поставить куку, которая ставится javascript’ом, но тут нет ничего сложного.

Скачивать будем wget’ом, чтобы проще, а чтобы еще проще взаимодействовать с консольными утилитами будет использовать гем Cocaine от создателей Paperclip’а и FacoryGirl’ы.
class Attachment < ActiveRecord::Base
  validates_presence_of :name, :url 

  attr_accessible :name, :status, :file, :url 

  before_create :download_torrent_file

  private
  def download_torrent_file
    response = open(self.url).read
    
    cookie = response[/tmbx=[^"]*/]

    torrent_url = response[/http:\/\/[^"]*/]

    output_path = '/home/deployer/apps/my_site/current/public/attachment/incomplete-dir/'

    output_file = "#{ output_path }#{ Date.today.to_s }-#{ self.name }.torrent"

    # wget 'http://tfile.ru/forum/download.php?id=549501&attempt=1#r' --no-cookies --header "Cookie: tmbx=6df9fc90ad02d5bdf2ca7449d0cc44
    line = Cocaine::CommandLine.new('wget', ":torrent_url --no-cookies --header 'Cookie: #{ cookie }' -O :output_file",
                                    :torrent_url => torrent_url,
                                    :output_file => output_file)

    line.run
  end  
end

Так мы будем скачивать наши торрент файлы. Первый запуск transmission-daemon’а попробуем не в режиме демона, чтобы не ждать до ишачьей пасхи, а видеть все ошибки воочию. Для этого запустим transmission-daemon с ключом **-f** и все ошибки посыпятся в stdout и мы легко их сможем исправить, если все пойдет по плану, то наш первый файл будет скачен. Если все прошло тип-топ, то можем запускать transmission-daemon без параметров и озадачивать его новыми закачками.

Для того, чтобы отобразить результат и использовал следующее костальное решение в контроллере Attachments#index, я написал следующее:
def index
  @files = Dir.glob("public/attachment/complete-downloads/*")
end

во вьюхе уже выводил список файлов
  <ul>
    <% @files.each do |f| %>
      <li><%= link_to f, f.sub('public', '') %></li>
    <% end %>
  </ul>

Так как мне нужно было лишь скачивать pdf журналы, которые распространяются одним pdf файлом, то у меня не было необходимости выводить папки в папках, достаточно было отобразить файлы в одной папке.

Заключение

Данное решение служит мне верой и правдой уже где-то с месяц и не вызывало никаких нареканий, процессы не отваливаются, журнальчики скачиваются. Было бы неплохо, наверное, сделать так, что при скачивании торрент файла, мы получаем список файлов, которые в нем содержатся и каким-нибудь демоном проверяем все ли файлы скачались, если да, то оповещаем по имейлу пользователя. Но данная «плюшка» выходит за рамки данной заметки. Надеюсь, вам было интересно, до новых встреч.

Добавлено: 21 Сентября 2013 10:08:59 Добавил: Андрей Ковальчук

Поиграем? или совместное использование ruby и php

Сегодня заметка будет посвящена теме далекой от веб-программирования, она будет посвящена немного алгоритмам, немного парсингу и немного Mongodb. Будем сегодня играть в игру.

Часто по вечерам мы с женой играем в приложение на iPad’е под названием Словомания.



Смысл в нем, как вы, наверное, заметили из названия, поиск слов. На входе матрица 4х4, можно начать из любой клетки и задействовать соседние по одному разу, чтобы отыскать имеющееся у них в словаре слово (в том словаре есть и наречия, и глаголы, и прилагательные, и конечно же существительные).

Задача: отыскать все слова. Но я немного облегчу себе задачу, потому как по примерным подсчетам в матрице 4х4 всевозможных комбинаций поиска слова около 16 миллионов, что даже по скромным подсчетам и при словаре в ~160 тысяч слов займет около 5 часов. В данной заметке мы будем использовать матрицу 3х3, что сокращает количество комбинаций до 8 с небольшим тысяч, что вполне приемлимо, но недостаточно быстро, для одной игры, которая длится около минуты. Да и плевать, для меня главное решить задачу, хоть и читерить в игре не получится ).

Для начала отыщем все возможные комбинации, для этого воспользуемся php-скриптом поиска в ширину, немного его изменив для наших нужд. На выходе должны получить файл, где в каждой строке одна комбинация.

Цифры на матрице я обозначил от 1 до 9 (3х3), поэтому в выходном файле будут строки подобного вида — 1,2,3,6,5,4,7,8,9.

Но сначала нам пригодится небольшой скрипт, который поможет построить граф для таблицы 3х3.

for i in (1..9) do 
  print "%-2s" % i
  if i % 3 == 0 then print "\n" end
end

На выходе получим таблицу
1 2 3
4 5 6
7 8 9

которая поможет нам построить граф, ключами массива которого будут вершины «откуда», значениями — точки «куда», мы можем попасть из данной вершины. Из таблицы следует, что из точки 1, мы можем попасть в точки: 2,5 (по диагонали тоже можно) и 4. Так нам нужно описать каждую вершину графа.
<?php 
$paths = array();
/**
* рекурсивный поиск в глубину
*
* @param $graph array наш граф
* @param $start string точка "откуда"
* @param $end string точка "куда"
* @param $path array массив предыдущих шагов
**/
function find_path($graph, $start, $end, $path = array())
{
  global $paths;
  $path[] = $start;
  
  if ($start == $end)
     return $path;
  
  if (!isset($graph[$start]))
     return false;

  # находим всех соседей
 foreach($graph[$start] as $node) {
    if (!in_array($node, $path)) {
      $newpath = find_path($graph, $node, $end, $path);
      if ($newpath) 
        $paths[] = implode(',', $newpath);
    }
  }
  return array();
}

# получившийся граф
$graph = array(
  '1' => array('2', '4', '5'),
  '2' => array('1', '3', '5', '4', '6'),
  '3' => array('2', '5', '6'),
  '4' => array('1', '2', '7', '8', '5'),
  '5' => array('1', '2', '3', '4', '6', '7', '8'),
  '6' => array('3', '2', '5', '9', '8'),
  '7' => array('4', '5', '8'),
  '8' => array('4', '6', '6', '7', '9'),
  '9' => array('5', '6', '8')
);

# циклы по всем вершинам графа,
# каждая вершина может быть начальной и конечной
for($i = 1; $i <= 9; $i++)  
  for($j = 1; $j <= 9; $j++)
    find_path($graph, (string) $i, (string) $j);

# избавляемся от дубликатов
$result = array_unique($paths);
echo "\n\nКоличество комбинаций: ", count($result), "\n";

# записываем результаты в файл
$fh = fopen('combinations.dat', 'a');
 fwrite($fh, implode("\n", $result));
fclose($fh);

В коде старался комментировать каждый момент, поэтому, думаю, должно быть понятно. На выходе у нас файлы combinations.dat с 8175 строками из всевозможных комбинаций.

Теперь давайте подготовим наш словарь. Я скачал какой-то орфографический словарь, каждая строка которого предствляла собой следующее
жопа#ж`опа%ж`опа, -ы

Я решил сохранить два первый слова, точнее слово и ударение (которое в данном проекте не используется). Первое слово отделено от второго #, второе от остального %. По мере парсинга словаря, мы будем записывать все слова в нашу mongo базу данных для быстрого поиска по ней в дальнейшем.
# coding: utf-8

require 'mongo'
require 'mongo_mapper'

MongoMapper.database = 'words'

class Word
  include MongoMapper::Document

  key :word
  key :word2
end

f = File.open('./dic.txt', 'r')

# цикл по всем строкам словаря
f.lines.each do |line|
  word, word2 = line.split('#') # отделяем первое слово
  word2 = word2.split('%')[0] # и второе
  puts "#{word} #{word2}\n"
  
  # и сохраняем его
  Word.create(
    :word => word,
    :word2 => word2
  )
end

Если вам лень запускать подобный скрипт, то в корне проекта лежит файл words.json, содержащий все слова, вы можете импортировать его в свою mongo базу командой
mongoimport -d DB -c COLLECTION words.json
.

Далее, чтобы ускорить и без того медленное приложение, нужно создать индекс в базе на поле word, запускаем mongo-терминал, выбираем базу данных use DB и пишем следующее
db.COLLECTION.ensureIndex({word: 1})

, где DB — имя ваше базы данных, а COLLECTION — имя коллекции.

Итак, переходим к завершающей части — моменту истины, так сказать, поиску слов.
# encoding: utf-8

require 'mongo'
require 'mongo_mapper'

MongoMapper.database = 'words'

class Word
  include MongoMapper::Document

  key :word
  key :word2
end

# файл с полученным в предыдущих шагах комбинациями
COMBINATIONS_FILE = './combinations.dat'
# файл с результатами
OUTPUT_FILE = './results.dat'

# наше игровое поле
alphabet = %w{
  к о ж
  а п у
  а р р
}

# поиск текущего слова в словаре
def find_a_word(word)
  w = Word.where(:word => word).first
  if w
    return w.word
  else
    return nil
  end
end

# из комбинации, учитывая alphabet, составляем "слово"
def number_to_letters(alphabet, line)
  letters = line.split(',')
  word = []
  letters.each do |letter|
    word << alphabet[letter.to_i - 1]
  end
  word.join()
end

# основной цикл приложение
# проходимся во всему списку комбинаций
File.open(COMBINATIONS_FILE, 'r') do |f|
  f.lines.each do |line|
    # составляем слово
    n_t_l = number_to_letters(alphabet, line)
    # находим в бд
    result = find_a_word(n_t_l)
    unless result.nil? then # если нашли
      puts n_t_l # выводим его 
      File.open(OUTPUT_FILE, 'a') do |output| 
        output.puts n_t_l # и записываем в файл
      end 
    end
  end
end

После этого запускаем скрипт ruby find-words.rb, откидываемся на скпинку стула и ждем результат. Начнут появляться на экране слова, которые были найдены этим скриптом, возможны повторения, потому что одно слово может быть составлено из разных комбинаций.

Удачи в кодинге и побольше интересных задач, которые вам под силу решить icon smile Поиграем? или совместное использование ruby и php .

P.S.: репозиторий, как всегда, доступен на github.

update 25.03.2012
Все-таки намного интереснее заставить приложение работать как задумывалось, а именно, чтобы оно находило слова из матрицы 4х4. Для этого мною были предприняты следующие меры:

удаление лишних слов из словаря
оптимизация самого алгоритма
сужение диапазона поиска

Удаление лишних слов из словаря

Пробежался я по словарю и понял, что там много слово, содержащих дефисы, скобки, пробелы. Некоторые слова состояли из одной-двух букв. Я решил с ними попрощаться, благо mongo поддерживает регулярные выражения.
MongoMapper.database = 'words'

class Word
  include MongoMapper::Document
  key :word
end

#s = Word.all(:word => /^.*[-А-Я].*$/).each do |word|
#s = Word.all(:word => /^.*[ё].*$/).each do |word|
#s = Word.all(:word => /^.*[\(\)\.\s+].*$/).each do |word|
s = Word.all(:word => /^.*[-,.:;].*$/).each do |word|
  word.destroy
end

Запускаем и удаляем ненужные слова, из 160 тысяч слов, останется 140 тысяч, неплохо icon smile Поиграем? или совместное использование ruby и php

Оптимизация самого алгоритма

Самый быстрый код — это мало кода. Поэтому максимально постараемся избавиться от методов. Выбросив все лишнее получим следующее.
# encoding: utf-8

require 'mongo'
require 'mongo_mapper'

MongoMapper.database = 'words'

class Word
  include MongoMapper::Document

  key :word
end

COMBINATIONS_FILE = './game.dat'

Alphabet = %w{
ё ж е т
у ч е р
к о ю т
р м п ь
}

start = _start = Time.now
counter = 0

combinations = []

File.open(COMBINATIONS_FILE, 'r') do |f|
    while (line = f.gets)
      counter += 1
      line = line.split(',')
      if (3..9).include? line.size
          w = Word.first(:word => line.map{ |l| Alphabet[l.to_i - 1] }.join )
          line = nil
          combinations << w.word unless w.nil?
      end

      if counter % 100_000 == 0
          p combinations.join ' ' unless combinations.empty?
          combinations = []
        p "#{Time.now - start} - #{counter}"
        start = Time.now
      end
    end
end

p (Time.now - _start)

Код получился намного «легче» и быстрее.

Сужаем диапазон поиска

Чтобы хоть как-то еще прибавить скорость, я сузил длину слова, теперь проверяются только строки от 3 до 9 символов, это тоже очень хорошо сужает конечную выборку.

Обратная связь приветствуется

Если вы видите, что есть какие-то неточности или гипотезы по поводу оптимизации быстродействия кода, то всегда пожалуйста, я открыт для обсуждения всегда.

Не хочу быть излишне самоуверенным, но мне, кажется, что сейчас весь алгоритм упирается в «железо», точнее скорость считывания с диска. (К сожалению, у меня нет ssd диска, чтобы это проверить).

В заключение картинка с какой скоростью отрабатывает скрипт. Первое число на картинке — количество секунд на 100_000 строк, считанных из файла и проверенных с каждой строкой словаря, содержащего ~ 140.000 слов. Второе число — сколько строк уже просмотрено. Всего комбинаций получилось, если рассматривать поле 4х4 — 12.000.000 штук. icon smile Поиграем? или совместное использование ruby и php



Обновление от 27.03.2012 (redis)

Пришла еще одна мысля по оптимизации данного приложения, вместо mongodb, я решил попробовать использовать redis — очень быстрое ключ-значение хранилище.

Сначала нужно экспортировать базу mongo в redis, тут ничего сложного (с учетом того, что у вас и сам redis установлен, и redis gem).
require 'mongo'
require 'mongo_mapper'
require 'redis'

MongoMapper.database = 'words'

class Word
   include MongoMapper::Document

   key :word
end

redis = Redis.new(:host => "127.0.0.1", :port => 6379)
Word.all.each do |w|
  w = w.word
  unless /[-А-Я\(\)]/.match w
    redis.set("word:#{w}", w)
  end
end

Пробегаемся по всем словам из коллекции mongodb и сохраняем все слова в redis под ключом word:СЛОВО, чтобы не перепутать с другими ключами.

Теперь основной файл, только в качестве хранилища мы будем использовать redis.
# encoding: utf-8

require 'redis'
redis = Redis.new(:host => "127.0.0.1", :port => 6379)

COMBINATIONS_FILE = './game.dat'

Alphabet = %w{
ё ж е т
у ч е р
к о ю т
р м п ь
}

for i in (1..16) do 
    print "%-3s" % Alphabet[i-1] 
    if i % 4 == 0 then print "\n" end
end

start = Time.now
_start = Time.now
counter = 0

combinations = []
lines = []

File.open(COMBINATIONS_FILE, 'r') do |f|
  f.each do |line|
    counter += 1
    line = line.split(',')
    if (3..9).include? line.size
      w = redis.get("word:" + line.map{ |l| Alphabet[l.to_i - 1] }.join)
      line = nil
      combinations << w unless w.nil?
    end

    if counter % 100_000 == 0
      p combinations.uniq.join ' ' unless combinations.empty?
      combinations = []
      p "#{Time.now - start} - #{counter}"
      start = Time.now
    end
  end
end

p (Time.now - _start)
p counter

Вывод: приложение стало заметно быстрее (в 3-5 раз), скриншот прилагаю.



Надеюсь, смогу добиться скорость выполнения в раза больше, чем сейчас. Мне нужно чтобы приложение проходило по всем комбинациям, сравнивая со всем словами в словаре и тратило при этом меньше минуты, сейчас около 3х.

Добавлено: 21 Сентября 2013 09:23:34 Добавил: Андрей Ковальчук

Парсим news.ycombinator.com или не рельсами едиными жив человек (Sinatra, DataMapper)

Дело было вечером, делать было нечего и решил я написать небольшое приложение на Sinatra с Datamapper’ом. За идеей далеко ходить также не пришлось: решил написать небольшой «фильтратор» интересного для меня контента из новостей news.ycombinator.ru. Не стал изобретать велосипед на этот раз и интересными буду считать новости, названия которых содержат определенные слова. Будем отображать список прочитанных и непрочитанных новостей. Список новостей каждый час будет обновляться по cron’у — вот и вся задача.

Начнем с реализации: для этого нам понадобится:

data_mapper с двумя адаптерами (sqlite3 для локального использование и postgresql для production’а)
sinatra
coffeeScript, хоть можно было и легко обойтись без него
slim в качестве шаблонизатора
Итак, поехали:

Gemfile:

source 'https://rubygems.org'

gem 'sinatra'
gem 'data_mapper'

group :development do
  gem 'dm-sqlite-adapter'
  gem 'capistrano'
end

group :production do
  gem 'dm-postgres-adapter'
end

gem 'slim'
gem 'coffee-script'
gem 'whenever', :require => false

gem 'nokogiri'

gem 'unicorn'

В нем нет ничего необычного, добавляем необходимые гемы для разных сред.

Теперь самое интересное: основной файл приложения, который занимает больше всего места.

./app.rb
require 'sinatra'
require 'slim'

require 'coffee-script'
require 'data_mapper'

DataMapper::Property::String.length(400)

configure :development do
  DataMapper.setup(:default, 'sqlite3:./db/articles.db')
end

configure :production do
  DataMapper.setup(:default, 'postgres://deployer:funnydb@localhost/ycombinator')
end

class Article
  include DataMapper::Resource

  INTERESTING_KEYWORDS = %w(ruby rails coffee js javascript ember angular
    backbone tdd rspec shoulda gem unicorn nginx sinatra vim mac)

  property :id, Serial
  property :url, String, :unique_index => :u, :required => true, :format => :url
  property :title, String, :required => true, :index => true
  property :interesting, Boolean, :default => false
  property :read_at, DateTime
  timestamps :created_at, :updated_on

  def interesting?
    !!(title =~ Regexp.new(INTERESTING_KEYWORDS.join('|'), Regexp::IGNORECASE))
  end

  def self.interesting_to_me
    all(:interesting => true)
  end

  def self.unread
    all(:read_at => nil)
  end

  def self.read
    all(:read_at.not => nil)
  end

  def self.search(term='')
    if DataMapper.repository.adapter.options[:scheme] == 'sqlite3'
      all(:title.like => "%#{term.to_s}%")
    else
      all(:conditions => [ 'title ILIKE ?', "%#{term.to_s}%" ])
    end
  end
end

DataMapper.finalize
#DataMapper.auto_migrate!
DataMapper.auto_upgrade!

helpers do
  def do_process(scope=nil)
    @search_term = params[:term].nil? ? nil : params[:term]
    @articles = case scope
                when :all
                  Article
                when :all_read
                  Article.read
                when :all_unread
                  Article.unread
                else
                  Article.interesting_to_me.unread
                end.search(@search_term)
    slim :index
  end
end

get '/application.js' do
  coffee :application
end

post '/:id/read' do
  @article = Article.get(params[:id])
  @article.read_at = Time.now
  @article.save
end

get '/all' do
  do_process :all
end

get '/all/read' do
  do_process :all_read
end

get '/all/unread' do
  do_process :all_unread
end

get '/*' do
  do_process
end

А теперь немного комментариев:

1-5 строки — подключаем необходимые для работы файлы
7 строка — сообщаем DataMapper’y, что длина строки (String) не 80 символов, а 400, 255 не хватает.
9-15 — конфигурируем два адаптера: один для разработки, другой для продакшна.
17-53
20,21 — объявляем интересные мне ключевые слова
23-28 — описываем все поля, которые будут в нашей модели
30-32 — метод interesting? определяет по заголовку новости интересна она мне или нет
34-40 — несколько используемых в приложении scope’ов
46-52 — метод search (из-за того, что в Postgresql like учитывает регистр букв, пришлось переписать оператор поиска на ilike, который этого не делает)
60-73 — объявляем метод, который является «сердцем» и в зависимости от параметра заполняет коллекцию определенными статьями и рендерит вьюху ./views/index.slim
76-78 — рендерим coffeeScript, которые делает следующее, если мы кликам по новости, то отправляем ajax post запрос и помечаем новость как прочитанную (read_at = Time.now)
80-84 — сам метод, который помечает новость прочитанной при post запросе
86-100 — разные коллекции (все, прочитанные, непрочитанные и т.д.)
теперь Rakefile, который будет парсить news.ycombinator.com каждый час
require './app'

require 'nokogiri'
require 'open-uri'

desc 'Parse all articles'
task :parse do
  doc = Nokogiri::HTML(open('http://news.ycombinator.com/'))
  links = doc.css('td.title a')
  next_page_link = links.pop

  links.each do |link|
    href = link[:href]
    text = link.children.text

    unless Article.first(:url => href)
      Article.create(:url => href, :title => text) 
    end
  end

  puts Time.now.to_s
end

desc 'Update Interesting tasks'
task :update_interesting do
  Article.all.each do |a|
    a.update(:interesting => a.interesting?)
    puts "#{a.id} updated"
  end
end

В нем всего две задачи: первая — парсит новости, вторая нужна для того, что если вдруг изменися интересные ключевые слова, то вы сможете легко обновить список инетересных вам новостей.

Файл, который отвечает за частоту выполнения определенных тасков ./config/shedule.rb
set :output, '/home/deployer/projects/ycombinator/shared/log/shedule.log'

job_type :rake, "cd :path && RACK_ENV=:environment bundle exec rake :task --silent :output"

every :hour do
  rake 'parse'
end

В первой строчке я указываю путь до файла с логами, чтобы каждый раз при запуске rake task’а в конец добавлялось время последнего обновления. В блоке с every можно очень гибко указать как часто выполняться, смотрите документацию к гему whenever.

Также я добавил несколько строк к файлу, выполняющего деплой из Разворачиваем Rails приложение вместе с Capistrano. ./config/deploy.rb
...
set :application, 'ycombinator'
set :whenever_command, "bundle exec whenever"
require "whenever/capistrano"
...

Теперь мы можем запустить обновления cron’а deployer’а командой
cap whenever:update_crontab

После ее запуска вы можете проверить, что вышло, обновился ли cron, запустив на сервере, список cron задач:
crontab -l

Без комментариев оставлю вьюхи, но текст их приведу.

./views/application.coffee
$ ->
  $(".article").click ->
    $.post "/" + $(this).attr("id") + "/read", ->
    $(this).parent('li').remove()


./views/layout.slim
doctype html
 
head
  title Ycombinator
  script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"
  script src="/application.js"
 
body
  a{ href="/all" } All
  br
  a{ href="/all/read" } All read
  br
  a{ href="/all/unread" } All unread
  br
  a{ href="/" } Home
  hr
  == yield

И наконец, ./views/index.slim
h1
  ' Unread articles
  - if @search_term
    = "searched by: '#{ @search_term }'"

p Search form
form{ method="get" action="" }
  input{ type="text" name="term" value="#{ @search_term }" }
  input{ type="submit" value="Find" }

- if @search_term
  a{ href="/" } Home

ul
  - @articles.each do |article|
    li
      a{ href="#{ article.url }" id="#{ article.id }" class="article" target="_blank" }
        = article.title

Получислось такое незамысловатое и некрасивое приложение icon smile Парсим news.ycombinator.com или не рельсами едиными жив человек (Sinatra, DataMapper) .



Вместо заключения: чтобы мозги не были напичканы только рельсами (читать как одним фреймворком), мне кажется, необходимо покидать зону комфорта и писать небольшые приложения для души на смежных технологиях. Скажу честно, для того, чтобы реализовать это несложное приложение у меня ушло масса времени на чтение мануалов к Sinatra, DataMapper’у, нежели на написание кода. Но мне понравилось, практической ценности, конечно, приложение почти не имеет, но мозги размялись однозначно. Разминай мозги, коллега icon smile Парсим news.ycombinator.com или не рельсами едиными жив человек (Sinatra, DataMapper)

Добавлено: 21 Сентября 2013 09:14:39 Добавил: Андрей Ковальчук

Разворачиваем rails приложение вместе с capistrano

Здравствуйте, дорогие друзья, сегодня я расскажу вам как быстро развернуть Ruby on Rails приложение на сервере. Если у вас маленькое приложение, которое посещает 10 человек в день, то вам достаточно будет задеплоить его на heroku (имею в виду бесплатный тариф) и не читать все то, что написано ниже. Если же у вас приложение побольше, то милости прошу на огонек.

Сервер

Сервер: я буду настраивать все на Ubuntu 12.04 (64x), но не думаю, что должны возникнуть сложности с подобной операционной системой. Если же появятся траблы, пишите в комментах, попытаемся решить вместе.

Итак, поехали, логинимся по root’ом и производим базовую настройку сервера. Я начинаю всегда с локалей, для этого делаю следующее.

localedef ru_RU.UTF-8 -i ru_RU -fUTF-8 и в файл /etc/default/locale добавляю

LANG="ru_RU.UTF-8"
LC_CTYPE="ru_RU.UTF-8"
LC_NUMERIC=C
LC_TIME=C
LC_COLLATE=C
LC_MESSAGES=C

Затем создаем пользователя, от лица которого мы и будем все делать и наделим его привилегиями запускать команды через sudo.

useradd -m -g staff -s /bin/bash deployer && passwd deployer — создаем пользователя deployer в группе staff (флаг -g) и с домашней папкой /home/deployer (флаг -m), флаг -s назначает пользоватлю шел по умолчанию. И задаем ему пароль. В дальнейшем весь экшн будет производиться от имени этого пользователя.

Чтобы наш пользователь мог выполнять команды от рута, необходимо добавить его в группу в файл /etc/sudoers
(%staff ALL=(ALL) ALL)

Теперь выходим из под рута и логинимся под деплоером. Для того, чтобы не писать пароль каждый раз при входе, необходимо добавить на сервер инфо, что мы свои (авторизация по ключу).

cat ~/.ssh/id_rsa.pub | ssh deployer@server «mkdir ~/.ssh; cat >> ~/.ssh/authorized_keys»

Если у кого на рабочей машине ubuntu, то они могут запустить эту операцию намного проще (ssh-copy-id deployer@server).

Проверяем, ssh deployer@server более не должно спрашивать пароль, а сразу же пустить нас на сервер.

Для удобства можно добавить в файл ~/.ssh/config, чтобы удобно было заходить, печатая только ssh my_serv
Host my_serv
  HostName server.com$
  User deployer$

sudo apt-get update && sudo apt-get upgrade — обновляем все источники приложений и накатываем обновления

Установка и базовая настройка Postgres’а

sudo apt-get install postgresql libpq-dev (второй пакет нужен для того, чтобы гем pg поставился)

Данная установка подразумевает, что вы не собираетесь соединяться с базой данных с других машин. По умолчанию в Ubuntu, Postgresql сконфигурирован так, чтобы использовать логин текущего пользователя, т.е. если вы вошли под пользователем deployer и в Postgresql есть пользователь deployer, то спрашивать пароль у вас никто не будет.

sudo -u postgres createuser —superuser $USER (создаем пользователя deployer, который будет являться суперпользователем, этого делать не желательно, если у вас более, чем один проект, лучше под каждую бд создать отдельных пользователей с разными паролями)

sudo -u postgres psql — запускаем postgres консоль

postgres=# \password deployer — задаем пароль для нашего пользователя (имя пользователя заменить на свое) — этот пароль будет использоваться в конфигах вашего приложения (config/database.yml)

createdb $USER — создаем базу данных deployer

Ставим rbenv, ruby и gem bundler

Ставим все необходимое, чтобы склонировать репозиторий rbenv, установить последние ruby и пользоваться загрузкой картинок в наших приложениям (последние два пакета).

sudo apt-get install build-essential bison openssl libreadline6 libreadline6-dev curl git-core zlib1g zlib1g-dev libssl-dev libyaml-dev libxml2-dev libxslt1-dev autoconf libc6-dev libncurses5-dev libmagickcore-dev imagemagick

Теперь давайте перейдем к установки ruby с помощью rbenv и ruby-build. Здесь нет ничего не обычного, в основном все взято со страниц README rbenv и ruby-build.

rbenv
git clone git://github.com/sstephenson/rbenv.git ~/.rbenv
echo ‘export PATH=»$HOME/.rbenv/bin:$PATH»‘ >> ~/.bash_profile — для доступа к команде rbenv

echo ‘eval «$(rbenv init -)»‘ >> ~/.bash_profile — для доступа к бинарникам установленных гемов и автокомплиту rbenv команд
exec $SHELL — перезапускам шел

ruby-build — для простой загрузки и сборки ruby из исходников
git clone git://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build

устанвливаем ruby rbenv install 1.9.3-p392 (для этого необходимо немало времени, поэтому наберитесь терпения, на машине с 512 оперативы эта операция занимает около 7 минут)

Пока приложение настраивается можно добавить пару строк в ~/.gemrc файл, для того, чтобы не устанавливать документацию вместе с гемами

install: —no-ri —no-rdoc
update: —no-ri —no-rdoc

после этого rbenv global 1.9.3-p392 и gem install bundler

Локальное приложение для деплоя

Теперь давайте подготовим наше локальное приложение (я для
этих целей создал простое приложение rails

rails new simple_deployment -T

rails g scaffold item name description:text — для того, чтобы проверить и соединение с базой данных.

создадим файл .rbenv-version (echo ’1.9.3-p392′ > .rbenv-version) для того, чтобы unicorn запускал необходимую версию ruby

также добавим в Gemfile gem unicorn и gem capistrano (последний в группу :development)
gem 'unicorn'

group :development do
   gem 'capistrano'
end

и запустим в консоли capify . (capistrano создаст для нас файлик config/deploy.rb, который и будет «пультом управления» нашего приложения на сервере)

config/deploy.rb
require 'bundler/capistrano'
load 'deploy/assets'

set :repository, 'YOUR_GIT_OR_SVN_REPOSITORY_URL'
set :scm, :git

server 'YOUR_SERVER_IP_OR_HOSTNAME', :app, :web, :db, :primary => true

set :ssh_options, { :forward_agent => true }
default_run_options[:shell] = 'bash -l'

set :user, 'deployer'
set :group, 'staff'
set :use_sudo, false
set :rails_env, 'production'

set :project_name, 'simple_deployment'

set :deploy_to, "/home/deployer/projects/#{ project_name }"

desc "Restart of Unicorn"
task :restart, :except => { :no_release => true } do
  run "kill -s USR2 `cat /home/deployer/projects/#{ project_name }/shared/pids/unicorn.pid`"
end

desc "Start unicorn"
task :start, :except => { :no_release => true } do
  run "cd #{current_path} ; bundle exec unicorn_rails -c config/unicorn.rb -D -E #{ rails_env }"
end

desc "Stop unicorn"
task :stop, :except => { :no_release => true } do
  run "kill -s QUIT `cat /home/deployer/projects/#{ project_name }/shared/pids/unicorn.pid`"
end

after 'deploy:finalize_update', 'deploy:symlink_db'

namespace :deploy do
  desc "Symlinks the database.yml"
  task :symlink_db, :roles => :app do
    run "ln -nfs #{deploy_to}/shared/config/database.yml #{release_path}/config/database.yml"
  end
end

Проекты у нас на сервере будут жить в домашнией папке + projects, т.е. для нашего пользователя deployer это будет /home/deployer/projects.

В каждой папке приложения находятся еще три папки (releases — где хранятся все релизы нашего приложения, current — симлинк на текущий релиз из папки releases и shared, где хранится общая шняга для все релизов: пиды, идентификаторы сессий, логи и прочее.)

Также необходимо добавить ssh ключ сервера на github или bitbucket

содержимое ключа cat ~/.ssh/id_rsa.pub, если такого файла нет, то нужно его сгенерировать ssh-keygen -t rsa

и сделать с сервера ssh git@bitbucket.org, чтобы подтвердить соединение.

config/unicorn.rb — минимальный конфиг для нашего HTTP сервера.
worker_processes 2
user 'deployer', 'staff'
preload_app true
timeout 30

project_name = 'simple_deployment'

working_directory "/home/deployer/projects/#{ project_name }/current"

listen "/tmp/#{ project_name }.socket", :backlog => 64
pid "/home/deployer/projects/#{ project_name }/shared/pids/unicorn.pid"

stderr_path "/home/deployer/projects/#{ project_name }/shared/log/unicorn.stderr.log"
stdout_path "/home/deployer/projects/#{ project_name }/shared/log/unicorn.stdout.log"

Если вы указали все верно в config/deploy.rb, то можно запустить cap deploy:setup для создания capistrano необходимых для разворачивания приложения папок на стороне сервера.

Nginx

Устанвливаем nginx командой sudo apt-get install nginx и переходим к его настройке. Для начала отредактируем файл /etc/nginx/nginx.conf
user deployer staff;
worker_processes 2;

pid /var/run/nginx.pid;
events {
  worker_connections 1024;
  multi_accept on;
}

http {
  sendfile on;
  tcp_nopush on;
  tcp_nodelay off;

  include /etc/nginx/mime.types;
  default_type application/octet-stream;
  access_log /var/log/nginx/access.log;
  error_log /var/log/nginx/error.log;

  gzip on;
  gzip_disable "msie6";
  gzip_proxied any;
  gzip_min_length 500;
  gzip_types text/plain text/css application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript;

  include /etc/nginx/conf.d/*.conf;
  include /etc/nginx/sites-enabled/*;
}

Последняя строчка нужна, чтобы nginx загружал свои конфиги и из папка /etc/nginx/sites-enabled/* (это нужно для того, чтобы не хранить все конфиги для проектов в одном файле).

А теперь nginx конфиг для нашего приложения.Будем считать, что наше приложение называется simple_deployment, а домен у него simple-deployment.com (чтобы вам удобнее было заменять на свои данные).
upstream simple_deployment {
  server unix:/tmp/simple_deployment.socket fail_timeout=0;
}

server {
  server_name simple-deployment.com;
  root /home/deployer/projects/simple_deployment/current/public;
  access_log /var/log/nginx/simple_deployment_access.log;
  rewrite_log on;

  location / {
    proxy_pass http://simple_deployment;
    proxy_redirect off;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    client_max_body_size 10m;
    client_body_buffer_size 128k;

    proxy_connect_timeout 90;
    proxy_send_timeout 90;
    proxy_read_timeout 90;
    proxy_buffer_size 4k;
    proxy_buffers 4 32k;
    proxy_busy_buffers_size 64k;
    proxy_temp_file_write_size 64k;
  }

  location ~ ^/(assets|images|javascripts|stylesheets|system)/ {
    root /home/deployer/projects/simple_deployment/current/public;
    expires max;
    break;
  }
}

и удалить файлы /etc/nginx/sites-available/default и /etc/nginx/sites-enabled/default

Теперь можно смело пробовать запускать cap deploy:migrations в консоли локального приложения. Если все было выполнено по данной заметке, то вы должны получить развернутое приложения на сервере в папке /home/deployer/projects/simple_deployment, если вы конечно строго следовали инструкциям.

Запустим последную команду на сервере, запустим nginx (sudo service nginx start).

Теперь, после деплоя можно запускать команду cap start или cap stop для запуска сервера или его остановки. Надеюсь, следуя этому мануалу, у вас получилось развернуть свое приложение на сервере. Если нет, то милости прошу в комментарии, чем смогу, постараюсь помочь.

Ссылка на репозиторий — пустышку с placeholder’ами для ваших нужд. (config/unicorn.rb config/nginx.conf config/deploy.rb)

Ссылки на дополнительные материлы:

деплой вместе с bundler
конфигурация unicorn’а
Enjoy, my little colleagues icon smile Разворачиваем rails приложение вместе с capistrano

UPDATE: 06.03.2013
Немного защитим наш сервер, в первую очередь давайте запретим логин рута по ssh, у нас на это есть пользователь deployer, который может выполлнять команды от рута с помощью sudo. Для этого в файле
/etc/ssh/sshd_config

необходимо изменить
PermitRootLogin на no и PasswordAuthentication на no, последняя настройка запретит авторизацию для SSH по паролю (только по ключу).

Добавлено: 21 Сентября 2013 09:10:38 Добавил: Андрей Ковальчук

Оповещение после выполнения «тяжелой» фоновой задачи с помощью faye и PrivatePub

Допустим нам необоходимо после выполнения большой задачи в фоновом режиме сообщить об этом пользователю и совершить что-нибудь, например, показать ему какой-нибудь popup.
Для этого по-старинке, можно с интервалом, скажем в 1 секунду опрашивать сервер и смотреть не завершилась ли наша задача, но в пору HTML5 делать это, по крайней мере, не престижно. Будем использовать для этих целей инструмент по обмену сообщениями между сервером и клиентом Faye.

Инструменты

Нам понадобится Rails приложение, к которому мы и будем привязывать всю эту функциональность. Также будем использовать Resque для выполнений фоновых задач — инструмент, зарекомендовавший себя, как надежный и стабильный помощник, спасающий всегда, когда нужно выполнить тяжелые задачи в фоне. Вместе с faye воспользуемся оберткой для него от Ryan Bates Private Pub, который мне очень облегчил жизнь, надеюсь, и облегчит вам.

Реализация

Для начала установим redis и обновим наш Gemfile, дополнив его необходимыми гемами resque, faye, private_pub, thin.

Я буду рассказать все на примере Mac OS, установка подобного инструментарий, скажем, на Ubuntu, не должна вызвать вопросов, потому что инструменты очень распространенные.

Устанавливал redis я с помощью всем известного пакетного менеджера Homebrew, напечатав в терминале всего одну коменду brew install redis

Допустим у нас имеется какой-нибудь тяжелый объект с несколькими картинками, которые лежат в Amazon S3 и, чтобы создать копию этого объекта нам понадобится скачать все эти картинки, чтобы вновь их туда загрузить, привязав в новому объекту. Не спрашивайте почему так сложно, так работает CarrierWave или я просто не нашел лучшего решения.

Если у нас раньше был метод в контролле, к примеру, clone, которй вызывал метод из модели, делающий всю грязную работу, то сейчас нам нужно лишь добавить новую задачу для resque, выглядеть это будет примерно так

def clone
  Resque.enqueue(CloneProfileWorker, params[:id])
end

Теперь тяжелая задача будет добавляться в очередь всякий раз, когда мы пройдем по ссылке /profiles/#{ id }/clone.

Давайте поставим и настроим PrivatePub, который будет со стороны клиента подписываться на определенные события, и со стороны сервера, после наступления определенного события (в нашем примере это, когда resque job отработает) делать нужные нам вещи.

Для этого в консоли нужно запустить
rails g private_pub:install

и добавить в файл app/assets/javascripts/application.js[.cofeee] строчку
#= require private_pub, если вы используете cofeeScript или же //= require private_pub, если js

и дальше во вьюхе /profile/clone.html.haml (я использую haml в данном проекте)
= subscribe_to "/profile_cloning_#{ params[:id] }"

:javascript
  PrivatePub.subscribe('/profile_cloning_#{ params[:id] }', function(data, channel) {
    location.href = data.url;
  });

Первая строчка это метод из гема, который инициализует объект необходимыми параметрами из файла /config/private_pub.yml, а после мы «подписываемся на событие ‘/profile_cloning_#{ params[:id] }’, где в params[:id] содержится текущий id профиля. При наступлении данного события, мы перенаправляем пользователя на страницу ‘/profile/#{ new_id }/edit», полный урл мы получим после того, как resque job отработает.

app/workers/clone_profile_worker.rb
require 'resque'

class CloneProfileWorker
  @queue = :default

  def self.perform(profile_id)
    c = Profile.find profile_id
    new_profile = c.clone_self

    if new_profile.is_a? Profile
      PrivatePub.publish_to "/profile_cloning_#{ profile_id }", :url => "/profiles/#{ new_profile.id }/edit"
    end
  end 
end

В данном воркере нет ничего магического: сначала мы клонируем наш профиль и если все прошло успешно, то оповещаем нашего клиента и передаем туда url, на который он перенаправится.

Проверяем работоспособность

Запускаем в разных окнах терминала:

rails s
redis-server /usr/local/etc/redis.conf (у меня MacOs, на других ОС должно быть нечто подобное)
VERBOSE=1 rake resque:work QUEUE=* (запускаем все очереди, устанвливаем verbose=1 для того, чтобы видеть что происходит внутри resque)
rackup private_pub.ru -s thin -E production (сервер для PrivatePub и Faye)
Проходим по ссылке наподобие /profiles/73/clone и смотрим в терминале как отрабатывает наш resque job и мы перенаправляемся на редактирование уже склонированного профиля, если все отработало без ошибок. Если же возникли ошибки, то они отобразятся в терминале, если произойдет что-то невообразимое и непредвиденное, пишите в комментариях, я попробую помочь вам.

Добавлено: 21 Сентября 2013 09:06:28 Добавил: Андрей Ковальчук

Как послать почту с учетной записи Google из Ruby on Rails

В интернете мне попадалось несколько статей, в которых объясняется, как посылать почту с учетной записи Google из приложения под Ruby on Rails. Однако, некоторые из них не работают, а некоторые объяснены не до конца. Поэтому я хочу поделиться работающим примером, которым я пользовался в различных проектах.

Основная проблема с Gmail (или адресом, подключенным к Google Apps) заключается в том, что для работы с ними требуется соединение по SSL. Как это часто случается в Ruby, решение практически элементарное: вам просто нужно установить gem под названием tlsmail с помощью следующей команды:

gem install tlsmail

После этого, просто пропишите настройки требуемого почтового ящика, например в инициализаторе mail.rb или где вам угодно, не забывая добавить в начало строчку:
require 'tlsmail'

И выставляя параметр :tls в значение true.

Вот полный кусок кода:
# Кофигурация почты только для режима production
if RAILS_ENV == 'production'
  require 'tlsmail'
  Net::SMTP.enable_tls(OpenSSL::SSL::VERIFY_NONE)
  ActionMailer::Base.delivery_method = :smtp
  ActionMailer::Base.perform_deliveries = true
  ActionMailer::Base.default_charset = "utf-8"
  ActionMailer::Base.raise_delivery_errors = true
  ActionMailer::Base.smtp_settings = {
  :domain          => "your-domain.com",
  :address         => 'smtp.gmail.com',
  :port            => 587,
  :tls             => true,
  :authentication  => :plain,
  :user_name       => 'address@your-domain.com',
  :password        => 'your-password'
}
end

Видите? С помощью gem'а tlsmail настройка почты Google стала очень простой.

Добавлено: 04 Сентября 2013 02:01:27 Добавил: Андрей Ковальчук

Динамический контекст Rspec - не заставляйте себя повторяться

Преимущества rspec позволяют использовать эффективную технику для проведения тестов.
Теперь вы можете создавать и определять контекст, используя более гибкий подход, чем ранее.
У нас было меньше проблем с предметом (субъектом) и поведением. А тестирование
проводится как раз ради контекста. Из-за того, что веб-приложения работают с данными, их
поведение в значительной степени зависит от базы данных.

Хорошо организованный контекст создает реальную проблему при тестировании. в отличие от
TestUnit, примеры Rspec могут включать в себя сгруппированный контекст и даже динамический
контекст с элементом let.
Не смотря на отложенную инициализацию блоков let, они определяются перед каждым
добавочным блоком. Вы поймете это, глянув на пример.

В данном примере заказы должны быть доставлены сразу после формирования в
подтвержденную ученую запись, и не быть доставлены в неподтвержденную учетную запись.

describe Order do
  context "after create" do #defining a partial context
    before(:each) do
      subject.customer.confirmed = confirmed
      subject.save!
    end
 
    context "when customer is confirmed" do 
      let(:confirmed) { true }
      it { should be_delivered }
    end
    
    context "when customer is not confirmed" do 
      let(:confirmed) { false }
      it { should_not be_delivered }
    end
  end
end

В этом примере вы можете видеть частичное определение контекста и особое поведение в двух вложенных контекстах. Мы можем вызывать необъявленную функцию и определить ее позже, и так по-
разному в разных контекстах.

Другой пример, т.е. сопоставление с эталоном (термин эрланга), разработан для тестирования
служебных функций.
Полагаю, мы имеем функцию интерпретации булевого выражения:
describe Expression
  describe ".run" do
 
    subject { Expression.run(arg) }
 
    context "with '&' where both true" do
      let(:arg) { "true & true" }
      it {should be_true}
    end
 
    context "with '&' where one false" do
      let(:arg) { "false & true" }
      it {should be_false}
    end
    ........
  end
end

Отличная стратегия - запускать одну и ту же функцию с разными аргументами.

Rspec в значительной степени превосходит все существующие фреймворки для
автотестирования приложений. В отличие от других клонов Rspec (в том числе для других языков
программирования), авторы Rpsec глубоко вникли в проблемы тестирования и изобрели гибкий и
элегантный синтаксис.

Добавлено: 04 Сентября 2013 02:00:30 Добавил: Андрей Ковальчук

Новый синтаксис хешей в Руби

Начиная с Руби версии 1.9, появился новый синтаксис для хешей. Если раньше мы писали нечто вот такое:

{:key => 'value', :key2 => 'value2'}

Сейчас это будет выглядеть вот так:
{key: 'value', key2: 'value2'}

Мне, лично, такое обновление очень нравится и я стараюсь использовать новый синтаксиис всегда, когда работаю с хешами в своих проектах на Руби. Некоторые проекты, над которыми я работаю в данный момент, уже имеют большое количество кода и в них синтаксис хешей все еще написан по старому. Решение, к которому я пришел, было написать конвертер, который заменит все места в которых используется старый способ работы с хешем. Таким образом, родился джем syntax_fix, который доступен на Github https://github.com/HeeL/syntax_fix. Релиз джема доступен на RubyGems https://rubygems.org/gems/syntax_fix.
Для установки джема просто запустите:
gem install syntax_fix

После успешной установки, вам будет доступна команда "syntax_fix". Вы можете сразу запустить данную команду без параметров с корневой директории вашего проекта, в результате синтаксис во всех файлах будет изменен на новый (включая вложенные файлы и директории). Вы можете воспользоваться опцией -v или --verbose чтобы скрипт был более информативным и выводил в консоли имена файлов, в которых были сделаны изменения.

Вот впринципе и все, ничего сложного. Изначально, идея была сделать джемку которая будет конвертировать не только хеши, но и лямбды, кейсы и т.д.

Добавлено: 04 Сентября 2013 01:28:20 Добавил: Андрей Ковальчук

Tips&Tricks: Превращение строки в константу

Недавно в статье о Переменных и константах в Ruby я упомянул о том, что из строки можно получить константу используя Kernel.const_get, на что получил в комментариях целых два замечания. Первое касалось того, что следует использовать Object.const_get, вместо Kernel.const_get, а второе касалось того, что нужно учитывать неймспейсы, а потому получение констант из строки несколько сложнее реализовать. Та статья была о другом, потому, я решил добавить отдельную статью в Tips & Tricks о том, как правильно получать из строки константу.

Код выполняющий это действие я решил поместить в метод #constantize, метод называется так же, как аналогичный метод в Rails, и создать для него псевдоним #to_const. Собственно вот его код:

class String
  def constantize
    constants = self.split("::")
    main = Object.const_get(constants.shift)
    constants.inject(main) { |full, nested| full = full.const_get(nested) }
  end
 
  alias :to_const :constantize
end

Пример работы:
module SuperExtensions
  module MySuperExtension
    class Extension
      def self.hello
        puts "Hello!"
      end
    end
  end
end
 
klass = "SuperExtensions::MySuperExtension::Extension".to_const
klass #=> SuperExtensions::MySuperExtension::Extension
klass.hello # Hello!

Вот и все! Удачи!

Добавлено: 25 Августа 2013 02:35:41 Добавил: Андрей Ковальчук

Примеры реализации сортировки пузырьком

Ассемблер
Синтаксис Intel

    mov bx, offset array
    mov cx, n
for_i:
    dec cx
    xor dx, dx
for_j:
    cmp dx, cx
    jae exit_for_j
    jbe no_swap
    mov ah, byte ptr bx[di]
    mov byte ptr bx[di], al
    mov byte ptr bx[si], ah
no_swap:
    inc dx
    jmp for_j
exit_for_j:
    loop    for_i


Синтаксис AT&T (GNU)

.text
# void bubble_sort (unsigned *array, unsigned length);
.globl bubble_sort
    .type   bubble_sort, @function
bubble_sort:
    mov 8(%esp), %ecx # unsigned length
    cmp $1, %ecx
    jbe exit
    mov 4(%esp), %eax # unsigned *array
    dec %ecx
for_ecx:
    xor %edi, %edi
for_edi:
    mov (%eax,%edi,4), %ebx
    cmp %ebx, 4(%eax,%edi,4)
    jae  no_xchng
    mov 4(%eax,%edi,4), %edx
    mov %edx, (%eax,%edi,4)
    mov %ebx, 4(%eax,%edi,4)
no_xchng:
    inc %edi
    cmp %edi, %ecx
    jne for_edi
    loop for_ecx
exit:
    ret


C

#define SWAP(A, B) { int t = A; A = B; B = t; }
 
void bubblesort(int *a, int n)
{
  int i, j;
 
  for (i = n - 1; i > 0; i--)
  {
    for (j = 0; j < i; j++)
    {
      if (a[j] > a[j + 1]) 
        SWAP( a[j], a[j + 1] );
    }
  }
}


C++
С использованием Template

#include <algorithm>
template< typename Iterator >
void bubble_sort( Iterator First, Iterator Last )
{
    while( First < --Last )
        for( Iterator i = First; i < Last; ++i )
            if ( *(i + 1) < *i )
                std::iter_swap( i, i + 1 );
}

Без использования Template

void bubble(int*a, int n)
{
for (int i=n-1;i>0;i--)
  {
    for (int j=0;j<i;j++)
      {
        if(a[j]>a[j+1])
          {
            int tmp=a[j];
            a[j]=a[j+1];
            a[j+1]=tmp;
          }
     }
  }
}


C#

void BubbleSort(ref int[] A)
{
   int temp;
 
   for(int i = 0; i < A.Length - 1; i++)
   {
      for(int j = 0; j < A.Length - i - 1; j++)
      {
         if(A[j] > A[j + 1])
         {
            temp = A[j];
            A[j]=A[j+1];
            A[j + 1] = temp;
         }
      }
   }
}


Delphi

Сортировка одномерного динамического целочисленного массива:

type
 TIntVec = array of Integer;
...
procedure BubbleSort(var a: TIntVec);
 var i,p,n: Integer; b: boolean;
begin
 n:= Length(a);
 if n < 2 then exit;
 repeat
  b:= false;
  Dec(n);
  for i:= 0 to n-1 do
   if a[i] > a[i+1] then
    begin
     p:= a[i];
     a[i]:= a[i+1];
     a[i+1]:= p;
     b:= true;
    end;
 until not b;
end;


D

void bubbleSort(alias op, T)(T[] inArray) {
    foreach (i, ref a; inArray) {
        foreach (ref b; inArray[i+1..$]) {
            if (mixin(op)) {
                auto tmp = a;
                a = b;
                b = tmp;
            }
        }
    }
}


Fortran

do i=n-1,1,-1
do j=1,i
    if (a(j).gt.a(j+1)) then
        t=a(j)
        a(j)=a(j+1)
        a(j+1)=t
    endif
enddo
enddo


Java

void swap(int[] arr, int i, int j) {
    int t = arr[i];
    arr[i] = arr[j];
    arr[j] = t;
}
 
void bubblesort(int[] arr){
    for(int i = arr.length-1 ; i > 0 ; i--){
        for(int j = 0 ; j < i ; j++){
            if( arr[j] > arr[j+1] )
               swap(arr, j, j+1);
        }
    }
}


Pascal

for i:=n-1 downto 1 do {n - размер массива M[]}
    for j:=1 to i do
        if M[j]>M[j+1] then 
            begin
               tmp:= M[j];
               M[j]:= M[j+1];
               M[j+1]:= tmp;
            end;
write('вывод значений M[]: ');
for i:=1 to n do
    write(M[i]:4);
writeln;

Усовершенствованный алгоритм сортировки пузырьком:

P:=True; {есть перестановка?}
K:=1; {Номер просмотра}
While P Do
Begin
    P:=false;
    For i:=1 To n-k Do
        If X[i] > X[i+1] Then
        Begin
            A:=X[i];
            X[i]:=X[i+1];
            X[i+1]:=A;
            P:=true;
        End;
    k:=k+1;
End;


Perl

for(@out){
  for(0..$N-1){
    if($out[$_]gt$out[$_+1]){
      ($out[$_],$out[$_+1])=($out[$_+1],$out[$_]);
    }
  }
}


Ruby

arr = [5, 20, 3, 11, 1, 17, 3, 12, 8, 10]
swap = true
size = arr.length - 1
while swap
  swap = false
  for i in 0...size
    swap |= arr[i] > arr[i + 1]
    arr[i], arr[i+1] = arr[i + 1], arr[i] if arr[i] > arr[i + 1]
  end
  size -= 1
end
puts arr.join(' ')
# output => 1 3 3 5 8 10 11 12 17 20


Python

def swap(arr, i, j):
    arr[i], arr[j] = arr[j], arr[i]
 
def bubble_sort(arr):
    i = len(arr)
    while i > 1:
       for j in xrange(i - 1):
           if arr[j] > arr[j + 1]:
               swap(arr, j, j + 1)
       i -= 1

VBA

Sub Sort(Mus() As Long)
Dim i As Long, tmp As Long, t As Boolean
t = True
Do While t
    t = False
    For i = 0 To UBound(Mus()) - 1
        If Mus(i) > Mus(i + 1) Then
            tmp = Mus(i)
            Mus(i) = Mus(i + 1)
            Mus(i + 1) = tmp
            t = True
        End If
    Next
Loop
End Sub


PHP

$size = sizeof($arr)-1;
for ($i = $size; $i>=0; $i--) {
    for ($j = 0; $j<=($i-1); $j++)
        if ($arr[$j]>$arr[$j+1]) {
            $k = $arr[$j];
            $arr[$j] = $arr[$j+1];
            $arr[$j+1] = $k;
        }
}

JavaScript

function sortBubble(data) {
    var tmp;
 
    for (var i = data.length - 1; i > 0; i--) {
        for (var j = 0; j < i; j++) {
            if (data[j] > data[j+1]) {
                tmp = data[j];
                data[j] = data[j+1];
                data[j+1] = tmp;
            }
        }
    }
    return data;
}


JavaFX

function bubbleSort(seq:Number[]):Number[]{
 
    var sort = seq;
    var elem:Number;
 
    for(n in reverse [0..sizeof seq - 2]){
        for(i in [0..n] ){
            if(sort[i+1] < sort[i] ){
                elem = sort[i];
                sort[i] = sort[i+1];
                sort[i+1] = elem;
            }
        }
    }
 
    return sort;
}

Nemerle

using System.Console;
using Nemerle.Collections;
 
def arr = array [100, 45, 2, 5, 3, 122, 3, 5, 6, 1, 3];
 
foreach (i in [0..arr.Length-1])
    foreach (j in [0..arr.Length-2])
       when (arr[j] > arr[j+1])
           (arr[j], arr[j+1]) = (arr[j+1], arr[j]);
 
WriteLine($"Sorted list is a $(arr.ToList())");


TurboBasic 1.1

CLS
RANDOMIZE TIMER
DEFINT X, Y, N, I, J, D
N = 10 ' 32 766 - 62.7 SEC
DIM Y[N], Y1[N], Y2[N], Y3[N], Y4[N] 'FRE(-1)=21440-21456
 
PRINT " ZAPOLNENIE MASSIVA ELEMENTAMI"
 
FOR X = 1 TO N
   Y[X] = X
   PRINT Y[X];
NEXT X:PRINT
 
PRINT " PEREMESHIVANIJE ELEMENTOV MASSIVA"
 
PRINT " SLUCHAINYE CHISLA"
 
FOR X = 1 TO N
   YD=Y[X]
   XS=INT(RND*N)+1
   PRINT XS;
   Y[X]=Y[XS]
   Y[XS]=YD
NEXT X:PRINT
 
PRINT " PEREMESHANNYJ MASSIV"
 
FOR X=1 TO N
   PRINT Y[X];
NEXT X:PRINT
 
'ALGORITM "SORTIROVKA PROSTYM OBMENOM" O(N^2)
MTIMER
FOR J=1 TO N-1 STEP 1
   F=0
   FOR I=1 TO N-J STEP 1
      'IF Y[I] > Y[I+1] THEN D=Y[I]:Y[I]=Y[I+1]:Y[I+1]=D:F=1
      IF Y[I] > Y[I+1] THEN SWAP Y[I],Y[I+1]:F=1
 
      LOCATE 8,1                    REM
      PRINT " ANYMACIJA SORTIROVKI" REM
      FOR X1=1 TO N                 REM ANIMATION BLOCK
         PRINT Y[X1];               REM
      NEXT X1:PRINT                 REM
      DELAY .5                      REM
 
   NEXT I
   IF F=0 THEN EXIT FOR
NEXT J
T1=MTIMER
 
PRINT " OTSORTIROVANNYJ MASSIV"
 
FOR X=1 TO N
   PRINT Y[X];
NEXT X:PRINT
PRINT "ELAPSED TIME=";T1

Добавлено: 15 Августа 2013 01:39:23 Добавил: Андрей Ковальчук