Модуль:Срок
Описание модуля
Модуль:Срок предназначен для автоматического расчёта времени, прошедшего с момента открытия или основания объекта (транспортной системы, линии, депо, предприятия и т.п.) до сегодняшнего дня или даты закрытия.
Механизм работы
- Модуль автоматически удаляет квадратные скобки вики-ссылок (
[[20 мая]] [[2024|2024 года]]), HTML-комментарии и слова «года/г.». - Принимает текстовые даты (
20 мая 2024), цифровые (20.05.2024,2024-05-20), а также неполные даты (май 2024или2024). - Модуль вычисляет разницу строго по календарным границам с учётом високосных годов. Если текущий день месяца меньше дня открытия, дни корректно заимствуются из предыдущего месяца без погрешностей усреднения.
- Автоматически подставляет грамматические формы (1 год / 2 года / 5 лет, 1 месяц / 4 месяца / 7 месяцев, 1 день / 3 дня / 12 дней).
Примеры использования
1. Внутри шаблона-карточки (инфобокса)
В коде шаблона карточки (например, Шаблон:Метрополитен):
|метка5 = Дата открытия
|текст5 = {{#if: {{{дата открытия|}}} | {{{дата открытия}}}{{#if: {{#invoke:Срок|diff|{{{дата открытия}}}|{{{дата закрытия|}}}}} | <br><small style="color: #57606a;">({{#invoke:Срок|diff|{{{дата открытия}}}|{{{дата закрытия|}}}}})</small> }} }}
Как это выглядит в карточке статьи:
- Действующая система (
|дата открытия = 20 мая 2024): - > Дата открытия: 20 мая 2024
(действует 2 года, 3 месяца и 22 дня) - Закрытая система (
|дата открытия = 10 мая 1995,|дата закрытия = 15 октября 2012): - > Дата открытия: 10 мая 1995
(проработал 17 лет, 5 месяцев и 5 дней)
2. Внутри связного текста статьи
Муроморский скоростной трамвай был запущен в эксплуатацию [[9 мая]] [[2024 года]]
({{#invoke:Срок|diff|9 мая 2024}}).
Как это выглядит: > Муроморский скоростной трамвай был запущен в эксплуатацию 9 мая 2024 года (действует 2 года, 4 месяца и 2 дня).
3. Внутри таблиц и информационных блоков
Если требуется вывести только интервал без префиксов «действует/проработал»:
{| class="wikitable"
! Маршрут !! Открыт !! Возраст маршрута
|-
| Трамвай №1 || 9 мая 2024 || {{#invoke:Срок|diff|9 мая 2024|только_число=да}}
|-
| Трамвай №5 || 8 августа 2026 || {{#invoke:Срок|diff|8 августа 2026|только_число=да}}
|}
Как это выглядит:
| Маршрут | Открыт | Возраст маршрута |
|---|---|---|
| Трамвай №1 | 9 мая 2024 | 2 года, 4 месяца и 2 дня |
| Трамвай №5 | 8 августа 2026 | 1 месяц и 3 дня |
4. Отключение отображения дней
Если точность до дней избыточна:
{{#invoke:Срок|diff|20 мая 2024|без_дней=да}}
"действует 2 года и 3 месяца" </noinclude>
5. Всплывающая подсказка при наведении
Позволяет не перегружать интерфейс карточки лишним текстом: дата остаётся аккуратной и компактной, а точный отсчёт времени (лет, месяцев и дней) отображается в нативной всплывающей подсказке браузера при наведении курсора на дату.
Дата помечается тонким пунктирным подчёркиванием снизу, а курсор меняется на справочный (`?` / `help`).
Поведение подсказки:
- При наведении на дату открытия действующей системы всплывает: «действует X лет, Y месяцев и Z дней» (для закрытой — «проработал...»).
- При наведении на дату закрытия всплывает общий срок эксплуатации: «Срок работы: X лет, Y месяцев и Z дней».
Код для шаблона-карточки:
|метка5 = Дата открытия
|текст5 = {{#if: {{{дата открытия|}}} | {{#if: {{#invoke:Срок|diff|{{{дата открытия}}}|{{{дата закрытия|}}}}} | <abbr title="{{#invoke:Срок|diff|{{{дата открытия}}}|{{{дата закрытия|}}}}}" style="text-decoration: none; border-bottom: 1px dashed #8c959f; cursor: help;">{{{дата открытия}}}</abbr> | {{{дата открытия}}} }} }}
|метка6 = Дата закрытия
|текст6 = {{#if: {{{дата закрытия|}}} | {{#if: {{#invoke:Срок|diff|{{{дата открытия|}}}|{{{дата закрытия}}}}} | <abbr title="Срок работы: {{#invoke:Срок|diff|{{{дата открытия|}}}|{{{дата закрытия}}}|только_число=да}}" style="text-decoration: none; border-bottom: 1px dashed #8c959f; cursor: help;">{{{дата закрытия}}}</abbr> | {{{дата закрытия}}} }} }}
Использование в обычном тексте статьи:
Движение было открыто <abbr title="{{#invoke:Срок|diff|20 мая 2024}}" style="border-bottom: 1px dashed #8c959f; cursor: help;">20 мая 2024 года</abbr>.
"Движение было открыто 20 мая 2024 года".
6. Нейтральный отсчёт (для городов, населённых пунктов и событий)
Для объектов, к которым не подходит транспортное слово «действует» (города, крепости, памятники, исторические события, битвы или конференции), предусмотрены нейтральные формулировки.
Для городов и поселений (|тип=город или |тип=нп):
- Действующий город: «существует 323 года»
- Заброшенный/упразднённый город: «просуществовал 142 года»
Для событий и годовщин (|нейтрально=да или |тип=событие):
- Прошедшее событие: «прошло 80 лет, 4 месяца и 2 дня»
- Длившийся процесс (при указании даты окончания): «продлилось 3 года и 1 месяц»
Относительный формат (|режим=назад):
- Выводит: «2 года, 3 месяца и 15 дней назад»
Собственный префикс и суффикс (|префикс=..., |суффикс=...):
{{#invoke:Срок|diff|1147|префикс=возраст Москвы: }}→ возраст Москвы: 879 лет
Примеры вызова в тексте
{{#invoke:Срок|diff|1703|тип=город}}→ действует 323 года и 8 месяцев{{#invoke:Срок|diff|12 апреля 1961|нейтрально=да}}→ действует 65 лет, 4 месяца и 30 дней{{#invoke:Срок|diff|9 мая 1945|режим=назад}}→ действует 81 год, 4 месяца и 2 дня
Использование в шаблоне населённого пункта ({{НП}})
Для всплывающей подсказки в карточке населённого пункта над полем «Основан»:
|метка13 = Основан
|текст13 = {{#if: {{{дата основания|}}} | {{#if: {{#invoke:Срок|diff|{{{дата основания}}}|тип=город}} | <abbr title="{{#invoke:Срок|diff|{{{дата основания}}}|тип=город}}" style="text-decoration: none; border-bottom: 1px dashed #8c959f; cursor: help;">{{{дата основания}}}</abbr> | {{{дата основания}}} }} }}
local p = {}
local months = {
['января'] = 1, ['янв'] = 1,
['февраля'] = 2, ['фев'] = 2,
['марта'] = 3, ['мар'] = 3,
['апреля'] = 4, ['апр'] = 4,
['мая'] = 5,
['июня'] = 6, ['июн'] = 6,
['июля'] = 7, ['июл'] = 7,
['августа'] = 8, ['авг'] = 8,
['сентября'] = 9, ['сен'] = 9,
['октября'] = 10, ['окт'] = 10,
['ноября'] = 11, ['ноя'] = 11,
['декабря'] = 12, ['дек'] = 12
}
-- Склонение числительных
local function plural(n, f1, f2, f5)
n = math.abs(n) % 100
local n1 = n % 10
if n > 10 and n < 20 then return f5 end
if n1 > 1 and n1 < 5 then return f2 end
if n1 == 1 then return f1 end
return f5
end
-- Количество дней в месяце с учётом високосного года
local function daysInMonth(year, month)
local daysPerMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
if month == 2 and ((year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)) then
return 29
end
return daysPerMonth[month] or 30
end
-- Парсинг даты
local function parseDate(str)
if not str or str == '' then return nil end
str = mw.ustring.gsub(str, '[%[%]]', '')
str = mw.ustring.gsub(str, '<!%-%-.-%-%->', '')
str = mw.ustring.gsub(str, '%s*года?%.?', '')
str = mw.ustring.gsub(str, '%s*г%.?', '')
str = mw.text.trim(str)
-- Формат: 20 мая 2024
local d, mName, y = mw.ustring.match(str, '^(%d%d?)%s+([а-яё]+)%s+(%d%d%d%d)$')
if d and mName and y and months[mw.ustring.lower(mName)] then
return tonumber(y), months[mw.ustring.lower(mName)], tonumber(d), true
end
-- Формат: 20.05.2024 или 20/05/2024
d, mName, y = mw.ustring.match(str, '^(%d%d?)[./](%d%d?)[./](%d%d%d%d)$')
if d and mName and y then
return tonumber(y), tonumber(mName), tonumber(d), true
end
-- Формат ISO: 2024-05-20
y, mName, d = mw.ustring.match(str, '^(%d%d%d%d)%-(%d%d?)%-(%d%d?)$')
if y and mName and d then
return tonumber(y), tonumber(mName), tonumber(d), true
end
-- Только месяц и год: май 2024
mName, y = mw.ustring.match(str, '^([а-яё]+)%s+(%d%d%d%d)$')
if mName and y and months[mw.ustring.lower(mName)] then
return tonumber(y), months[mw.ustring.lower(mName)], 1, false
end
-- Только год: 2024
y = mw.ustring.match(str, '^(%d%d%d%d)$')
if y then
return tonumber(y), 1, 1, false
end
return nil
end
function p.diff(frame)
local args = frame.args[1] and frame.args or frame:getParent().args
local rawStart = args[1] or args['дата'] or args['открытие']
local rawEnd = args[2] or args['до'] or args['закрытие']
local y1, m1, d1, hasExactDay1 = parseDate(rawStart)
if not y1 then return '' end
local y2, m2, d2, hasExactDay2
local isClosed = false
if rawEnd and mw.text.trim(rawEnd) ~= '' and mw.text.trim(rawEnd) ~= '-' then
y2, m2, d2, hasExactDay2 = parseDate(rawEnd)
if y2 then isClosed = true end
end
if not y2 then
local now = os.date('*t')
y2, m2, d2 = now.year, now.month, now.day
hasExactDay2 = true
end
-- Алгоритм календарного заёма дней и месяцев
local showDays = (args['без_дней'] ~= 'да' and hasExactDay1 and hasExactDay2)
local years = y2 - y1
local monthsCount = m2 - m1
local days = d2 - d1
if days < 0 then
-- Занимаем дни у предыдущего месяца
m2 = m2 - 1
if m2 < 1 then
m2 = 12
y2 = y2 - 1
end
days = days + daysInMonth(y2, m2)
monthsCount = m2 - m1
years = y2 - y1
end
if monthsCount < 0 then
years = years - 1
monthsCount = monthsCount + 12
end
if years < 0 then return '' end
-- Сборка текстовых фрагментов
local parts = {}
if years > 0 then
table.insert(parts, years .. ' ' .. plural(years, 'год', 'года', 'лет'))
end
if monthsCount > 0 then
table.insert(parts, monthsCount .. ' ' .. plural(monthsCount, 'месяц', 'месяца', 'месяцев'))
end
if showDays and days > 0 then
table.insert(parts, days .. ' ' .. plural(days, 'день', 'дня', 'дней'))
end
local result = ''
if #parts == 0 then
result = showDays and 'менее суток' or 'менее месяца'
elseif #parts == 1 then
result = parts[1]
elseif #parts == 2 then
result = parts[1] .. ' и ' .. parts[2]
else
result = parts[1] .. ', ' .. parts[2] .. ' и ' .. parts[3]
end
if args['только_число'] == 'да' or args['сырой'] == 'да' then
return result
end
if isClosed then
return 'проработал ' .. result
else
return 'действует ' .. result
end
end
return p