11 ноября 2009, 20:46
Темы: ruby, ruby1.9, syntax, regexp
Постановка задачи
Собрать в одном месте важные, на мой взгляд, особенности сравнений и неравенств в руби.
Основа неравенств в руби
Основным методом сравнения является <=>. Определив его, мы определяем все остальные операции, включив модуль Comparable:
class MyComp
attr :value
include Comparable
def initialize(val)
@value = val
end
def <=>(other)
@value <=> other.value
end
end
v1 = MyComp.new(1)
v2 = MyComp.new(2)
puts v1 < v2 # > true
puts v1 <= v2 # > true
puts v1 > v2 # > false
puts v1 >= v2 # > false
puts v1 == v2 # > false
Сам метод можно было бы описать как «возвращает -1, 0 или 1 в зависимости от того, меньше равен или больше объект, чей метод вызывается в сравнении с объектом переданным в качестве параметра». Но на самом деле, скорее, наоборот понятия «больше», «меньше» и «равен» определяются исходя из работы <=>.
Далее всё понятно и более ли менее очевидно для чисел, массивов и строк. Но есть и интересная особенность.
Сравнение модулей и классов
Сравнение для модулей и классов определено таким образом, что в результате мы знаем направление наследования или включение одного модуля другим:
module T1
end
module T2
include T1
end
T3 = T1
class C1
end
class C2 < C1
end
C3 = C1
puts "T1 <=> T2: #{(T1 <=> T2).inspect}" # > 1
puts "T1 <=> T3: #{(T1 <=> T3).inspect}" # > 0
puts "C1 <=> C2: #{(C1 <=> C2).inspect}" # > 1
puts "C1 <=> C3: #{(C1 <=> C3).inspect}" # > 0
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > nil
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > nil
C3.send(:include, T1)
puts "после включения"
puts "C1 <=> T1: #{(C1 <=> T1).inspect}" # > -1
puts "T1 <=> C1: #{(T1 <=> C1).inspect}" # > 1
Наследник или модуль, который включает другой модуль, меньше, чем родитель или включаемый модуль. Это видно даже из синтаксиса наследования.
Равенство
Существует три метода равенства: ==, eql?, equal?. Последний из которых никогда не следует переопределять, т.к. он отвечает за идентичность. Первые же два обычно работают одинаково. Канонический пример различия из документации:
3 == 3.0 # > true
3.eql? 3.0 # > false
Что лишь свидетельствует о том, что == проводит конвертацию чисел перед сравнением. Обычно == соответствует случаю, когда <=> возвращает 0.
Сравнение case...when
Все мы знаем, что в case...when оператор сравнения это ===. В большинстве случаев он эквивалентен равенству из предыдущего параграфа. Но если равенство симметрично
(a.==(b)) == (b.==(a))
И если это не так, то это можно считать ошибкой. То === вовсе не обязано таковым быть. Нужно помнить, что в конструкции case...when вызывается метод сравнения объекта, стоящего после when, а в качестве параметра ему передаётся объект, стоящий после case:
puts String === "строка" # > true
puts "строка" === String # > false
puts /ок/ === "строка" # > true
puts "строка" === /ок/ # > false
puts (1..10) === 5 # > true
puts 5 === (1..10) # > false
Материалы для самостоятельного изучения
- Полный код статьи на github
- Что нужно помнить, создавая свой объект руби
16 мая 2009, 00:10
Темы: actionscript, regexp, flex, flash
В последнее время моя работа в текущем проекте заключается в отладке, оптимизации и отлове багов. Поэтому в самой работе практически не встречается того, о чём можно было бы здесь написать. Но на помощь пришла гугл-группа ruFlash и комьюнити молодых программистов. :)
Задача
Один из участников попросил составить выражение для удаления из текста ссылок с определенным текстом внутри целиком. Например, в выражении:
var str:String = '<a href="somelink"><i>some text</i></a> ';
str += 'More text! ';
str += '<a href="anotherlink"><b>remove me</b></a> ';
str += '<a href="yetanotherlink"><s>another text</s></a>';
Нужно удалить целиком ссылку, содержащую фразу remove me.
Понятно, что первое приходящее в голову выражение /<a.+?remove me.*?<\/a>/ захватит две первые ссылки. И «жадность» не поможет, т.к. поиск осуществляется по порядку, и, найдя первый <a, выражение не остановится до самого remove me.
Решение номер один
Поскольку к концу недели отладки и пересмотра одного и того же кода голова моя не была готова что-то изобретать, я последовал пути, предлагавшемуся одним из ответивших, слегка его доделав:
var re0:RegExp = /<a[^>]+>[^a]*remove me.*?<\/a>/g;
trace(str.replace(re0, "!removed!"));
Недостаток его очевиден. Хотя для приведенного примера он работает, но всё-таки, может и отказать, если встретит a между > и remove me. Например:
str += '<a href="anotherlink">eh! ah? <b>remove me</b></a> ';
Решение номер два (рекурсивное)
Поскольку к этому моменту мозг ещё не расслабился окончательно и не готов был отказаться от выбранного способа думать о задаче, второе решение, пришедшее сразу за первым, было значительно сложнее.
Оно использовало возможность подсовывать функцию в качестве аргумента. Вот оно:
var re1:RegExp = /<a(.+?remove me.*?<\/a>)/g;
var replacer1:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("<a") > 0) {
return "<a" + s.replace(re1, replacer1);
} else {
return "!removed!";
}
}
trace(str.replace(re1, replacer1));
Здесь речь идёт о том, чтобы в группе (см. скобки), следующей после <a проверять наличие ещё одного <a. И в случае его наличия запускать ту же процедуру замены, но уже на группе.
Довольный собой, я запостил своё решение в ruFlash и поехал домой. По дороге домой мозг окончательно расслабился, и я смог увидеть задачу в отрыве от способа думать, который я выбрал изначально. И мне пришло
Решение номер три
Зачем городить рекурсию, когда можно просто перебирать все ссылки и заменять (удалять) только те, что нужно?
var re2:RegExp = /<a[^>]+>(.+?)<\/a>/g;
var replacer2:Function = function():String {
var s:String = arguments[1].toString();
if (s.indexOf("remove me") > 0) {
return "!removed!";
} else {
return arguments[0];
}
}
trace(str.replace(re2, replacer2));
Это ли не чудесно?
Выводы
- Решайте задачи.
- Решив (или не решив), записывайте то, что получилось, покажите кому-нибудь. Это позволит выкинуть решение из головы.
- Если есть решение лучше, то оно придет на освободившееся место.
07 мая 2009, 23:45
Темы: actionscript, regexp, flex
Введение
Я снова вернулся к работе над флэшовым проектом. Поэтому немного об ActionScript. Описанная здесь задача сейчас мне не кажется такой сложной, какой она казалась, когда я впервые с ней столкнулся. Но тем не менее.
Задача
Имеется строка, содержащая разметку для замены её составляющих. Одна из разметок: #[some_url] должна быть заменена содержимым этого самого some_url. Для замены с помощью регулярных выражений в ActionScript 3 существует функция String#replace. Но всё, что связано с загрузкой из внешних источников, создает асинхронность. А любая попытка остановить код, сделать паузу, приводит к огромной потере производительности и ошибкам, которые генерирует плеер, когда долго не может завершить вызов. «Как быть?» спросит меня пытливый читатель.
Решение
Решение состоит в том, чтобы заменять все вхождения ключевого выражения по-очереди и когда всё заменено, создавать событие.
Приведу основную часть. Файл AsyncStringReplaceExample.as
package {
import flash.events.Event;
import flash.net.URLLoader;
import flash.net.URLRequest;
public class AsyncStringReplaceExample {
public static const RE_URL:RegExp = /#\[([^\]]+)\]/g;
private var _str:String;
private var _currentExpr:String;
private var _ldr:URLLoader;
private var _loaded:Boolean;
public function AsyncStringReplaceExample(str:String) {
_str = str;
_ldr = new URLLoader();
_ldr.addEventListener(Event.COMPLETE, ldrCompleteHandler);
_loaded = false;
}
public function replace():Boolean {
_loaded = true;
_str = _str.replace(RE_URL, replaceURL);
return _loaded;
}
public function get string():String {
return _str;
}
private function replaceURL():String {
if (!_currentExpr) {
_loaded = false;
_currentExpr = arguments[0];
_ldr.load(new URLRequest(arguments[1]));
}
return arguments[0];
}
private function ldrCompleteHandler(evt:Event):void {
_str = _str.replace(_currentExpr, evt.target.data);
_currentExpr = null;
if (replace()) {
trace(_str); // здесь желанное событие
}
}
}
}
Теперь остается только использовать написанное нами богатство:
var str:String = "строка для примера\n";
str += "добавим: #[http://some.url/file.txt] или";
str += "ещё добавим: #[http://another.url/another/file.txt]! хватит?";
var asyncString:AsyncStringReplaceExample = new AsyncStringReplaceExample(str);
if (asyncString.replace()) {
trace(asyncString.string); //не нужно ничего заменять
} else {
asyncString.addEventListener(......)
}
...
// внутри обрабочика событий
trace(asyncString.string);
Выводы
Сразу видно, что последнее время я забросил ActionScript и занимался больше Ruby. Потому что подсветка синтаксиса в Ruby красивее. Но это не страшно. :)
Упражнения
Мне было бы интересно написать функцию, которая могла бы работать сколь угодно долго. Например, рекурсивную, или долгую по любой другой причине. Но ActionScript принудительно завершает процессы, которые долго не подают признаков жизни.
Как быть?
14 апреля 2009, 16:30
Темы: ruby, rack, regexp, xml
Задача
Для фильтрации rss-потоков сужествует множество инструментов. Для своей задачи мне захотелось написать простейшее решение и заодно попробовать пару новых инструментов.
Надо: собрать воедино несколько единообразных rss-потоков, отфильтровав только нужное, и выдать единый rss-поток.
Для удобства предположим, что потоки имеют одинаковый формат atom. Адреса нужных нам потоков будут находиться в текстовом файле, разделенные переносом строки. Так же как и необходимые нам ключевые слова. Так же допустим, что наличие ключевых слов будем отслеживать в заголовках.
Ресурсы
Поскольку я собираюсь фильтровать на лету, мне не нужно ничего нигде хранить, я решил попробовать лёгкий руби-фреймворк под названием Sinatra.
А для работы с самими потоками, для получения их с их серверов используем простой и удобный инструмент HTTParty.
sudo gem i sinatra
sudo gem i httparty
Сбор и фильтрация
Создадим библиотечный файл feed_fetcher.rb:
require 'rubygems'
require 'httparty'
class FeedFetcher
include HTTParty
format :xml # позволяет получть результат сразу расфасованный
# в Hash
def self.get_items
urls = nil # будет массив адресов
titles = nil # будет массив нужных частей заголовков
items = [] # будет массив записей
File.open("path_to_feed_urls_file") do |f|
urls = f.readlines.each(&:strip!)
end
File.open("path_to_titles_file") do |f|
titles = f.readlines.each(&:strip!)
end
# составим единое регулярное выражение для фильтрации
retitles = Regexp.union(titles.reject(&:empty?).map { |t| %r{\b#{Regexp.escape(t)}\b}i })
# соберём записи со всех адресов в единый масив
urls.each do |u|
items += get(u)["rss"]["channel"]["item"] unless u.empty?
end
# отфильтруем по регулярному выражению и упорядочим по дате
items.select { |i| i["title"] =~ retitles }.sort do |x, y|
DateTime.parse(y["pubDate"]) <=> DateTime.parse(x["pubDate"])
end
end
end
Выдача результата
Результат будем так же выдавать в формате atom, поэтому нам понадобится builder, который, например, входит в состав active_support. Но можно установить его и отдельно.
Файл feed_filter.rb:
require 'rubygems'
require 'sinatra'
require 'active_support'
require 'feed_fetcher.rb'
get '/' do
content_type 'application/xml', :charset => 'utf-8'
@items = FeedFetcher.get_items
builder :index
end
По-умолчанию Sinatra хранит шаблоны в папке views. Файл views/index.builder:
xml.instruct!
xml.rss "version" => "2.0", "xmlns:atom" => "http://www.w3.org/2005/Atom" do
xml.channel do
xml.title "My Filtered Feed"
xml.link "http://lonelyelk.com"
xml.pubDate CGI::rfc1123_date Time.parse(@items.first["pubDate"]) if @items.any?
xml.description "Some description"
@items.each do |item|
xml.item do
item.each_pair do |key, value|
xml.tag!(key, value)
end
end
end
end
end
Запуск приложения с помощью passenger
Для запуска приложения мы будем использовать Passenger, который поддерживает не только rails, но и rack. Для этого нам понадобится создать папку public и указать к ней путь.
В установках виртуального сервера для apache:
<VirtualHost *:80>
ServerAdmin webmaster@mydomain.ru
ServerName feedfilter.mydomain.ru
DocumentRoot /path/to/feed_filter/public
...
</VirtualHost>
А в папке приложения нужно создать файл config.ru:
require 'rubygems'
require 'sinatra'
Sinatra::Application.set(:run, false)
Sinatra::Application.set(:environment, ENV['RACK_ENV'])
require 'feed_filter'
run Sinatra::Application
Вот и всё. Естественно, ещё следует написать тесты. Так же для публикации можно использовать capistrano. Но это, я думаю, всем под силу.
Материалы для изучения
Первое знакомство с фреймворком Sinatra
24 февраля 2009, 23:10
Темы: ruby, regexp, rails
Ещё один бонус, который я ожидал от этого блога, и который уже успел получить это обратная связь. После разговора с Лёшей Кукушкиным было решено модифицировать задачу из предыдущего поста так, чтобы стало удобнее.
Задача
- Ввести дополнительный тэг для кода
- Печатать код внутри тэга так, как он должен выглядеть (прошлый раз пришлось шаманить)
- Иметь возможность более ли менее безнаказанно употреблять сами тэги в тексте
Решение
Итак, для кода будем использовать тэг [code]...[/code]. Для того, чтобы не провоцировать преобразование кода там, где не нужно, будет использовать знак «/». (То есть в этом абзаце жирным на самом деле написано «/[code]...[/code]»)
Так же используем стандартный метод rails для приведения в порядок того, что находится внутри тэга [code]. Результат выглядит вот так:
def lonelyelk_format(text)
res = "<p>" + text.to_s.dup
codes = []
res.gsub!(/[^\/]\[code\]([\s\S]+?)\[\/code\]/) do |s|
codes.push(s.gsub(/(^[^\/]\[code\]|\[\/code\]$)/, ""))
"#{s[0,1]}[code#{codes.length - 1} /]"
end
res.gsub!(/\r\n?/, "\n")
res.gsub!(/\n*\[h\]\n*/, "</p><h2>")
res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>")
res.gsub!(/\n\n+/, "</p><p>")
res.gsub!(/\n(?=\[code\d+\s\/\])/, "</p><p>")
res.gsub!(/(\[code\d+\s\/\])\n/, '\1</p><p>')
res.gsub!("\n", "<br />")
res += "</p>"
res.gsub!(/<p>\[code\d+\s\/\]<\/p>/) do |s|
"<pre><code>#{h codes[s.gsub(/\D+/, '').to_i]}</code></pre>"
end
res.gsub!("<p></p>", "")
res.gsub!("/[", "[")
res
end
Итоги
По результатам могу сказать, что код ещё можно улучшать и дорабатывать для разных целей. Чем, безусловно, мне ещё предстоит заниматься. Но писать об этой задаче больше не буду. Есть много другого интересного, о чём можно поговорить.
23 февраля 2009, 17:31
Темы: ruby, regexp
С регулярными выражениями я знаком не очень хорошо. Поэтому каждый раз, когда нужно что-то сделать, приходится собираться с силами. Но зато когда это сделать удается, наступает радость и счастье.
Задача
Сделать форматирование текста для блога, чтобы:- Можно было вставлять подзаголовки;
- Текст разбивался на параграфы и просто переносы строки;
- Со вставками кода ничего не происходило;
- Было написано на ruby.
Использовать RedCloth не хотелось, а стандартное форматирование не подходило. Поэтому приступим.
Вытащить код
Для того, чтобы не делать лишних проверок, вытаскиваем код из страницы. Код находится внутри тэга <pre>. Первое, что приходит на ум, это выражение типа «<pre> слева, </pre> справа и ни одного </pre> посередине.». Но оказалось, что исключить выражение невозможно (по крайней мере, я не нашёл способа). Выражение типа
/<pre>[^(<\/pre>)]+<\/pre>/
По крайней мере в ruby, интерпретируется как «тэг <pre>, внутри которого не встревается ни "<", ни "p", ни "r"... и т.д.»
Для этого понадобится концепция «жадности». То есть:
/<pre>.+<\/pre>/
Cоответствует куску от первого «<pre>» до последнего «</pre>». А нам нужно жадное:
/<pre>.+?<\/pre>/
То есть до ближайшего.
Теперь про wild card. Оказалось, что точка не включает перенос строки. Поэтому нам понадобится что-то более дикое. Wild, wild card. На эту роль подходит [\s\S]/: пробельный символ или непробельный.
Итак, вытаскивание кусков кода выглядит так:
codes = []
res.gsub!(/<pre>[\s\S]+?<\/pre>/) do |s|
codes.push(s)
"code#{codes.length - 1}"
end
Вокруг кусков кода
Дальше задачи попроще. Приведение переноса строки к единому виду, замена выбранных выражений для заголовков на тэги заголовков, замена двух и более переносов строки на параграф. Это не представляет особых сложностей. Меня интересует, чтобы параграф кончился до кода и начался после, даже если там всего один перенос строки.
Что касается «кончился до», то тут используется lookahead (то есть операция при условии, что впереди есть что-то):
res.gsub!(/\n(?=code\d+)/, "</p><p>")
А чтобы начать параграф после куска кода, нам понадобится lookbehind (то есть операция при условии, что перед совпадением есть что-то), который в ruby не работает (по крайней мере в версии 1.8.7). поэтому здесь мы используем группы. И включим группу в результат:
res.gsub!(/(code\d+)\n/, '\1</p><p>')
Видите, вот этот \1?
Остались мелочи: вставить обратно куски кода. Убрать пустые параграфы и параграфы, окружающие куски кода. И вы видите то, что обрабатывает текст этого сообщения.
application_helper.rb:
...
def lonelyelk_format(text)
res = "<p>" + text.to_s.dup
codes = []
res.gsub!(/<pre>[\s\S]+?<\/pre>/) do |s| # вытаскиваем куски кода
codes.push(s)
"code#{codes.length - 1}"
end
res.gsub!(/\r\n?/, "\n") # приводим перево каретки к одному виду
res.gsub!(/\n*\[h\]\n*/, "</p><h2>") # заголовки начало [h]
res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>") # заголовки конец [/h]
res.gsub!(/\n\n+/, "</p><p>") # более одного переноса строки - параграф
res.gsub!(/\n(?=code\d+)/, "</p><p>") # параграф перед кодом
res.gsub!(/(code\d+)\n/, '\1</p><p>') # параграф после кода
res.gsub!("\n", "<br />") # единичный перенос строки
res.gsub!(/(<p>)?code\d+(<\/p>)?/) do |s| # вставляем код обратно
codes[s[4,1].to_i] # здесь ошибка :)
end
res.gsub!("<p></p>", "") # убираем пустые параграфы
res += "</p>"
end
...
Остается одна проблема. Нельзя написать в тексте поста выражение «сode{цифры}». Но для этого просто можно генерировать случайный маркер, которого точно нет в тексте вместо «code».
Обновление
После того, как я попытался написать данный пост, я обнаружил ещё ряд интересных особенностей поведения кода и браузера. А так же нашёл ошибку. Публикую финальный код без пояснений:
application_helper.rb:
...
def lonelyelk_format(text)
res = "<p>" + text.to_s.dup
codes = []
res.gsub!(/<pre><code>[\s\S]+?<\/code><\/pre>/) do |s|
codes.push(s.gsub(/(^<pre><code>|<\/code><\/pre>$)/, ""))
"code#{codes.length - 1}"
end
res.gsub!(/\r\n?/, "\n")
res.gsub!(/\n*\[h\]\n*/, "</p><h2>")
res.gsub!(/\n*\[\/h\]\n*/, "</h2><p>")
res.gsub!(/\n\n+/, "</p><p>")
res.gsub!(/\n(?=code\d+)/, "</p><p>")
res.gsub!(/(code\d+)\n/, '\1</p><p>')
res.gsub!("\n", "<br />")
res.gsub!(/(<p>)?code\d+(<\/p>)?/) do |s|
"<pre><code>" + codes[s.gsub(/\D/, "").to_i].to_s.gsub("<", "<").gsub(">", ">") + "</code></pre>"
end
res.gsub!("<p></p>", "")
res += "</p>"
end
...
Материалы для изучения
http://www.regular-expressions.info/
http://regexp.ru/