Этот модуль содержит функции для работы с датами.

Тестирование конвертаций

эдер
  • {{#invoke:Dates|parseISO8601|1380-09-08T00:00:00Z}} → -18596908800
  • {{#invoke:Dates|parseISO8601|1700-03-11T00:00:00Z}} → -8514374400
  • {{#invoke:Dates|parseISO8601|1799-06-06T00:00:00Z}} → -5382720000
  • {{#invoke:Dates|parseISO8601|1800-03-12T00:00:00Z}} → -5358614400
  • {{#invoke:Dates|parseISO8601|1825-12-26T00:00:00Z}} → -4544726400
  • {{#invoke:Dates|parseISO8601|1837-02-10T00:00:00Z}} → -4193596800
  • {{#invoke:Dates|parseISO8601|1900-03-13T00:00:00Z}} → -2202854400
  • {{#invoke:Dates|parseISO8601|2900-03-13T00:00:00Z}} → 29354140800

--[[
 В это модуле собраны функции, связанные с работой с датами.
]]
    local monthg = {'январьның', 'февральдың', 'марттың', 'апрельдиң', 'майның', 'июньнуң',
    'июльдуң', 'августтуң', 'сентябрьның', 'октябрьның', 'ноябрьның', 'декабрьның', 'бир айның', 'ийи айның', 'үш айның', 'дөрт айның', 'беш айның', 'алды айның',
    'чеди айның', 'сес айның', "тос айның", 'он айның', 'он бир айның', 'он ийи айның',
    'январь', 'февраль', 'март', 'апрель', 'май', 'июнь',
    'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'}

local monthd = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}

local function DecodeDate(d)-- Ч, М, Г, СЧ, СМ, СГ, хвост
    --дата: "%-?%d+"=год, "%d+%.%d+"=число месяца, "%d+%.%d+%.%-?%d+"=ЧМГ,
    -- потом в скобках м.б. переопределено для старого стиля начиная с числа
    local nd=d:match("^[%d.-]*");
    local od=d:match("^[%d.-]*%s*%(%s*([%d.-]*)%s*%)");
    local tail = d:match("^[%d.-]+%s*%(%s*[%d.-]+%s*%)%s*(%S.*)") or d:match("^[%d.-]+%s*([^%s%d].*)");
    if nd:match('^%-?%d+$' ) then
        return nil, nil, tonumber(nd), nil, nil, od and tonumber(od:match("%-?%d+$")),tail
    else
        local j,m,y=nd:match("^(%d+)%.(%d+)%.?(%-?%d*)");
        if j then
            if od then
                local oj, om, oy = od:match("^(%d+)%.?(%d*)%.?(%-?%d*)");
                return j and tonumber(j),
                       m and tonumber(m),
                       y>'' and tonumber(y) or nil,
                      oj and tonumber(oj),
                      om>'' and tonumber(om) or nil,
                      oy>'' and tonumber(oy) or nil,
                      tail
            end
            return j and tonumber(j), m and tonumber(m), y>'' and tonumber(y) or nil, nil, nil, nil, tail
        else return nil
        end
    end
end

local function Diffy(d1,m1,y1,d0,m0,y0)--аналог Персона/Дата/Прошло лет
    return y1-y0 - ( y1*y0<=0 and 1 or 0 ) - ( (m1<m0 or m1==m0 and d1<d0) and 1 or 0 )
end

local function Year0(y,t)-- аналог Год0
    if y>0 then return table.concat{
        '[[', tostring(y), ' чыл|', t and tostring(y)..'&nbsp;'..t or tostring(y), ']]'
    } else return table.concat{
        '[[', tostring(-y), ' б.э.ч. чыл|', 
        t and tostring(-y)..'&nbsp;'..t or tostring(-y),
        '&nbsp;б.&nbsp;э.&nbsp;ч.]]'
    }
    end
end

local function FormDate(j,m,y,oj,om,oy,mo)-- ~ Персона/Дата/Logic 4
    if j then
        if not m then return "''меге формат''" end
        if y then return
         string.format(
            '<span style="white-space:nowrap;">%s<span style="display:none">(<span class="%s">%04i-%02i-%02i</span>)</span></span>',
            table.concat(
                oj and (
                    om and (
                        oy and {-- ДД ММММ ГГГГ ([[ДД ММММ]] [[ГГГГ]])
                            oj,'&nbsp;',monthg[om],'&nbsp;',oy,
                            '</span> <span style="white-space:nowrap;">([[',
                            j, ' ', monthg[m],']] ',Year0(y),')'
                        } or {-- ДД ММММ ([[ДД ММММ]]) [[ГГГГ]]
                            oj,'&nbsp;',monthg[om],' ([[',j,'&nbsp;',monthg[m],']]) ',Year0(y)
                        }
                    ) or {-- ДД [[ДД ММММ|(ДД) ММММ]] [[ГГГГ]]
                        oj,'&nbsp;[[',j,' ',monthg[m],'|','(',j,')&nbsp;',monthg[m],']] ',Year0(y)
                    }
                ) or {'[[',j,'&nbsp;',monthg[m],']]&nbsp;',Year0(y)}
            ),--/table.concat
            ({['Рождения']='bday',['Смерти']='dday'})[mo] or '',
            y,m,j
         )--/string.format
        else return
            '<span style="white-space:nowrap;">' .. table.concat(
                oj and (
                    om and {-- ДД ММММ ([[ДД ММММ]])
                            oj,'&nbsp;',monthg[om],' ([[',j,'&nbsp;',monthg[m],']])</span>'
                    } or {-- ДД [[ДД ММММ|(ДД) ММММ]]
                        oj,'&nbsp;[[',j,' ',monthg[m],'|','(',j,')&nbsp;',monthg[m],']]</span>'
                    }
                ) or {'[[',j,'&nbsp;',monthg[m],']]</span>'}
            )
        end
    else
        return y and string.format(
            '<span style="white-space:nowrap;">%s<span style="display:none;">(<span class="bday">%04i</span>)</span></span>',
            Year0(y,'год'),y) or "''формат неверен''"
    end
end

local function GetDate(D)--dd.mm.-?yyyy или -?yyyy-mm-dd в три переменных d,m,y
    local d,m,y = d:match('^%s*(%d%d?)[/.]([01]?%d)[/.](%-?%d+)')
    if not d then y,m,d = D:match('^%s*(%-?%d+)[-\\]0*(1?%d)[-\\]0*(%d+)') end
    return tonumber(d),tonumber(m),tonumber(y)
end

local function Cmp(a,b)--Сравнивает две даты, результат соответственно -1, 0 или 1 
    local d1,m1,y1 = GetDate(a)
    local d2,m2,y2 = GetDate(b)
    return d1 and d2 and (--nil, если формат не опознан
        y1==y2 and (
            m1==m2 and (
                d1==d2 and 0 or d1<d2 and -1 or 1
            ) or m1<m2 and -1 or 1
        ) or y1<y2 and -1 or 1
    )
end

function Yyyymmdd(r)--Переводит русскую дату в YYYY,MM,DD
    local y, m, d = mw.ustring.match(r, "^%s*(%d+)%s+([а-яА-ЯөңүӨҢҮ ]+)%s+(%d%d?)")
    if not m then return nil end
    m = mw.ustring.lower(m)

    --тупо перебор
    for i = 1, 36 do
    	if m == monthg[i] then
    		M = i%12
    		break
		end
	end

    if not M then
    	return nil
	end
    return tonumber(y), M, tonumber(d)
end

local p = {}

p = {

ifdate=function(f)-- Для шаблона "Если дата", имитирует старое поведение
 -- Аргументы передаются шаблону 
 return f:getParent().args[ mw.ustring.match(frame.args[1],"^[ %d.%-−%()]*$") and 2 or 3 ]
end;

DecodeDate=DecodeDate;Diffy=Diffy;Year0=Year0;GetDate=GetDate;Cmp=Cmp;
Yyyymmdd=Yyyymmdd;

diffy=function(f)-- принимает параметры #invoke в виде двух строк-дат
    local d1,m1,y1=DecodeDate(f.args[1]);
    local d0,m0,y0=DecodeDate(f.args[2])
    return Diffy(d1,m1,y1,d0,m0,y0)
end;

monthg=function(f) return monthg[ f.args[1] or f:getParent().args[1] ] end;--realmonth

persdate=function(f)-- Для шаблона Персона/Дата;{{#invoke:dates|persdate|nocat={{NAMESPACE}}}}
 local frame=f:getParent();
 local catpref,mo,d,d2={['Рождения']='Төрүттүнген',['Смерти']='Мөчээн'}, frame.args[1],frame.args[2],frame.args[3]
 local cat, j,m,y,oj,om,oy,tail, j2,m2,y2, age = ''
 if d then
     j,m,y,oj,om,oy,tail=DecodeDate(d:gsub('−','-'));
     if not (j or y) then
         return (frame.args.nocat and d or d..'[[Category:Википедия:Статьи с ручной викификацией дат в карточке]]')
     end
 end;
 if d2 then
     j2,m2,y2 = DecodeDate(d2:gsub('−','-'));
 end;
 return table.concat{
     FormDate(j,m,y,oj,om,oy,mo),
     ( (frame.args['nopersoncat'] or '')~='' or (f.args['nocat'] or '')~='' ) and '' or table.concat{
         '[[Category:Алфавиттээн кижилер]]',
         j and string.format('[[Аңгылал:%s %i %s]]',catpref[mo],j,monthg[m]) or '',
         y and string.format('[[Аңгылал:%s %s]]',catpref[mo],y,Year0(y,'году')) or ''
     },--/table.concat внутр.
     (function(F)--возраст
         if not F then return '' end;
         local n=F();
         return n and string.format(" (%i %s)%s",
             n,
             mw.getLanguage('ru'):plural(n,'хар','харлыг','хар'),
             n>150 and '[[Category:Википедия:Амгы узун назылыг кижилер]]' or ''
         ) or ''
     end)( ({
         ['Рождения']=function()
             local now=os.date('*t');
             if (not d2 or d2=='') and j and m and y then
                 return Diffy(now.day,now.month,now.year,j,m,y)
             end
         end,
         ['Смерти']=function()
             return j and m and y and j2 and m2 and y2 and Diffy(j,m,y,j2,m2,y2);
         end,
     })[mo] ),--конец вызова функции возраста
     tail or '',
     cat
 }--/table.concat внеш.
end;

formdate=function(f) -- Формирует дату по 3--6 параметрам #invoke или шаблона
    --не использовать с пустыми аргументами
    if (f.args[1] or '')~='' and (f.args[2] or '')~='' or (f.args[3] or '')~='' then
        return FormDate(f.args[1],f.args[2],f.args[3],f.args[4],f.args[5],f.args[6],f.args['m'])
    else
        local tf=f:getParent();
        return FormDate(tf.args[1],tf.args[2],tf.args[3],tf.args[4],tf.args[5],tf.args[6],tf.args['m'])
    end
end;

cmp=function(f)--Сравнивает две даты, результат соответственно -1, 0 или 1
    return Cmp(f.args[1],f.args[2])
end;

G2J=function(f)--перевод григорианских дат в юлианские, возврат DD.MM.YYYY
--Не знает про 15 октября 1582 года, не работает до нашей эры и после ???99 года
--Если есть второй аргумент, преобразует только ДО этой даты включительно
--Если есть третий аргумент, результат форматирует под Персона/Дата
    local d,m,y=GetDate(f.args[1])
    if f.args[2] and Cmp(f.args[1],f.args[2])==1 then
        return string.format("%i.%i.%i",d,m,y)
    end
    local shift=math.floor(y/100)-math.floor(y/400)-2
    if d-shift>0 then
        return f.args[3] and string.format("%i.%i.%i (%i)",d,m,y,d-shift)
        or string.format("%i.%i.%i",d-shift,m,y)
    else
        if m==1 then
            return f.args[3]
            and string.format("%i.1.%i (%i.12.%i)",d,y,31+d-shift,y-1)
            or string.format("%i.12.%i",31+d-shift,y-1)
        elseif m==3 then
            return f.args[3] and string.format("%i.3.%i (%i.2)", d,y,
                (y%4==0 and 29 or 28)+d-shift-(y%100==0 and y%400~=0 and 1 or 0)
            )
            or string.format("%i.2.%i",
                (y%4==0 and 29 or 28)+d-shift-(y%100==0 and y%400~=0 and 1 or 0)
            ,y)
        else
            return f.args[3] and string.format(
                "%i.%i.%i (%i.%i)", d,m,y, monthd[m-1]+d-shift,m-1
            )
            or string.format("%i.%i.%i",monthd[m-1]+d-shift,m-1,y)
        end
    end
end;

yyyymmdd = function(f)
    local date, hourmin = f.args[1]
    if mw.ustring.match(date, "^%s*%d+\-%d+\-%d+") then
    	return date
	end
    hourmin = mw.ustring.match(date, "%s+%d+:%d+$")
    local y, m, d = Yyyymmdd(date)
    if not y then
    	return '<span class="error">Алдаг: ай-хүннүң формады меге.</span>'
	end
    return string.format('%4i-%02i-%02i', y, m, d) .. (hourmin or '')
end
}

function table.val_to_str ( v )
  if "string" == type( v ) then
    v = string.gsub( v, "\n", "\\n" )
    if string.match( string.gsub(v,"[^'\"]",""), '^"+$' ) then
      return "'" .. v .. "'"
    end
    return '"' .. string.gsub(v,'"', '\\"' ) .. '"'
  else
    return "table" == type( v ) and table.tostring( v ) or
      tostring( v )
  end
end

function table.key_to_str ( k )
  if "string" == type( k ) and string.match( k, "^[_%a][_%a%d]*$" ) then
    return k
  else
    return "[" .. table.val_to_str( k ) .. "]"
  end
end

function table.tostring( tbl )
  local result, done = {}, {}
  for k, v in ipairs( tbl ) do
    table.insert( result, table.val_to_str( v ) )
    done[ k ] = true
  end
  for k, v in pairs( tbl ) do
    if not done[ k ] then
      table.insert( result,
        table.key_to_str( k ) .. "=" .. table.val_to_str( v ) )
    end
  end
  return "{" .. table.concat( result, "," ) .. "}"
end

function parseISO8601Date(str)
	local pattern = "(%-?%d+)%-(%d+)%-(%d+)T"
	local Y, M, D = mw.ustring.match( str, pattern )
	return tonumber(Y), tonumber(M), tonumber(D)
end

function parseISO8601Time(str)
	local pattern = "T(%d+):(%d+):(%d+)%Z"
	local H, M, S = mw.ustring.match( str, pattern)
	return tonumber(H), tonumber(M), tonumber(S)
end

function parseISO8601Offset(str)
	if str:sub(-1)=="Z" then return 0,0 end -- ends with Z, Zulu time

	-- matches ±hh:mm, ±hhmm or ±hh; else returns nils 
	local pattern = "([-+])(%d%d):?(%d?%d?)$"
	local sign, oh, om = mw.ustring.match( str, pattern) 
	sign, oh, om = sign or "+", oh or "00", om or "00"

	return tonumber(sign .. oh), tonumber(sign .. om)
end

function p.parseISO8601(str)
	if 'table'==type(str) then
		if str.args and str.args[1] then
			str = '' .. str.args[1]
		else
			return 'unknown argument type: ' .. type( str ) .. ': ' .. table.tostring( str )
		end
	end
	local Y,M,D = parseISO8601Date(str)
	local h,m,s = parseISO8601Time(str)
	local oh,om = parseISO8601Offset(str)
	return tonumber(os.time({year=Y, month=M, day=D, hour=(h+oh), min=(m+om), sec=s}))
end

local g2uBoundary1 = p.parseISO8601('1582-10-15T00:00:00Z')
local g2uBoundary2 = p.parseISO8601('1700-03-12T00:00:00Z')
local g2uBoundary3 = p.parseISO8601('1800-03-13T00:00:00Z')
local g2uBoundary4 = p.parseISO8601('1900-03-14T00:00:00Z')
local g2uBoundary5 = p.parseISO8601('1918-01-26T00:00:00Z') -- декрет Ленина

-- Передаваемое время обязано быть по Григорианскому календарю (новому стилю)
function p.formatWiki( time, infocardClass, categoryNamePrefix )
	if 'table'==type( time ) then
		if time.args and time.args[1] then
			time = tonumber( time.args[1] )
		else
			return 'unknown argument type: ' .. type( time ) .. ': ' .. table.tostring( time )
		end
	end
	local t = os.date("*t", time)
	if time < g2uBoundary1 then
		-- выводим просто юлианский календарь. Задавать тут григорианский некорректно
		return p.formatWikiImpl( t, t, infocardClass, categoryNamePrefix )
	end
	
	-- Специальные даты
	if t.year == 1700 and t.month == 3 and t.day == 11 then
		return p.formatWikiImpl( {year=1700, month=2, day=29}, t, infocardClass, categoryNamePrefix)
	end
	if t.year == 1800 and t.month == 3 and t.day == 12 then
		return p.formatWikiImpl( {year=1800, month=2, day=29}, t, infocardClass, categoryNamePrefix )
	end
	if t.year == 1900 and t.month == 3 and t.day == 13 then
		return p.formatWikiImpl( {year=1900, month=2, day=29}, t, infocardClass, categoryNamePrefix )
	end

	if g2uBoundary1 <= time and time < g2uBoundary2 then
		return p.formatWikiImpl( os.date("*t", time - 10 * 24 * 60 * 60), t, infocardClass, categoryNamePrefix )
	end
	if g2uBoundary2 <= time and time < g2uBoundary3 then
		return p.formatWikiImpl( os.date("*t", time - 11 * 24 * 60 * 60), t, infocardClass, categoryNamePrefix )
	end
	if g2uBoundary3 <= time and time < g2uBoundary4 then
		return p.formatWikiImpl( os.date("*t", time - 12 * 24 * 60 * 60), t, infocardClass, categoryNamePrefix )
	end
	if g2uBoundary4 <= time and time < g2uBoundary5 then
		return p.formatWikiImpl( os.date("*t", time - 13 * 24 * 60 * 60), t, infocardClass, categoryNamePrefix )
	end

	--только Григорианский календарь
	return p.formatWikiImpl( t, t, infocardClass, categoryNamePrefix )
end

function ternary ( cond , T , F )
    if cond then return T else return F end
end

local nominativeMonthes = {'январь', 'февраль', 'март', 'апрель', 'май', 'июнь',
    'июль', 'август', 'сентябрь', 'октябрь', 'ноябрь', 'декабрь'}
    
local genitivusMonthes = {'январьның', 'февральдың', 'марттың', 'апрельдиң', 'майның', 'июньнуң',
    'июльдуң', 'августтуң', 'сентябрьның', 'октябрьның', 'ноябрьның', 'декабрьның'}

function nominativeYear( year )
    if ( year >= 0 ) then
        return '[[' .. year .. ' чыл|' .. year .. ']]'
    else
        return '[[' .. ( 0 - year ) .. ' чыл б. э. ч.|' .. ( 0 - year ) .. ' б. э. ч.]]'
    end
end

function inYear( year )
    if ( year >= 0 ) then
        return '' .. year .. ' чылда'
    else
        return '' .. 'б.э. чедир ' .. ( 0 - year) .. ' чылда'
    end
end

function p.formatWikiImpl( t1, t2, infocardClass, categoryNamePrefix )
    local nd = t2.day;
    local nm = t2.month;
    local ny = t2.year;
    local od = ternary ( t1.day ~= t2.day , t1.day, nil );
    local om = ternary ( t1.month ~= t2.month , t1.month, nil );
    local oy = ternary ( t1.year ~= t2.year , t1.year, nil );

	local JulianComment = function(s)
		return tostring(mw.html.create("abbr")
			:attr("title","по юлианскому календарю")
			:wikitext(s)
			:done())
	end

	
    local template =
        (nd ~= nil and "1" or "") .. (nm ~= nil and "2" or "") .. (ny ~= nil and "3" or "") ..
        (od ~= nil and "4" or "") .. (om ~= nil and "5" or "") .. (oy ~= nil and "6" or "")
 
    local datePart = '<span style="white-space:nowrap;">'
    if (template == "12") then
        datePart = datePart .. string.format( "[[%s %d]]",
        								nd, genitivusMonthes[nm] )
    elseif (template == "23") then
        datePart = datePart .. string.format( "%s [[%s]]",
        								nominativeYear( ny ), nominativeMonthes[nm] )
    elseif (template == "3") then
        datePart = datePart .. nominativeYear( ny )
    elseif (template == "123") then
        datePart = datePart .. string.format( "%s [[%s %d]]",
                                        nominativeYear( ny ), genitivusMonthes[nm], nd )
    elseif (template == "124") then
        datePart = datePart .. JulianComment(string.format( "%d", od )
							).. string.format( " [[%s %d|%s (%d)]]",
                                        genitivusMonthes[nm], nd, genitivusMonthes[nm], nd )
    elseif (template == "1234") then
        datePart = datePart .. JulianComment(string.format( "%d", od )
							).. string.format( " [[%d %s|(%d) %s]] %s",
                                        nd, genitivusMonthes[nm], nd, genitivusMonthes[nm], nominativeYear( ny ) )
    elseif (template == "1245") then
        datePart = datePart .. JulianComment(string.format( "%d %s", od, genitivusMonthes[om] )
							).. string.format(" ([[%d %s]])", nd, genitivusMonthes[nm] )										
    elseif (template == "12345") then
        datePart = datePart .. JulianComment(string.format( "%d %s", od, genitivusMonthes[om] )
							).. string.format(" ([[%d %s]]) %s", nd, genitivusMonthes[nm], nominativeYear( ny ) )
    elseif (template == "123456") then
        datePart = datePart .. JulianComment(string.format( "%d %s %d", od, genitivusMonthes[om], oy ))
							.. '</span> <span style="white-space:nowrap;">'
							.. string.format(' ([[%d %s]] %s)', nd, genitivusMonthes[nm], nominativeYear( ny ) )
    else
        datePart = datePart .. 'формат неверен'
    end
    datePart = datePart .. '</span>'
 
    local infocardTemplate =
        (nd ~= nil and "1" or "") .. (nm ~= nil and "2" or "") .. (ny ~= nil and "3" or "")
 
	if infocardClass then
	    if (infocardTemplate == "123") then
	        datePart = datePart .. string.format('<span style="display:none">(<span class="%s">%04d-%02d-%02d</span>)</span>', infocardClass , ny , nm , nd )
	    elseif (infocardTemplate == "23") then
	        datePart = datePart .. string.format('<span style="display:none">(<span class="%s">%04d-%02d</span>)</span>', infocardClass , ny , nm )
	    elseif (infocardTemplate == "3") then
	        datePart = datePart .. string.format('<span style="display:none;">(<span class="%s">%04d</span>)</span>', infocardClass , ny )
	    end
	end
 
    if categoryNamePrefix then
        if ( nd ~= nil and nm ~= nil) then
            datePart = datePart .. '[[Аңгылал:' .. genitivusMonthes[nm] .. ' ' .. nd .. ' ' .. categoryNamePrefix .. ']]'
        end
        if ( ny ~= nil) then
            datePart = datePart .. '[[Аңгылал:' .. inYear( ny ) .. ' ' .. categoryNamePrefix .. ']]'
        end
    end
 
    return datePart
end

return p