Перейти к содержанию

Модуль:Срок: различия между версиями

Материал из MURS вики
Новая страница: «local p = {} local months = { ['января'] = 1, ['янв'] = 1, ['февраля'] = 2, ['фев'] = 2, ['марта'] = 3, ['мар'] = 3, ['апреля'] = 4, ['апр'] = 4, ['мая'] = 5, ['июня'] = 6, ['июн'] = 6, ['июля'] = 7, ['июл'] = 7, ['августа'] = 8, ['авг'] = 8, ['сентября'] = 9, ['сен'] = 9, ['октября'] = 10, ['окт'] = 10, ['ноября'] = 11, ['н...»
 
мНет описания правки
 
(не показана 1 промежуточная версия этого же участника)
Строка 38: Строка 38:
local function parseDate(str)
local function parseDate(str)
     if not str or str == '' then return nil end
     if not str or str == '' then return nil end
   
    -- Нормализация пробелов и очистка неразрывных пробелов
    str = mw.ustring.gsub(str, ' ', ' ')
    str = string.gsub(str, '\194\160', ' ')
   
    -- Очистка ссылок с пайпами: [[1703 год|1703]] -> 1703
    str = mw.ustring.gsub(str, '%[%[[^%]|]*|([^%]]+)%]%]', '%1')
     str = mw.ustring.gsub(str, '[%[%]]', '')
     str = mw.ustring.gsub(str, '[%[%]]', '')
     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)
     str = mw.text.trim(str)


     -- Формат: 20 мая 2024
    -- Удаление окончания «года / год / г. / г» строго в конце строки ($)
     local d, mName, y = mw.ustring.match(str, '^(%d%d?)%s+([а-яё]+)%s+(%d%d%d%d)$')
    str = mw.ustring.gsub(str, '%s*года?%.?$', '')
    str = mw.ustring.gsub(str, '%s*г%.?$', '')
    str = mw.text.trim(str)
 
     -- Формат: 22 августа 2024
     local d, mName, y = mw.ustring.match(str, '^(%d%d?)%s+([а-яё]+)%s+(%d+)$')
     if d and mName and y and months[mw.ustring.lower(mName)] then
     if d and mName and y and months[mw.ustring.lower(mName)] then
         return tonumber(y), months[mw.ustring.lower(mName)], tonumber(d), true
         return tonumber(y), months[mw.ustring.lower(mName)], tonumber(d), true
Строка 51: Строка 61:


     -- Формат: 20.05.2024 или 20/05/2024
     -- Формат: 20.05.2024 или 20/05/2024
     d, mName, y = mw.ustring.match(str, '^(%d%d?)[./](%d%d?)[./](%d%d%d%d)$')
     d, mName, y = mw.ustring.match(str, '^(%d%d?)[./](%d%d?)[./](%d+)$')
     if d and mName and y then
     if d and mName and y then
         return tonumber(y), tonumber(mName), tonumber(d), true
         return tonumber(y), tonumber(mName), tonumber(d), true
Строка 57: Строка 67:


     -- Формат ISO: 2024-05-20
     -- Формат ISO: 2024-05-20
     y, mName, d = mw.ustring.match(str, '^(%d%d%d%d)%-(%d%d?)%-(%d%d?)$')
     y, mName, d = mw.ustring.match(str, '^(%d+)%-(%d%d?)%-(%d%d?)$')
     if y and mName and d then
     if y and mName and d then
         return tonumber(y), tonumber(mName), tonumber(d), true
         return tonumber(y), tonumber(mName), tonumber(d), true
     end
     end


     -- Только месяц и год: май 2024
     -- Только месяц и год: август 2024
     mName, y = mw.ustring.match(str, '^([а-яё]+)%s+(%d%d%d%d)$')
     mName, y = mw.ustring.match(str, '^([а-яё]+)%s+(%d+)$')
     if mName and y and months[mw.ustring.lower(mName)] then
     if mName and y and months[mw.ustring.lower(mName)] then
         return tonumber(y), months[mw.ustring.lower(mName)], 1, false
         return tonumber(y), months[mw.ustring.lower(mName)], 1, false
     end
     end


     -- Только год: 2024
     -- Только год: 2024, 1703, 862
     y = mw.ustring.match(str, '^(%d%d%d%d)$')
     y = mw.ustring.match(str, '^(%d+)$')
     if y then
     if y then
         return tonumber(y), 1, 1, false
         return tonumber(y), 1, 1, false
Строка 79: Строка 89:
function p.diff(frame)
function p.diff(frame)
     local args = frame.args[1] and frame.args or frame:getParent().args
     local args = frame.args[1] and frame.args or frame:getParent().args
     local rawStart = args[1] or args['дата'] or args['открытие']
     local rawStart = args[1] or args['дата'] or args['открытие'] or args['основание']
     local rawEnd = args[2] or args['до'] or args['закрытие']
     local rawEnd = args[2] or args['до'] or args['закрытие'] or args['ликвидация']


     local y1, m1, d1, hasExactDay1 = parseDate(rawStart)
     local y1, m1, d1, hasExactDay1 = parseDate(rawStart)
Строка 98: Строка 108:
     end
     end


     -- Алгоритм календарного заёма дней и месяцев
     -- Алгоритм расчёта разницы
     local showDays = (args['без_дней'] ~= 'да' and hasExactDay1 and hasExactDay2)
     local showDays = (args['без_дней'] ~= 'да' and hasExactDay1 and hasExactDay2)
     local years = y2 - y1
     local years = y2 - y1
Строка 105: Строка 115:


     if days < 0 then
     if days < 0 then
        -- Занимаем дни у предыдущего месяца
         m2 = m2 - 1
         m2 = m2 - 1
         if m2 < 1 then
         if m2 < 1 then
Строка 123: Строка 132:
     if years < 0 then return '' end
     if years < 0 then return '' end


     -- Сборка текстовых фрагментов
     -- Сборка строки
     local parts = {}
     local parts = {}
     if years > 0 then
     if years > 0 then
Строка 146: Строка 155:
     end
     end


    -- 1. Только числовой результат
     if args['только_число'] == 'да' or args['сырой'] == 'да' then
     if args['только_число'] == 'да' or args['сырой'] == 'да' then
         return result
         return result
     end
     end


    -- 2. Пользовательский префикс / суффикс
    if args['префикс'] or args['суффикс'] then
        return (args['префикс'] or '') .. result .. (args['суффикс'] or '')
    end
    local mode = mw.ustring.lower(args['режим'] or args['тип'] or '')
    -- 3. Режим для городов и населённых пунктов
    if mode == 'город' or mode == 'нп' or mode == 'существует' or mode == 'основан' then
        if isClosed then
            return 'просуществовал ' .. result
        else
            return 'существует ' .. result
        end
    end
    -- 4. Нейтральный режим для событий, дат и годовщин
    if args['нейтрально'] == 'да' or args['нейтральный'] == 'да' or mode == 'нейтрально' or mode == 'событие' or mode == 'прошло' then
        if isClosed then
            return 'продлилось ' .. result
        else
            return 'прошло ' .. result
        end
    end
    -- 5. Режим «назад»
    if mode == 'назад' then
        return result .. ' назад'
    end
    -- 6. Стандартный транспортный режим (по умолчанию)
     if isClosed then
     if isClosed then
         return 'проработал ' .. result
         return 'проработал ' .. result

Текущая версия от 07:31, 11 сентября 2026

Описание модуля

[править код]

Модуль:Срок предназначен для автоматического расчёта времени, прошедшего с момента открытия или основания объекта (транспортной системы, линии, депо, предприятия и т.п.) до сегодняшнего дня или даты закрытия.

Механизм работы

[править код]
  1. Модуль автоматически удаляет квадратные скобки вики-ссылок ([[20 мая]] [[2024|2024 года]]), HTML-комментарии и слова «года/г.».
  2. Принимает текстовые даты (20 мая 2024), цифровые (20.05.2024, 2024-05-20), а также неполные даты (май 2024 или 2024).
  3. Модуль вычисляет разницу строго по календарным границам с учётом високосных годов. Если текущий день месяца меньше дня открытия, дни корректно заимствуются из предыдущего месяца без погрешностей усреднения.
  4. Автоматически подставляет грамматические формы (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, '&nbsp;', ' ')
    str = string.gsub(str, '\194\160', ' ')
    
    -- Очистка ссылок с пайпами: [[1703 год|1703]] -> 1703
    str = mw.ustring.gsub(str, '%[%[[^%]|]*|([^%]]+)%]%]', '%1')
    str = mw.ustring.gsub(str, '[%[%]]', '')
    str = mw.ustring.gsub(str, '<!%-%-.-%-%->', '')
    str = mw.text.trim(str)

    -- Удаление окончания «года / год / г. / г» строго в конце строки ($)
    str = mw.ustring.gsub(str, '%s*года?%.?$', '')
    str = mw.ustring.gsub(str, '%s*г%.?$', '')
    str = mw.text.trim(str)

    -- Формат: 22 августа 2024
    local d, mName, y = mw.ustring.match(str, '^(%d%d?)%s+([а-яё]+)%s+(%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+)$')
    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?)$')
    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+)$')
    if mName and y and months[mw.ustring.lower(mName)] then
        return tonumber(y), months[mw.ustring.lower(mName)], 1, false
    end

    -- Только год: 2024, 1703, 862
    y = mw.ustring.match(str, '^(%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['открытие'] or args['основание']
    local rawEnd = args[2] or args['до'] 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 .. '&nbsp;' .. plural(years, 'год', 'года', 'лет'))
    end
    if monthsCount > 0 then
        table.insert(parts, monthsCount .. '&nbsp;' .. plural(monthsCount, 'месяц', 'месяца', 'месяцев'))
    end
    if showDays and days > 0 then
        table.insert(parts, days .. '&nbsp;' .. 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

    -- 1. Только числовой результат
    if args['только_число'] == 'да' or args['сырой'] == 'да' then
        return result
    end

    -- 2. Пользовательский префикс / суффикс
    if args['префикс'] or args['суффикс'] then
        return (args['префикс'] or '') .. result .. (args['суффикс'] or '')
    end

    local mode = mw.ustring.lower(args['режим'] or args['тип'] or '')

    -- 3. Режим для городов и населённых пунктов
    if mode == 'город' or mode == 'нп' or mode == 'существует' or mode == 'основан' then
        if isClosed then
            return 'просуществовал ' .. result
        else
            return 'существует ' .. result
        end
    end

    -- 4. Нейтральный режим для событий, дат и годовщин
    if args['нейтрально'] == 'да' or args['нейтральный'] == 'да' or mode == 'нейтрально' or mode == 'событие' or mode == 'прошло' then
        if isClosed then
            return 'продлилось ' .. result
        else
            return 'прошло ' .. result
        end
    end

    -- 5. Режим «назад»
    if mode == 'назад' then
        return result .. ' назад'
    end

    -- 6. Стандартный транспортный режим (по умолчанию)
    if isClosed then
        return 'проработал ' .. result
    else
        return 'действует ' .. result
    end
end

return p