Как правильно запрограммировать условие «по такое-то число»
Наверно, не будет преувеличением сказать, что я ни разу не видел, чтобы
Сейчас, в эпоху нейросетей, легко обобщить это наблюдение на всех программистов. Нейросети выдают как раз «усредненный» код. Так что рассмотрим фрагмент сгенерированного кода (дополнительный плюс в том, что на это некому обижаться, по крайней мере пока):
$dateFrom = $this->createDateTime($filters['dateFrom'], endOfDay: false);
if (null !== $dateFrom) {
$queryBuilder
->andWhere('log.loggedAt >= :dateFrom')
->setParameter('dateFrom', $dateFrom)
;
}
$dateTo = $this->createDateTime($filters['dateTo'], endOfDay: true);
if (null !== $dateTo) {
$queryBuilder
->andWhere('log.loggedAt <= :dateTo')
->setParameter('dateTo', $dateTo)
;
}
// ...
private function createDateTime(?string $value, bool $endOfDay): ?\DateTimeImmutable
{
if (null === $value || '' === $value) {
return null;
}
$date = \DateTimeImmutable::createFromFormat('Y-m-d', $value) ?: \DateTimeImmutable::createFromFormat(\DateTimeInterface::ATOM, $value);
if (false === $date) {
return null;
}
return $endOfDay ? $date->setTime(23, 59, 59) : $date->setTime(0, 0, 0);
}
Для простоты понимания перепишем этот код на чистом SQL для интервала, скажем, с 1 по 10 ноября:
SELECT *
FROM audit_logs
WHERE logged_at >= '2025-11-01'
AND loggged_at <= '2025-11-10 23:59:59'
Когда я вижу эти 23:59:59, у меня сразу возникает неприятное ощущение от того, насколько это неэстетичное решение. Некоторые не останавливаются на секундах, а добавляют еще и микросекунды, и время превращается в

Правильный способ состоит в том, чтобы не приписывать финальной дате последний доступный момент времени, а заменить условие «по
SELECT *
FROM audit_logs
WHERE logged_at >= '2025-11-01'
AND loggged_at < '2025-11-11'
Стоит запомнить этот шаблон использования полуоткрытых интервалов, когда нижняя граница диапазона включается в условие, а верхняя исключается. Он пригождается чаще, чем может показаться на первый взгляд.
«Вежливый» ChatGPT
Попросил тут ChatGPT решить задачу. В ходе диалога он мне написал следующее:

Чуть выше я ему написал, начав с фразы «это всё чушь, потому что…». Получил достойный ответ.
Сам диалог можете почитать по ссылке, если интересно.
Распаковка сжатых URL на сервере
Недавно я рассказывал, как использовать API браузеров для сжатия адресов страниц. В продолжение опишу, как эти сжатые адреса распаковыать на сервере на примере того же сервиса генерации картинок с формулами.
Текущая версия конфига nginx для обработки и старых несжатых URL, и новых сжатых получилась такой:
location ~ ^(?s)/(?<ext>svg|png)(?<is_base64>b?)/(?<formula>.*)$ {
gunzip on;
gzip_static always;
gzip_vary on;
gzip_proxied expired no-cache no-store private auth;
expires 1d;
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
set $compress_error 0;
set_by_lua_block $file_path {
local ext = ngx.var.ext
local is_base64 = ngx.var.is_base64
local formula = ngx.var.formula
if is_base64 == "b" then
local base64 = require "ngx.base64"
local zlib = require "zlib"
local compressed, err = base64.decode_base64url(formula)
if not compressed then
ngx.log(ngx.ERR, "base64 decode error: ", err)
ngx.var.compress_error = 1
return ""
end
local inflator = zlib.inflate(-15)
local ok, decoded_formula = pcall(inflator, compressed)
if not ok then
ngx.log(ngx.ERR, "deflate decompress error: ", decoded_formula)
ngx.var.compress_error = 1
return ""
end
formula = decoded_formula
end
formula = formula:gsub("^%s*(.-)%s*$", "%1")
local md5 = ngx.md5(formula)
return md5:sub(1, 2) .. "/" .. md5:sub(3, 4) .. "/" .. md5:sub(5) .. "." .. ext
}
if ($compress_error) {
return 400;
}
if (-f $document_root/_error/$file_path) {
return 400;
}
rewrite ^ /_cache/$file_path break;
error_page 404 = @s2_latex_renderer;
log_not_found off;
}
location @s2_latex_renderer {
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
include /etc/nginx/fastcgi.conf;
fastcgi_pass php-tex-sock;
fastcgi_param SCRIPT_FILENAME $document_root/render.php;
fastcgi_param SCRIPT_NAME /render.php;
fastcgi_cache_key "$request_method$uri"; # В $uri УРЛ после rewrite, напр. "/_cache/4d/81/658b25df7544f9e2d0cb7f4dc402.svg"
fastcgi_cache i_upmath;
fastcgi_cache_valid 200 10m;
fastcgi_cache_methods GET HEAD;
fastcgi_cache_lock on;
fastcgi_cache_lock_age 9s;
fastcgi_cache_lock_timeout 9s;
fastcgi_buffers 8 16k;
fastcgi_buffer_size 32k;
fastcgi_connect_timeout 90;
fastcgi_send_timeout 90;
fastcgi_read_timeout 300;
}
Чтобы встраивать set_by_lua_block, в Debian достаточно установить пакет nginx-extras. Для распаковки сжатого текста в этом скрипте через функции zlib также требуется установить пакет lua-zlib.
Напомню алгоритм обработки адресов картинок. Исходник формулы, например, x^2, извлекается из адреса и декодируется. Вычисляется rewrite ^ /_cache/$file_path break;), то nginx отдает его содержимое напрямую. Если файла нет, то запрос передается в error_page 404 = @s2_latex_renderer;).
Раньше вместо rewrite и error_page я использовал более современную и подходящую директиву try_files. Но она перестает работать после активации модуля gunzip. Этот модуль позволяет держать в файловом кеше только сжатые версии файлов с расширением .gz, экономя место на диске. Причем для обработки большинства запросов от нормальных браузеров, поддерживающих rewrite корректно работает с модулем gunzip, а try_files — нет, не очень понятно. Но что есть, то есть.
В конфиге есть интересный момент, связанный с кешем внутри nginx (инструкции fastcgi_cache*). Он предотвращает race condition при одновременном запросе формулы, которой нет в файловом кеше. В противном случае nginx будет передавать в
Для распаковки полученного фрагмента URL в PHP подойдет следующий код:
public static function decodeCompressedFormula(string $compressed): string
{
$base64 = strtr($compressed, '-_', '+/'); // URL-safe base64 to standard
$compressed = base64_decode($base64);
$result = @gzinflate($compressed);
if ($result === false) {
throw new \RuntimeException('Failed to decompress formula.');
}
return $result;
}
В PHP весь алгоритм уместился в три строки — как минимум в несколько раз короче, чем в JS и nginx/lua. Такими моментами PHP радует меня до сих пор.
Cайту 20 лет
Ровно 20 лет назад этот сайт появился в интернете. Весомый повод заняться рефлексией. Но с этой заметкой я дотянул до последнего, так что буду краток :)

20 лет интеллектуальных достижений с точки зрения ChatGPT.
Зачем нужен свой сайт
Возможно, многие в 2005 году сочли бы такую затею излишне амбициозной. Действительно, кому интересны твои рассказы, мысли и истории, если есть сайт Экслера, тысячники в Живом журнале и множество тематических форумов. Однако у меня было ощущение, что мне тоже есть что сказать, и достаточно энтузиазма, чтобы воплотить идею в жизнь.
Сейчас я нахожу некоторые старые заметки наивными, некоторые — вымученными и неинтересными. Но в других заметках зафиксированы события и мысли, о которых я уже и забыл. И один только этот факт уже оправдывает все усилия, которые я потратил на сайт.
Кроме того, всё еще имеют силу стандартные аргументы о пользе написания текстов для структурирования и закрепления знаний. Так что молодым читателям могу порекомендовать как минимум вести дневник. Через
Что изменилось в интернете
За эти 20 лет веб преобразовался под влиянием корпораций. С одной стороны, социальные сети упростили распространенные пользовательские сценарии, переманили существующих пользователей и привлекли новых.
С другой стороны, от этого сильно пострадала распределенность и открытость веба. Крупные игроки перестали поддерживать открытые технологии вроде RSS и OpenID. Вместо них теперь алгоритмические подборки внутри сервисов и несовместимые проприетарные API.
Гугл в своем браузере вообще урезал рефереры до домена якобы из соображений приватности, и теперь непонятно, на какой именно странице
В этой ситуации сложно придумать
Технологии
Сайт начинался с отдельных
Наверно, вместо самописного движка непонятного качества можно было найти готовую CMS. Но вордпресс мне не нравился. Typo3 показался слишком монструозным. В общем, получилось как у Дональда Кнута, который разработал собственную систему компьютерного набора TeX для издания многотомника «Искусство программирования».
За последние несколько лет я практически полностью переписал устаревший код движка. Тяжелее всего было бы довести до ума админку, потому что её интерфейс был «одностраничным приложением» на лапшеобразном коде из jQuery, который нельзя небольшими шагами довести до нормального состояния. Поэтому я решил выкинуть этот код и написать свою библиотеку для создания административных интерфейсов под названием AdminYard. Статью о ней я уже публиковал на хабре.
В целом, даже если движком S2 в изменившемся интернете никто кроме меня не пользуется, отдельные его части обрели свою жизнь. Так, поисковый движок Rose, благодаря развитию которого я сделал систему рекомендаций на этом сайте, набрал 120 звезд на гитхабе. А редактор математических текстов Upmath собрал 350 звезд и даже несколько донатов с марта этого года.
Вместо вывода
На удивление самой посещаемой страницей на всём сайте оказалась заметка о делении окружности на 5 частей. По запросу «как поделить окружность на 5 частей» она до сих пор на первом месте в гугле.
Самой комментируемой была статья о том, что такое суперсимметрия. Сейчас на ней 115 опубликованных комментариев и еще около 50 скрытых. Суперсимметрию, кстати, так и не нашли на большом адронном коллайдере.
Также совсем недавно наступил момент, когда часть жизни, когда у меня есть сайт, оказалась длиннее той, когда сайта еще не существовало.
Что будет дальше? Посмотрим через 20 лет.
Если вы
Боги, созданные человеком
Помните,
Доказательство №1
Современной науке неизвестны сущности, столь же близкие к Всеведенью, как близка к этому Google. Она проиндексировала более 9,5 миллиардов
веб-страниц, что больше чем у любой другой поисковой машины в сети на сегодняшний день. Google — это не только самая близкая к Всеведенью сущность. Она ещё и сортирует свои обширные знания (используя технологию PageRank, Ею же запатентованную) , структурирует сей информационный океан, делая его легкодоступным для нас, простых смертных.Доказательство №2
Google вездесуща (т. е. находится везде). Фактически Google находится одновременно по всей Земле. Миллиарды проиндексированных
веб-страниц доступны из любой, даже самой удалённой точки Земли. С ростом сетейWi-Fi любой желающий сможет получить доступ к Googleпо-настоящему отовсюду, что сделает Её истинно вездесущей.Доказательство №3
Google отвечает на молитвы. Можно молиться Google о любом, даже самом пустяковом вопросе или проблеме. Например, вы можете легко найти информацию об альтернативных методах лечения рака, о способах улучшения вашего здоровья, о новых и инновационных медицинских технологиях, и вообще обо всем, о чём вопрошают на типичной молитве. Спросите Google, и Она покажет вам путь. Но это всё, что Она может сделать для вас, ибо всё остальное — в ваших руках.
Доказательство №4
Google потенциально бессмертна. Её нельзя считать физической сущностью, такой как мы с вами. Её Алгоритмы распределены по многим серверам; если один из них упадёт или поломается, другой несомненно займёт его место. Google, теоретически, будет существовать вечно.
Доказательство №5
Google бесконечна. Интернет, теоретически, может расти вечно, и Google будет вечно индексировать сей бесконечный рост.
Доказательство №6
Google помнит всё. Google регулярно кэширует
веб-страницы и хранит их на своих безграничных серверах. Фактически, загрузив ваши мысли и представления в Интернет, вы будете жить вечно в кэше Google («загробнаяGoogle-жизнь») , даже после того, как умрёте.Доказательство №7
Google — Всеблагая (не совершающая никакого зла) . Часть корпоративной философии Google — вера в то, что компания может делать деньги, не делая зла.
Доказательство №8
Согласно Google Trends, термин «Google» ищется чаще, чем «Бог» , «Иисус» , «Аллах» , «Будда» , «христианство» , «ислам» , «буддизм» и «иудаизм» вместе взятые. Считается, что бог — сущность, к которой мы, смертные, можем обратиться при первой же необходимости. Google соответствует этому в значительно большей степени, чем традиционные «боги» .
Доказательство №9
Google существует, и этому есть великое множество свидетельств. Их значительно больше, чем у любого другого сегодняшнего «бога» . Экстраординарные заявления требуют экстраординарных фактов. Так узри же их своими глазами: приди к Google и познай всю безграничную мощь Google. От тебя не требуется ни капли веры.
Нативное gzip-сжатие в JS
Я недавно закрыл тикет на гитхабе, который висел с 2017 года. Его автор обращал внимание на длинные адреса картинок в моем сервисе математических формул. С 2023 года нативное сжатие произвольных данных в JS стало доступным во всех основных браузерах, и с его помощью я сделал вариант сжатых адресов.
Давние читатели вспомнят, что я уже рассказывал об адресах картинок, и даже упоминал об этом тикете. Повторю, что для использования в вебе формулы, например, $$a^2+b^2=c^2$$, её исходник на латехе a^2+b^2=c^2 кодируется через проценты (RFC 3986) и подставляется в URL:
//i.upmath.me/svg/a%5E2%2Bb%5E2%3Dc%5E2
Кодирование через символы процента очень неэкономное, поэтому и без того длинный код изображений и диаграмм становится ещё больше. Адрес $$a^2+b^2=c^2$$ в новой схеме выглядит так:
//i.upmath.me/svgb/S4wz0k6KM7JNjjMCAA
Здесь вместо кодирования через проценты используется сжатие deflate (тот же алгоритм, что и в gzip) и кодировка, аналогичная base64. Вот рабочий пример кода, который делает такое преобразование:
function deflateRaw(text, callback) {
if (typeof CompressionStream === 'undefined') {
callback(null);
return;
}
try {
var stream = new Blob([text]).stream();
var compressedStream = stream.pipeThrough(new CompressionStream('deflate-raw'));
new Response(compressedStream).blob().then(function (compressedBlob) {
return compressedBlob.arrayBuffer();
}).then(function (buffer) {
var compressedArray = new Uint8Array(buffer);
var binary = Array.from(compressedArray).map(function (b) {
return String.fromCharCode(b);
}).join('');
var base64 = btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
callback(base64);
}).catch(function () {
callback(null);
});
} catch (e) {
callback(null);
}
}
function getImgPath(formula, callback) {
var fallbackUrl = '//i.upmath.me/svg/' + encodeURIComponent(formula);
deflateRaw(formula, function (compressed) {
var shortUrl = compressed ? '//i.upmath.me/svgb/' + compressed : null;
callback(shortUrl && shortUrl.length < fallbackUrl.length ? shortUrl : fallbackUrl);
});
}
Важная особенность API браузеров по сжатию заключается в его асинхронности. Мы не можем получить результат сжатия в той же функции, в которой его инициируем. API возвращает promise, который «разрешится» позднее. Чтобы обеспечить обратную совместимость и откатываться к несжатым адресам в старых браузерах, я проверяю саму поддержку CompressionStream и перехватываю возможные исключения. Также для обратной совместимости результат возвращаю через вызов пользовательского коллбэка, а не в виде промиса.
Вот пример того, как с вышеприведенным кодом создать картинку с формулой:
var node = document.createElement('img');
getImgPath('a^2+b^2=c^2', function(path) {
node.setAttribute('src', path);
});
Стоит отметить, что сам алгоритм сжатия deflate был давно портирован на JS, поэтому при необходимости можно было использовать сторонние библиотеки, например, pako. Кроме того, код библиотек работает синхронно, так что ни о каких коллбэках и промисах думать не нужно. В моём же случае я не хотел в
Оценим результат на примере диаграммы из предыдущей заметки. Длина старого несжатого URL равна 6,3 килобайт, а сжатого — 1,3 килобайт, что почти в 5 раз короче.
Разбираем конечный автомат в системе личных сообщений
В прошлый раз я рассказывал о применении понятия конечного автомата в программировании. В этот раз рассмотрим практический пример.
Одним из первых моих заданий в команде форума PunBB была система обмена личными сообщениями, которую я проектировал и разрабатывал с нуля. На примере этой системы посмотрим, как требования к системе преобразуются в набор состояний и переходов между ними.
Требование №1: черновики и уведомления о прочтении
Когда один пользователь отправляет другому сообщения внутри
$$\usetikzlibrary{arrows} \begin{tikzpicture}[node distance=4cm,font=\sffamily] \tikzset{ mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.7em, text width=7em,text centered}, myarrow/.style={->, >=latex', shorten >=1pt, shorten <=2pt,thick,font=\small\sffamily} } \node[mynode,fill=gray!10] (Draft) {Черновик\\status=draft}; \node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent}; \node[mynode, right of=Sent,fill=green!10] (Read) {Прочитано\\status=read}; \draw[myarrow] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель нажал\\«Отправить»} (Sent); \draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщение} (Read); \end{tikzpicture}$$
Требование №2: отзыв отправленных сообщений
Кроме очевидной функциональности мы решили добавить уникальную по тем временам фичу — отзыв сообщения. Если получатель не заходил на форум и не имел возможности узнать о том, что ему пришло сообщение, отправитель мог бесследно отозвать это сообщение, даже если после отправки прошло много времени.
Чтобы дать отправителю возможность отзывать сообщения, нужно не только разрешить обратный переход из «отправлено» в «черновик», но и добавить дополнительный статус «доставлено». Переход к нему происходит в тот момент, когда отправитель может узнать о том, что ему пришло новое сообщение. (В зависимости от настройки форума это либо посещение любой страницы, если количество непрочитанных сообщений отображается в меню, либо переход непосредственно ко входящим сообщениям.)
$$\usetikzlibrary{arrows} \begin{tikzpicture}[node distance=4cm,font=\sffamily] \tikzset{ mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.7em, text width=7em,text centered}, myarrow/.style={->, >=latex', shorten >=1pt, shorten <=2pt,thick,font=\small\sffamily} } \node[mynode,fill=gray!10] (Draft) {Черновик\\status=draft}; \node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent}; \node[mynode, right of=Sent,fill=green!10] (Delivered) {Доставлено\\status=delivered}; \node[mynode, right of=Delivered,fill=green!10] (Read) {Прочитано\\status=read}; \draw[myarrow] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель нажал\\«Отправить»} (Sent); \draw[myarrow] (Sent) to[in=-50,out=-130] node[below,align=center] {Отправитель нажал\\«В черновики»} (Draft); \draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель увидел\\ кол-во сообщений} (Delivered); \draw[myarrow] (Delivered) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщение} (Read); \end{tikzpicture}$$
Если бы требовалось сделать отправку уведомлений о новых личных сообщениях по электронной почте, пришлось бы делать переход «отправлено» → «доставлено» в момент отправки письма, так как мы уже не контролируем процесс доставки этого письма и не знаем, когда оно будет прочитано.
Требование №3: удаление сообщений
По требованиям и получатель, и отправитель могут удалять сообщения. В свое время я сделал два флага: deleted_by_sender и deleted_by_receiver. Они нужны для того, чтобы знать, от кого из участников уже надо скрыть сообщение, а от ещё кого нет. Если же они оба удаляют сообщение, то оно удаляется из базы данных целиком. (Здесь важно не повторить мою ошибку и устанавливать флаги в транзакции после блокировки строк, иначе при одновременном удалении отправителем и получателем получим оба установленных флага вместо полного удаления сообщения.)
С одной стороны флаги deleted_by_sender и deleted_by_receiver выглядят красиво и симметрично. Но с другой стороны, если вы внимательно читали предыдущий пост, то уже догадались, что набор из трех полей (status, deleted_by_sender и deleted_by_receiver) — не самое лучшее решение для кодирования состояния.
Первая проблема этих флагов и поля состояния заключается в том, что некоторые наборы значений (например, status = draft и deleted_by_receiver = 1) не соответствуют ни одному допустимому состоянию (черновик не может иметь отметку об удалении получателем, потому что получатель ничего не получал). Вторая проблема проявляется в повторении в коде одних и тех же условий. Так, условие deleted_by_receiver = 0 AND (status = 'delivered' OR status = 'read'), которое соответствует доступным получателю сообщениям, повторяется в коде три раза.
Что же делать с признаками удаления сообщений? На диаграмме состояний видно, что статусами управляет сначала отправитель, а потом получатель. Также получатель может удалить сообщение только после его доставки. Поэтому состояние «удалено получателем» вполне естественно вписывается в имеющийся набор состояний:
$$\usetikzlibrary{arrows} \begin{tikzpicture}[node distance=3.6cm,font=\sffamily] \tikzset{ mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.5em, text width=7em,text centered}, myarrow/.style={->, >=latex', shorten >=1pt, shorten <=2pt,thick,font=\small\sffamily} } \node[mynode,fill=gray!10] (Draft) {\shortstack{Черновик\\status=draft}}; \node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent}; \node[mynode, right of=Sent,fill=green!10] (Delivered) {\shortstack{Доставлено\\status=delivered}}; \node[mynode, right of=Delivered,fill=green!10] (Read) {Прочитано\\status=read}; \node[mynode, right of=Read,fill=red!10] (Deleted) {Удалено\\status=deleted}; \draw[myarrow] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель нажал\\«Отправить»} (Sent); \draw[myarrow] (Sent) to[in=-50,out=-130] node[below,align=center] {Отправитель нажал\\«В черновики»} (Draft); \draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель увидел\\ кол-во сообщений} (Delivered); \draw[myarrow] (Delivered) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщение} (Read); \draw[myarrow] (Read) to[in=130,out=50] node[above,align=center] {Получатель удалил\\ сообщение} (Deleted); \end{tikzpicture}$$
К сожалению, от флага deleted_by_sender не получится избавиться так же просто. Дело в том, что удаление сообщения отправителем может произойти в любом статусе, и у этого действия будут разные последствия:
- при удалении черновика или отправленного сообщения запись удаляется из базы данных;
- при удалении доставленного или прочитанного сообщения мы скрываем его от отправителя;
- при удалении отправителем уже удаленного получателем сообщения запись также удаляется из базы данных.
С учетом этих требований диаграмма всех возможных состояний приобретает следующий вид:
$$\usetikzlibrary{arrows} \begin{tikzpicture}[node distance=3.6cm,font=\sffamily] \tikzset{ mynode/.style={rectangle,rounded corners,draw=black,thick, inner sep=0.5em, text width=7.0em,text centered}, myarrow/.style={->, >=latex', shorten >=1pt, shorten <=2pt,thick,orange!70!black,font=\small\sffamily}, sender/.style={blue!80!black} } \clip (-10,10) rectangle (20,-8); %костыль \node[mynode,fill=gray!10] (Draft) {Черновик\\status=draft}; \node[mynode,above of=Draft,dashed] (New) {В БД пусто}; \node[mynode, right of=Draft,fill=cyan!10] (Sent) {Отправлено\\status=sent}; \node[mynode, right of=Sent,fill=green!10] (Delivered) {Доставлено\\status=delivered}; \node[mynode, below of=Delivered,fill=yellow!10] (DeliveredAndDeleted) {\shortstack{Доставл. и удал.}\\status=delivered\\del\_by\_sendr=1}; \node[mynode, right of=Delivered,fill=green!10] (Read) {Прочитано\\status=read}; \node[mynode, below of=Read,fill=yellow!10] (ReadAndDeleted) {Прочит. и удал.\\status=read\\del\_by\_sendr=1}; \node[mynode, right of=Read,fill=yellow!10] (Deleted) {Удалено\\status=deleted}; \node[mynode, below of=Deleted,dashed] (Deleted2) {Удалено из БД}; \draw[myarrow,sender] (New) to[] node[left,align=right] {Отправитель\\нажал\\«В черновики»} (Draft); \draw[myarrow,sender] (New) to[out=0,in=90] node[right,pos=0.2,align=center] {Отправитель нажал\\«Отправить»} (Sent); \draw[myarrow,sender] (Draft) to[in=130,out=50] node[above,align=center] {Отправитель\\нажал\\«Отправить»} (Sent); \draw[myarrow,sender] (Sent) to[in=-50,out=-130] node[below,align=center] {Отправитель\\нажал\\«В черновики»} (Draft); \draw[myarrow] (Sent) to[in=130,out=50] node[above,align=center] {Получатель увидел\\ кол-во сообщений} (Delivered); \draw[myarrow] (Delivered) to[in=130,out=50] node[above,align=center] {Получатель открыл\\ сообщ.} (Read); \draw[myarrow] (Read) to[in=130,out=50] node[above,align=center] {Получатель удалил\\ сообщ.} (Deleted); \draw[myarrow,sender] (Delivered) to node[left,align=right] {Отправитель\\удалил\\сообщ.} (DeliveredAndDeleted); \draw[myarrow,sender] (Read) to node[left,align=right] {Отправитель\\удалил\\сообщ.} (ReadAndDeleted); \draw[myarrow,sender] (Deleted) to node[left,align=right] {Отправитель\\удалил\\сообщ.} (Deleted2); \draw[myarrow,sender] (Draft) to[in=-80,out=-90,looseness=1.2] node[below,pos=0.6] {Отправитель удалил сообщ.} (Deleted2); \draw[myarrow,sender] (Sent) to[in=-90,out=-90,looseness=1.3] node[below,pos=0.58] {Отправитель удалил сообщ.} (Deleted2); \draw[myarrow] (DeliveredAndDeleted) to[in=-130,out=-50] node[below,align=center] {Получатель открыл\\ сообщ.} (ReadAndDeleted); \draw[myarrow] (ReadAndDeleted) to[in=-130,out=-50] node[below,align=center,pos=0.4] {Получатель удалил\\ сообщ.} (Deleted2); \end{tikzpicture}$$
Я добавил на диаграмму начальное состояние, когда в базе данных ещё нет сообщения и отправитель сохраняет его впервые, а также конечное состояние, когда оба участника переписки удалили сообщение. В принципе, никто не запрещает ввести отдельные коды для состояний «доставлено получателю и удалено отправителем» и «прочитано получателем и удалено отправителем», чтобы состояние кодировалось всего одним полем status. Но я бы оставил флаг deleted_by_sender отдельно от поля status, чтобы смысл данных в таблице был интуитивно понятным.
Выводы и анализ корректности требований
Я показал, как можно анализировать
Проанализируем корректность и полноту диаграммы из нашего примера. На диаграмме отсутствует переход между доставленным и удаленным состоянием сообщения. Должен ли он существовать? Должен, если получатель может удалить сообщение из списка, не открывая его. Должна ли система отображать отправителю, что получатель, не прочитав, удалил сообщение? Если должна, то одного состояния «удалено» недостаточно, так как мы теряем информацию о прочтении удаленных сообщений. Возможно, в этом случае не стоило удалять флаг deleted_by_receiver и заменять его на status=deleted.
Аналогичными вопросами проверяется корректность всей диаграммы состояний на этапе проектирования. Их можно задавать и самому себе, и постановщику задачи для полного прояснения требований.
Сложение вращений и анимация в TikZ
В рекомендациях ютуба мне часто попадалась задачка о вращающихся окружностях. Вот её формулировка: окружность катится без проскальзывания по другой окружности втрое большего радиуса и совершает вокруг неё один оборот. Сколько оборотов при этом она совершит вокруг своего центра?
Эта задача встречалась в американском тесте абитуриентов 1982 года и примечательна тем, что среди предложенных вариантов ответов не было правильного. Сама задача, её история и связанные вопросы разобраны в этом видео:
Я вспомнил об этой задаче, потому что, наконец, разобрался как делать анимации в TikZ, и теперь вместо множества слов могу просто показать анимированные иллюстрации.
В этой задаче кажущийся ответ — три оборота — неправильный. Оборотов будет на 1 больше, чем отношение радиусов. В этом можно убедиться напрямую, просто подсчитав количество оборотов. Я сомневаюсь, что
$$\dvisvgm\definecolor{cyan}{RGB}{0, 200, 250} \shorthandoff{"} \usetikzlibrary {shapes.geometric} \usetikzlibrary{animations} \begin{tikzpicture} \def\a{1} \def\b{3} \useasboundingbox (-\b-2*\a-0.1,-\b-2*\a-0.1) rectangle (\b+2*\a+0.1,\b+2*\a+0.1); \draw[cyan,very thin] (-\b-2*\a,-\b-2*\a) grid (\b+2*\a,\b+2*\a); \node[star,star points=57, star point ratio=1.07,minimum size=6.2cm, draw,fill=white] at (0,0); \draw[purple,fill] (0:\b) circle (1pt) -- (0,0) circle (1pt) node [midway, sloped, above] {$\b$} ; \begin{scope}:rotate = {0s="0", (5*\b)s="360",repeats} \begin{scope} :rotate = {0s="0", (5*\a)s="360", origin={(\b+\a,0)}, repeats} \node [star,star points=19, star point ratio=1.2,minimum size=2.2cm, draw,fill=white] at (0:\b+\a) {}; \draw [purple,fill] (0:\b+\a) circle (1pt) -- (0:\b) circle (1pt) node [midway, sloped, above] {$\a$} ; \end{scope} \end{scope} \coordinate (A) at (1,1.5); \node [fill=white,inner sep=1pt,anchor=east,xshift=3pt,yshift=-1pt] at (A) {$\text{обороты: }\,\,\,.$}; \foreach \t in {3,2,...,0} { \node :opacity = { 3.75*(0) s="0", 3.75*(0+\t) s="0", 3.75*(0.001+\t) s="1", 3.75*(0.999+\t) s="1", 3.75*(1+\t)s="0", 3.75*(4) s="0", repeats } [anchor=east,inner sep=1pt] at (A) {$\t$}; } \foreach \t in {0,1,...,9} { \node :opacity = { 0.375*(0) s="0", 0.375*(0+\t) s="0", 0.375*(0.01+\t) s="1", 0.375*(0.99+\t) s="1", 0.375*(1+\t)s="0", 0.375*(10) s="0", repeats } [anchor=west,inner sep=1.5pt] at (A) {$\t$}; } \end{tikzpicture}$$
Конечно, задачу можно решить стандартным геометрическим подходом: рассмотреть углы между радиусами к точкам касания в начальном и текущем положениях, и приравнять длины дуг между этими точками. Но как быть, если задачу надо решить в уме? Ход рассуждений может быть следующим.
Представим оборот меньшей окружности вокруг большей как сумму двух движений. В первом движении окружности вращаются так, что их центры остаются неподвижными. В этом случае малая окружность действительно совершит три оборота, пока большая окружность совершает один оборот. Второе движение — это вращение обеих окружностей, соприкасающихся в одной точке, на ещё один оборот. При сложении двух движений обороты большой окружности оказываются разнонаправленными и компенсируются, а обороты малой окружности — однонаправленными и суммируются, то есть малая окружность совершит четыре оборота. На следующей иллюстрации этой идеи легко подсчитать количество оборотов каждой окружности в полном цикле:
$$\dvisvgm\definecolor{cyan}{RGB}{0, 200, 250} \usetikzlibrary {shapes.geometric} \usetikzlibrary{animations} \begin{tikzpicture} \def\a{1} \def\b{3} \useasboundingbox (-\b-2*\a-0.1,-\b-2*\a-0.1) rectangle (\b+2*\a+0.1,\b+2*\a+0.1); \draw[cyan,very thin] (-\b-2*\a,-\b-2*\a) grid (\b+2*\a,\b+2*\a); \begin{scope}:rotate = {0s="0", 5s="-360", 6s="-360", 11s="0", 12s="0", repeats} \node[star,star points=57, star point ratio=1.07,minimum size=6.2cm, draw,fill=white] at (0,0); \draw[purple,fill] (0:\b) circle (1pt) -- (0,0) circle (1pt) node [midway, sloped, above] {$\b$} ; \end{scope} \begin{scope} :rotate = {0s="0", 6s="0", 11s="360", 12s="360", origin={(0,0)}, repeats} \begin{scope} :rotate = {0s="0", 5s="1080", 6s="1080", 11s="1080", 12s="1080", origin={(\b+\a,0)}, repeats} \node [star,star points=19, star point ratio=1.2,minimum size=2.2cm, draw,fill=white] at (0:\b+\a) {}; \draw [purple,fill] (0:\b+\a) circle (1pt) -- (0:\b) circle (1pt) node [midway, sloped, above] {$\a$} ; \end{scope} \end{scope} \end{tikzpicture}$$
Самодельная типографская раскладка
В повседневном использовании мне не хватает двух символов на русской раскладке клавиатуры: решетки # и собаки @. Решетка обозначает заголовки в распространенной разметке markdown, а собака применяется в адресах электронной почты в чатах, чтобы «тегать» определенных людей (@supercoder) или всех подряд (@channel).
Для ввода решетки и собаки приходится постоянно переключаться с русского языка на английский и назад, да и сами эти символы вводить с шифтом. Ещё у Windows случается баг, когда она переключает язык не сразу, а после небольшой паузы: ты нажал Alt + Shift и уже
Природу неудобства с этими специальными символами легко понять. Применять их в маркдауне и в чатах придумали в англоязычной среде с расчетом на англоязычную среду, когда никакой язык переключать не нужно. В других языках приходится терпеть.
Много лет назад я устанавливал типографскую раскладку Ильи Бирмана. Она специальным образом задействует правую клавишу Alt для ввода с клавиатуры расширенного набора символов. Например, вместо копирования
Казалось бы, сам бог велел сделать в типографской раскладке не зависящие от языка комбинации клавиш Alt + 2 и Alt + 3 для ввода собаки и решетки. Тем более, так уже сделано и для знака доллара $ с комбинацией Alt + 4, и для квадратных скобок, которые тоже нужны в маркдауне. Но у Ильи в раскладке через Alt + 2 и Alt + 3 вводятся символы верхних индексов ² и ³. Сегодня такое решение кажется устаревшим. Я, кстати, вообще не помню, чтобы хоть раз в жизни использовал верхние индексы именно как эти отдельные символы, а не через инструменты форматирования.
Илья делал свою раскладку в программе MSKLC (Microsoft Keyboard Layout Creator). И нам никто не мешает создать собственную раскладку, отталкиваясь от своих потребностей. К счастью, MSKLC позволяет импортировать любую установленную раскладку и затем её отредактировать.
Я оставил большинство дополнительных символов как у Ильи. Единственные изменения — это прямой ввод символов `@# через комбинацию с правым Alt.

Стоит отметить, что программа MSKLC давно не обновлялась. Это заметно и по ее внешнему виду, и по требованию скачать древнюю версию .NET Framework 3.5 SP1 2008 года. К счастью, эти трудности не помешали создать свою версию раскладки.
Ещё одно интересное наблюдение заключается в том, что раньше типографская раскладка у меня не прижилась, потому что она ломает сочетание клавиш правый Alt + Enter для перехода в полноэкранный режим. Дискомфорт от сломанной привычки оказался сильнее пользы, которую приносила раскладка. Сейчас же я не испытываю особого дискомфорта. Похоже, изменения в интерфейсах и способах просмотра видео привели к тому, что жест правый Alt + Enter потерял своё значение.
Подключил Akismet для борьбы со спамом
Со временем технологии развивались, и через selenium разработчики автоматизировали действия ботов через полноценные браузеры. Метод защиты с помощью Javascript стал фильтровать только самых тупых ботов.
Затем для борьбы со спамом я включил предарительную проверку комментариев перед публикацией. К этому времени поток комментариев на сайте как раз уменьшился. Немногочисленные нормальные комментарии легко одобрить вручную, особенно когда отвечаешь на них. Тогда же я запрограммировал обход предварительной проверки для залогиненных модераторов — пользователей, которые управляют отображением комментариев.
Чтобы облегчить себе жизнь по окончательному удалению спаммерских комментариев из очереди на модерацию, я задумался над тем, какова цель спаммеров? Конечная цель — разместить ссылки для манипуляции индексом цитирования и для привлечения посетителей. Если запретить оставлять ссылки, спаммерам не будет смысла оставлять комментарии без них. А если ссылку хочет разместить человек в хорошем комментарии, сайт скажет ему, чтобы он удалил http:// из ссылки. Запрет на ссылки принес свои плоды, но
Сейчас я решил посмотреть, как привлечь новые технологии для фильтрации спама. Теоретически можно натренировать нейросеть на
Akismet — это система фильтрации спама в комментариях, разработанная авторами WordPress. В вордпрессе есть плагин, который обращается к API Akismet. Однако сам API открыт и может быть использован любым сайтом, для обращения нужен только лицензионный ключ. Лизензия для некоммерческого использования бесплатная.
Основная особенность Akismet заключается в том, что он используется на множестве сайтов. Таким образом можно быстро выявлять новые
Я подключил сервис и несколько дней его тестировал. По каждому комментарию Akismet возвращает свое решение: либо это хороший комментарий, либо спам, либо «вопиющий» (blatant) спам. В итоге остановился на следующем алгоритме фильтрации комментариев:
- если комментарий хороший, он публикуется сразу;
- если комментарий признан вопиющим спамом, он даже не сохраняется, при попытке его отправить будет возвращено сообщение об ошибке;
- если комментарий спаммерский, он остается скрытым, а уведомление о нем отправляется модераторам;
- если владелец сайта не указал в настройке лицензионный ключ Akismet или если сервис не ответил, комментарий либо публикуется либо остается скрытым в зависимости от того, включен ли режим модерации (откат к старому алгоритму).
После внедрения за две недели пришло 62 комментария. Из них 60 спаммерских комментариев были отсеяны либо как вопиющий спам (21 комментарий), либо как спам с наличием ссылок в тексте. Остальные два комментария опубликованы: один хороший комментарий и один спаммерский со ссылкой на yotube.

Понятно, что у способа есть свои недостатки.