20090526

Twtrize — сократитель речи

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

Люди иногда сознательно сокращают слова, набирая SMS или твиты — чтобы потратить меньше денег или укоротить сообщение.

Идея возникла, когда на одном из многочисленных «сократителей URL» я увидел надпись «Shrink text». И мне пришло в голову, что вот он возьмёт, и сократит сам текст: выдаст что-нибудь вроде «shrnk txt». Конечно, сервис всего лишь заменял в тексте URL, но я подумал, что можно было бы сокращать и сам текст.

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

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

Зависимости


Программа написана на Literate Haskell (это значит, что то, что, вы сейчас читаете, и есть программа!). Используются следующие модули:
> import System.IO.UTF8 as U
> import Data.Char (toLower)
> import Text.Regex.Posix ((=~))
> import Data.Char (isPunctuation)

TODO: Я использую старый способ работать с UTF-8 (utf8-string), надо переделать под новую библиотеку text.

Алгоритм


Данная программа «сжимает» русский текст так:
I. Из слов убираются (почти) все гласные и мягкие знаки,
> filterVowels = filter (`notElem` (aVowels ++ jVowels))

Неприкосновенны гласные, которые:
I.a. являютя частью приставки «не-»
> rmVowels = map wordFilter
> where
> wordFilter ('н':'е':cs) = "не" ++ wordFilter cs

I.b. стоят в трёх- и менее -буквенных словах
>    wordFilter w = if length w <= 3
> then w

I.c. стоят в начале или конце слова
>                    else
> let (prefix,inner,ending) = splitWord w
> in prefix ++ (ajaFilter inner) ++ ending

>    splitWord s  = let p = takeWhile dontRemove s
> r = drop (length p) s
> e = reverse $ takeWhile dontRemove $ reverse r
> m = take ((length r) - (length e)) r
> dontRemove c = c `elem` vowels || isPunctuation c
> in (p,m,e)

I.d. являются комбинациями со звуком «й»: «-ою-», «-ая—» и проч.
>    ajaFilter [] = []
> ajaFilter s = let (b,m,a) = s =~ diftPat :: (String,String,String)
> diftPat = "[" ++ vowels ++ "][" ++ jVowels ++ "]"
> in (sameConsFilter b) ++ m ++ (ajaFilter a)

I.e. стоят меж двух одинаковых согласных
>    sameConsFilter [] = []
> sameConsFilter s =
> let (b,m,a) = s =~ sameConsPat :: (String,String,String)
> sameConsPat = "(["++consonants++"])[" ++ vowels ++ "]\\1"
> in (filterVowels b) ++ m ++ (sameConsFilter a)

Программа использует такой список гласных:
> vowels = aVowels ++ jVowels

где есть и простые гласные (к ним же причислен и мягкий знак)
> aVowels = "аиоуыэь"

и дифтонгообразующие (не знаю правильного термина — в общем, дающие звук «й»),
к ним же причислена и буква «й»:
> jVowels = "яйёюе"

Для некоторых правил требуется также список русских согласных:
> consonants = "бвгджзклмнпрстфхцчшщ"

II. из предложений убираются знаки препинания, кроме точек, вопросительных и восклицательных знаков
> rmSomePunctuation = filter (not . null) . map rmTrailing
> where rmTrailing = reverse . rmHead . reverse
> rmHead [] = []
> rmHead s@(c:cs) = case c `elem` rmlist of
> True -> rmHead cs
> False -> s

Список подлежащих удалению знаков препинания:
>         rmlist = ",;-—:–"

III. из текста удаляются некоторые предлоги (в телеграфном стиле)
> rmPrepositions = filter (`notElem` preps) . words
> where preps = [ "в", "во", "на", "над", "к", "от", "из"
> , "по", "под", "через" ]

IV. для пущей стилизации текст пишется в нижнем регистре
> tolower = map toLower


Использование программы


Программу можно использовать как простой unix-фильтр: он читает текст из потока stdin и печает «сжатый» текст в стандартный вывод (stdout).
> main = U.interact $ (++ "\n") . twtrize

> twtrize = unwords . filter ( not . null ) .
> rmVowels . rmSomePunctuation . rmPrepositions . tolower

Пример:
    $ printf "Гласные, а также некоторые предлоги — как, например, «на», — из \
текста удаляются, но какие-то остаются.\n" | runhaskell twtrize.lhs

глсные а ткже нектрые прдлги как нпрмр «на» ткста удляются но какие-то
остаются.


Последняя версия: исходник здесь. Лицензия: BSD-3.

20090515

Ledger — бухучёт в командной строке

Решил, что надо наводить порядок в своей жизни и деньги считать. В общем, вести домашнюю бухгалтерию. Познания в бухучёте у меня очень скромные (хотя когда-то и прослушал вводный курс), помню только, что такое двойная запись, и дебет/кредит. Когда-то, правда, пользовался GNUcash.

Кроме GNUcash в линуксе есть ещё несколько программ для учёта личных финансов: KMyMoney и Grisbi. Программы красивые, удобные, наглядные. Однако меня впечалил и заинтересовал ledger. Это бухучёт в стиле unix. В общем, для фанатов.

Идея проста: записываем все расходы/доходы в текстовый файл (файл редактируем сами, программа его не трогает), а программа всегда поможет проверить баланс и составить отчёт о текущем состоянии или по периоду. Что может быть естественней, проще, надёжней? Полный простор в организации учёта.

Отмечу, что вначале я нашёл не сам ledger, а его клон hledger, написанный на Haskell. Есть ещё и вариант написанный на Python — beancount. Какую программу выбрать — дело вкуса. Формат файла, к счастью, у них (почти) одинаковый. «Старший» ledger уже есть в репозиториях Ubuntu и Debian, но мне пока больше понравился вариант на Haskell (исходники короче и понятнее), про него и буду рассказывать.

Хотя сложного в использовании ledger вроде ничего нет, трудно начать, потому что все примеры в сети на английском и используют английскую бухгалтерскую терминологию. Для меня, чтобы разобраться, было важно понять формат в котором вести записи. Итак, файл состоит из записей, формат каждой записи
ДАТА[=ФАКТИЧЕСКАЯ ДАТА] [*|!] [(КОД)] [ОПИСАНИЕ] [ ; КОММЕНТАРИЙ ]
отступ НАЗВАНИЕ СЧЕТА хотя бы два пробела СУММА [ ; КОММЕНТАРИЙ ]
отступ НАЗВАНИЕ ДРУГОГО СЧЕТА [ хотя бы два пробела СУММА ] [ ; КОММЕНТАРИЙ ]
[другие счета, если необходимо]
Каждая запись начинается с цифры, то есть даты. Даты пишем в формате ISO, ГГГГ-ММ-ДД. Для краткости можем указать в файле
Y2009
и все последующие записи без указания года будут относится к 2009-му году.

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

СУММЫ можно писать так, как удобно, «13», «$42», «17 EUR», «121 Kb», «9 L». Ledger понимает, что разные единицы измерения нужно считать отдельно. Единицы можно указывать любые, те, которые нужно учитывать. В том числе и неденежные (и программа умеет их правильно пересчитывать, если ей дать файл с историей цен).

В каждой записи обычно два или три счета, но указывать сумму для одного из них необязательно, понятно, что изменение будет равно сумме всех других, но с противоположенным знаком:
2009-05-14
расходы:обед 123 RUB
актив:наличные ; понятно, что здесь должно быть -123 RUB
Счета можно называть как угодно. В том числе и по-русски. Можно делать подсчета, используя двоеточие в названии счёта, например, «расходы:услуги:интернет». Видимо, необходимо иметь счета как минимум пяти категорий: актив, долги, доходы, расходы и какой-то счёт для уравнивания балансов. Я назвал его «собственные», в англоязычных примерах его обычно называют «equity».

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

Составим такой файл со счетами, для начала. Пусть у нас есть текущий счёт в банке, на котором лежит 1001 рубль (единицы не будем указывать для краткости), есть что-то в кошельке, скажем 150 рублей. Пишем файл:
Y2009

05-14 начальное состояние счета в банке
актив:банк 1001
собственные:начало

05-14 начальное содержимое кошелька
актив:наличные 150
собственные:начало
Сохраняем в файл, например ~/.ledger (там его по-умолчанию ищет hledger). Смотрим, что получилось:
~$ hledger
1151 актив
1001 банк
150 наличные
-1151 собственные:начало
Программа сама догадалась подсчитать баланс виртуального счёта «актив». Всё сходится: всё, что было списано со счёта «собственные:начало» оказалось в «активе». Если что-то не сходится, программа будет страшно ругаться.

Как записывать расход-приход — см пример записи выше. Допустим, мы что-то купили и что-то получили в подарок:
05-15 * подарок
актив:наличные 100
доходы:подарки
05-15 * яблоко
расходы:покупки 9
актив:наличные
Я добавил звёздочку перед описнием проводок. Как её интерпретировать — дело хозяйское. Я помечаю ей те операции, которые уже завершены (а восклицательным знаком — те, которые запланированы). Программа потом позволяет легко отбирать проводки со звёздочкой и без.

Предположим, например, что на завтра я запланировал ответный подарок, но ещё его не сделал:
05-16 ! ответный подарок
расходы:подарки 90
актив:наличные
Теперь, если посмотреть баланс с ключиком -C, увидим только то, что завершено:
$ hledger -C bal
91 актив:наличные
-100 доходы:подарки
9 расходы:покупки
Чтобы посмотреть не баланс, а список проводок, есть команда register:
~$ hledger reg
2009/05/14 начальное состояни.. актив:банк 1001 1001
собственные:начало -1001 0
2009/05/14 начальное содержим.. актив:наличные 150 150
собственные:начало -150 0
2009/05/15 подарок актив:наличные 100 100
доходы:подарки -100 0
2009/05/15 яблоко расходы:покупки 9 9
актив:наличные -9 0
2009/05/16 ! ответный подарок расходы:подарки 90 90
актив:наличные -90 0
Отмечу, что в «сишной» версии ledger колонки здесь разъедутся. А вот в hledger-0.5 уже всё в порядке (и для 0.4 я тоже сделал патчик).

В общем, баланс (balance) и проводки (register) — два главных отчёта. Можно ограничивать отчёты по диапазону дат или периодам. Или по названиям счетов. Так, чтобы выбрать все счета со словом «подарки» в названии, используем регулярное выражение:
$ hledger reg .*подарки.*
2009/05/15 подарок доходы:подарки -100 -100
2009/05/16 ! ответный подарок расходы:подарки 90 -10
Чтобы выбрать записи по описанию, поступаем так:
$ hledger reg desc:яблоко
2009/05/15 яблоко расходы:покупки 9 9
актив:наличные -9 0
В общем, идея понятна. Для отчётов по неделям и по месяцам есть ключики -W и -M.

Бывают ещё периодические записи, но только в ledger, в hledger их пока нет. Они не означают, что со счёта что-то автоматически списывается. Проводку нужно всё равно вносить вручную, но периодические записи позволяют рассчитывать прогноз бюджета. В ledger также можно делать виртуальные записи, ими можно автоматически учитывать разные комиссии или проценты.

Вот такая программка. Думаю, не одному мне понравится. Вижу большое достоинство программы в том, что формат данных очень простой и естественный. Посмотрю теперь, будет ли её хватать для моих нужд.

Вот пример ledger-файла.

20090509

Подсветка табуляций между пробелами и концевых пробелов в Vim

Продолжаю тему вспомогательной подсветки в Vim. Прошлый раз писал как показать длинные (> 80 символов) строчки в Vim.

Другая зараза для исходного кода — концевые пробелы (trailing whitespace) и табуляции вперемежку с пробелами. Для их подсветки у меня в ~/.vimrc есть вот такие выделения:
" highlight trailing spaces
au BufNewFile,BufRead * let b:mtrailingws=matchadd('ErrorMsg', '\s\+$', -1)
" highlight tabs between spaces
au BufNewFile,BufRead * let b:mtabbeforesp=matchadd('ErrorMsg', '\v(\t+)\ze( +)', -1)
au BufNewFile,BufRead * let b:mtabaftersp=matchadd('ErrorMsg', '\v( +)\zs(\t+)', -1)
Первое включает подсветку любых пробелов на конце строки. Второе и третье подсвечивают табуляции, перед которыми или после которых есть пробелы. Понятно, что вместо * можно явно прописать типы файлов, для которых это должно работать, но единственное место, где мне это не нужно — это буферы со справкой. Поэтому у меня сделано сейчас так:
" disable matches in help buffers
au BufEnter,FileType help call clearmatches()
Выглядит это так:



Подобным же целям служат, но иначе работают, скрипт spacehi.vim и рецепт #396 (для подсветки пробелов).

20090505

Показать длинные (>80) строчки в Vim

Большинство программистов согласятся, что строчки кода должны быть короче 80 символов. Часто это просто хороший тон: читаем Linux Kernel Coding Style (80) , Style Guide for Python code (79), Good Haskell Style (79), Ruby Coding Conventions (80), Google C++ Style Guide (80)...

Практический вопрос: а как в Vim увидеть, что строка стала длиннее 80 символов? Это может быть очень полезно, если ширина окна больше 80. Простой и дубовый способ: 80| и курсор перемещается на 80-ю колонку. Однако каждую строчку так проверять неудобно.

Более элегантный выход — подсвечивать всё, что за 80-ю колонку вылазит. Сразу куча (похожих) рецептов: Highlight long lines.

Включить подсветку вручную:
:match ErrorMsg '\%>80v.\+'
Чтобы включать подсветку автоматически, каждый раз при открытии буфера, в ~/.vimrc помещаем:
:au BufWinEnter * let w:m1=matchadd('Search', '\%<81v.\%>77v', -1)
:au BufWinEnter * let w:m2=matchadd('ErrorMsg', '\%>80v.\+', -1)
Должно работать в Vim после 7.1.40. При этом последние 4 символа до 80-й колонки будут предупреждающе подсвечиваться «поиском», а все, что после 80-й — «ошибкой».

Дополнение: в комментариях предложен ещё и другой способ выделить последние 4 символа строки:
:au BufWinEnter * let w:m1=matchadd('Search', '\%>76v.*\%<81v', -1)
Тоже работает.


Получается вот так:



Про подсветку табуляций вперемежку с пробелами и концевых пробелов см. следующую заметку.

20090504

Исправление дефектов кожи и ручное удаление шума в GIMP

Александр Прокудин описал очень интересное расширение для GIMP — вейвлетный разбор. За красивым названием — мощнейший инструмент. Можно использовать для:
  • ретуши дефектов кожи,
  • избирательного ручного удаления шума,
  • выделения или удаления деталей определённого размера,
  • смягчения границ...
Принцип действия: вейвлетным преобразованием изображение разделяется на несколько слоёв, каждый с деталями разного размера (разной «длиной волны»), от слоя с мелкими деталями наверху к слоям со всё более крупными деталями внизу. Благодаря такому разбору изображения на слои можно исправить, например, мелкие детали, не затрагивая крупных.

Для установки в Ubuntu Jaunty: скачиваем исходники, устанавливаем пакет libgimp2.0-dev, распаковываем исходники во временный каталог, make, make userinstall.

Расширение появляется в меню ФильтрыОбщиеWavelet decompose.