La documentation pour ce module peut être créée à Module:Time/doc

require("strict")

local Time = {}

--Internal functions
--[[
    Check if a value is a number in the given range
    @param mixed value
    @param number min
    @param number max
    @return boolean
]]--
local function validateNumberInRange( value, min, max )
    return type( value ) == 'number' and value >= min and value <= max
end

--[[
    Validate a time defintion
    @param table definition data
    @return boolean
]]--
local function validate(definition)
	--Validate constants
	if not Time.knowsPrecision(definition.precision) or 
		(definition.calendar ~= Time.CALENDAR.GREGORIAN and definition.calendar ~= Time.CALENDAR.JULIAN) then
		return false
	end

    --Validate year
    if not (type( definition.year ) == 'number' or (definition.year == nil and precision == Time.PRECISION.DAY)) then
        return false
    end
    if definition.precision <= Time.PRECISION.YEAR then
        return true
    end

    --Validate month
    if not validateNumberInRange( definition.month, 1, 12 ) then
        return false
    end
    if definition.precision <= Time.PRECISION.MONTH then
        return true
    end

    --Validate day
    if not validateNumberInRange( definition.day, 1, 31 ) then
        return false
    end
    if definition.precision <= Time.PRECISION.DAY then
        return true
    end

    --Validate hour
    if not validateNumberInRange( definition.hour, 0, 23 ) then
        return false
    end
    if definition.precision <= Time.PRECISION.HOUR then
        return true
    end

    --Validate minute
    if not validateNumberInRange( definition.minute, 0, 59 ) then
        return false
    end
    if definition.precision <= Time.PRECISION.MINUTE then
        return true
    end

    --Validate second
    if not validateNumberInRange( definition.second, 0, 60 ) then
        return false
    end

    return true
end

--[[
    Try to find the relevant precision for a time definition
    @param table time definition
    @return number the precision
]]--
local function guessPrecision(definition)
    if definition.month == nil or (definition.month == 0 and definition.day == 0) then
        return Time.PRECISION.YEAR
    elseif definition.day == nil or definition.day == 0 then
        return Time.PRECISION.MONTH
    elseif definition.hour == nil then
        return Time.PRECISION.DAY
    elseif definition.minute == nil then
        return Time.PRECISION.HOUR
    elseif definition.second == nil then
        return Time.PRECISION.MINUTE
    else
        return Time.PRECISION.SECOND
    end
end

--[[
    Try to find the relevant calendar for a time definition
    @param table time definition
    @return string the calendar name
]]--
local function guessCalendar( definition )
    if definition.year ~= nil and definition.year < 1583 and definition.precision > Time.PRECISION.MONTH then
        return Time.CALENDAR.JULIAN
    else
        return Time.CALENDAR.GREGORIAN
    end
end

--[[
    Parse an ISO 2061 string and return it as a time definition
    @param string iso the iso datetime
    @param boolean withoutRecurrence concider date in the format XX-XX as year-month and not month-day
    @return table
]]--
local function parseIso8601( iso, withoutRecurrence )
    local definition = {}

    --Split date and time
    iso = mw.text.trim( iso:upper() )
    local beginMatch, endMatch, date, time, offset = iso:find( '([%+%-]?[%d%-]+)[T ]?([%d%.:]*)([Z%+%-]?[%d:]*)' )

    if beginMatch ~= 1 or endMatch ~= iso:len() then --iso is not a valid ISO string
        return {}
    end

    --date
    if date ~= nil then
        local isBC = false
        if date:sub( 1, 1 ) == '-' then
            isBC = true
            date = date:sub( 2, date:len() )
        end
        local parts = mw.text.split( date, '-' )
        if not withoutRecurrence and table.maxn( parts ) == 2 and parts[1]:len() == 2 then
            --MM-DD case
            definition.month = tonumber( parts[1] )
            definition.day = tonumber( parts[2] )
        else
            if isBC then
                definition.year = -1 * tonumber( parts[1] )  --FIXME - 1 --Years BC are counted since 0 and not -1
            else
                definition.year = tonumber( parts[1] )
            end
            definition.month = tonumber( parts[2] )
            definition.day = tonumber( parts[3] )
        end
    end

    --time
    if time ~= nil then
        local parts = mw.text.split( time, ':' )
        definition.hour = tonumber( parts[1] )
        definition.minute = tonumber( parts[2] )
        definition.second = tonumber( parts[3] )
    end

    --ofset
    if offset ~= nil then
        if offset == 'Z' then
            definition.utcoffset = '+00:00'
        else
            definition.utcoffset = offset
        end
    end

    return definition
end

--[[
    Format UTC offset for ISO output
    @param string offset UTC offset
    @return string UTC offset for ISO
]]--
local function formatUtcOffsetForIso( offset )
    if offset == '+00:00' then
        return 'Z'
    else
        return offset
    end
end

--[[
    Prepend as mutch as needed the character c to the string str in order to to have a string of length length
    @param mixed str
    @param string c
    @param number length
    @return string
]]--
local function prepend(str, c, length)
    str = tostring( str )
    while str:len() < length do
        str = c .. str
    end
    return str
end

--  LEAP_GREGORIAN  --  Is a given year in the Gregorian calendar a leap year ?
local function leapGregorian(year)
    return ((year % 4) == 0) and
            (not (((year % 100) == 0) and ((year % 400) ~= 0)))
end

--  GREGORIAN_TO_JD  --  Determine Julian day number from Gregorian calendar date
local GREGORIAN_EPOCH = 1721425.5

local function gregorianToJd(year, month, day)
    return (GREGORIAN_EPOCH - 1) +
           (365 * (year - 1)) +
           math.floor((year - 1) / 4) +
           (-math.floor((year - 1) / 100)) +
           math.floor((year - 1) / 400) +
           math.floor((((367 * month) - 362) / 12) +
           ((month <= 2) and 0 or
                               (leapGregorian(year) and -1 or -2)
           ) +
           day)
end

--  JD_TO_JULIAN  --  Calculate Julian calendar date from Julian day
local function jdToJulian(td)
    local z, a, alpha, b, c, d, e, year, month, day
    
    td = td + 0.5
    z = math.floor(td)
    
    a = z
    b = a + 1524
    c = math.floor((b - 122.1) / 365.25)
    d = math.floor(365.25 * c)
    e = math.floor((b - d) / 30.6001)
    
    month = math.floor((e < 14) and (e - 1) or (e - 13))
    year = math.floor((month > 2) and (c - 4716) or (c - 4715))
    day = b - d - math.floor(30.6001 * e)
    
    --[[
        If year is less than 1, subtract one to convert from
        a zero based date system to the common era system in
        which the year -1 (1 B.C.E) is followed by year 1 (1 C.E.).
    --]]
    
    if year < 1 then
        year = year - 1
    end
    
    return year, month, day
end

local function le(t1, t2)
	if t1.calendar ~= t2.calendar then
		-- error("Eltérő naptárak, nem lehet összehasonlítani", 2)
	end
	if t1.year < t2.year then
		return true
	end
	if t1.year == t2.year then
		if t1.month < t2.month then
			return true
		end
		if t1.month == t2.month and t1.day <= t2.day then
			return true
		end
	end
	return false
end

--Public interface
--[[
    Build a new Time
    @param table definition definition of the time
    @return Time|nil
]]--
function Time.new( definition )
    --Default values
    if definition.precision == nil then
        definition.precision = guessPrecision( definition )
    end
    if definition.calendar == nil then
        definition.calendar = guessCalendar( definition )
    end

    if not validate( definition ) then
        return nil
    end

    local time = {
        year = definition.year or nil,
        month = definition.month or 1,
        day = definition.day or 1,
        hour = definition.hour or 0,
        minute = definition.minute or 0,
        second = definition.second or 0,
        utcoffset = definition.utcoffset or '+00:00',
        calendar = definition.calendar or Time.CALENDAR.GREGORIAN,
        precision = definition.precision or 0
    }

    setmetatable( time, {
        __index = Time,
        __le = le,
        __tostring = function( self ) return self:toString() end
    } )
        
    return time
end

--[[
    Build a new Time from an ISO 8601 datetime
    @param string iso the time as ISO string
    @param boolean withoutRecurrence concider date in the format XX-XX as year-month and not month-day
    @return Time|nil
]]--
function Time.newFromIso8601( iso, withoutRecurrence )
    return Time.new( parseIso8601( iso, withoutRecurrence ) )
end

--[[
    Build a new Time from a Wikidata time value
    @param table wikidataValue the time as represented by Wikidata
    @return Time|nil
]]--
function Time.newFromWikidataValue( wikidataValue )
    local definition = parseIso8601( wikidataValue.time )
    definition.precision = wikidataValue.precision

    if  wikidataValue.calendarmodel == 'http://www.wikidata.org/entity/Q1985727' then
        definition.calendar = Time.CALENDAR.GREGORIAN
    elseif  wikidataValue.calendarmodel == 'http://www.wikidata.org/entity/Q1985786' then
        definition.calendar = Time.CALENDAR.JULIAN
    else
        return nil
    end

    return Time.new( definition )
end

--[[
    Return a Time as a ISO 8601 string
    @return string
]]--
function Time:toIso8601()
    local iso = ''
    if self.year ~= nil then
        if self.year < 0 then
             --Years BC are counted since 0 and not -1
            iso = '-' .. prepend(string.format('%.0f', -1 * self.year), '0', 4)
        else
            iso = prepend(string.format('%.0f', self.year), '0', 4)
        end
    end

    --month
    if self.precision < Time.PRECISION.MONTH then
        return iso
    end
    if self.iso ~= '' then
        iso = iso .. '-'
    end
    iso = iso .. prepend( self.month, '0', 2 )

    --day
    if self.precision < Time.PRECISION.DAY then
        return iso
    end
    iso = iso .. '-' .. prepend( self.day, '0', 2 )

    --hour
    if self.precision < Time.PRECISION.HOUR then
        return iso
    end
    iso = iso .. 'T' .. prepend( self.hour, '0', 2 )

    --minute
    if self.precision < Time.PRECISION.MINUTE then
        return iso .. formatUtcOffsetForIso( self.utcoffset )
    end
    iso = iso .. ':' .. prepend( self.minute, '0', 2 )

    --second
    if self.precision < Time.PRECISION.SECOND then
        return iso .. formatUtcOffsetForIso( self.utcoffset )
    end
    return iso .. ':' .. prepend( self.second, '0', 2 ) .. formatUtcOffsetForIso( self.utcoffset )
end

--[[
    Return a Time as a string
    @param mw.language|string|nil language to use. By default the content language.
    @return string
]]--
function Time:toString( language )
    if language == nil then
        language = mw.language.getContentLanguage()
    elseif type( language ) == 'string' then
        language = mw.language.new( language )
    end

    --return language:formatDate( 'r', self:toIso8601() )
    return self:toIso8601()
    --TODO: improve
end

--[[
    Return a Time in HTMl (with a <time> node)
    @param mw.language|string|nil language to use. By default the content language.
    @param table|nil attributes table of attributes to add to the <time> node.
    @return string
]]--
function Time:toHtml( language, attributes )
    if attributes == nil then
        attributes = {}
    end
    attributes['datetime'] = self:toIso8601()
    return mw.text.tag( 'time', attributes, self:toString( language ) )
end

--[[
    All possible precisions for a Time (same ids as Wikibase)
]]--
Time.PRECISION = {
	GY      = 0, --Gigayear
	MY100   = 1, --100 Megayears
	MY10    = 2, --10 Megayears
	MY      = 3, --Megayear
	KY100   = 4, --100 Kiloyears
	KY10    = 5, --10 Kiloyears
	KY      = 6, --Kiloyear
	YEAR100 = 7, --100 years
	YEAR10  = 8, --10 years
	YEAR    = 9,
	MONTH   = 10,
	DAY     = 11,
	HOUR    = 12,
	MINUTE  = 13,
	SECOND  = 14
}

--[[
    Check if the precision is known
    @param number precision ID
    @return boolean
]]--
function Time.knowsPrecision( precision )
	for _,id in pairs( Time.PRECISION ) do
		if id == precision then
			return true
		end
	end
    return false
end

--[[
    Supported calendar models
]]--
Time.CALENDAR = {
	GREGORIAN = 'Gregorian',
	JULIAN    = 'Julian'
}

function Time.age(time1, time2)
    if time2 == nil then
        time2 = Time.newFromIso8601(mw.getContentLanguage():formatDate('c', nil, true), true)
    end
    local age = time2.year - time1.year
    if time2.month < time1.month or (time2.month == time1.month and time2.day < time1.day) then
        age = age - 1
    end
    if time1.year < 0 and time2.year > 0 then
        age = age - 1
    end
    return age
end

--TODO Átszervezni, befejezni
function Time:formatDate(options)
    options = options or {}
    local fd = ''
    if self.precision >= Time.PRECISION.DAY then
        fd = self.year < 0 and 'i. e. ' .. (-1 * self.year) or fd .. self.year
        if options.link ~= 'nem' then fd = '[[' .. fd .. ']]' end
        local d = '2000-' .. prepend(self.month, '0', 2) .. '-' .. prepend(self.day, '0', 2)  -- kamu év
        local lang = mw.getContentLanguage()
        fd = fd .. '. ' .. lang:formatDate(options.link == 'nem' and 'F"&nbsp;"j.' or '[[F j.|F"&nbsp;"j.]]', d)
	elseif self.precision >= Time.PRECISION.MONTH then
		fd = self.year < 0 and 'i. e. ' .. (-1 * self.year) or fd .. self.year
		local month = mw.getContentLanguage():formatDate('F', '2000-' .. self.month)
		if options.link ~= 'nem' then fd = '[[' .. fd .. ']]' end
		fd = fd .. '. ' .. month
    elseif self.precision >= Time.PRECISION.YEAR then
        fd = self.year < 0 and 'i. e. ' .. (-1 * self.year) or fd .. self.year
        if options.link ~= 'nem' then fd = '[[' .. fd .. ']]' end
    elseif self.precision == Time.PRECISION.YEAR10 then
        local year = math.floor((self.year < 0 and -1 * self.year or self.year)  / 10) * 10
        local suffixesRoundBelow100 = {"es", "as", "as", "es", "es", "as", "es", "as", "es"}
        fd = self.year < 0 and 'i. e. ' .. year or tostring(year)
        if year % 10000 == 0 then fd = fd .. '-s'
        elseif year % 1000 == 0 then fd = fd .. '-es'
        elseif year % 100 == 0 then fd = fd .. '-as'
        else fd = fd .. '-' .. suffixesRoundBelow100[year % 100 / 10] end
        fd = fd .. ' évek'
        if options.link ~= 'nem' then fd = '[[' .. fd .. ']]' end
    elseif self.precision == Time.PRECISION.YEAR100 then
        if self.year < 0 then
            fd = 'i. e. ' .. math.floor(-1 * self.year / 100) .. '. század'
        else
            fd = math.floor(self.year / 100) .. '. század'
        end
        if options.link ~= 'nem' then fd = '[[' .. fd .. ']]' end
    elseif self.precision == Time.PRECISION.KY then
        if self.year < 0 then
            fd = 'i. e. ' .. math.floor(-1 * self.year / 1000) .. '. évezred'
        else
            fd = math.floor(self.year / 1000) .. '. évezred'
        end
        if options.link ~= 'nem' then fd = '[[' .. fd .. ']]' end
    else
        fd = tostring(self.year)
    end
    
    if options['életkor'] == 'igen' then
        local property = string.upper(options.property)
        if property == 'P570' then  -- halálozási dátum
            local claim = mw.wikibase.getEntityObject().claims['P569']
            if claim and claim[1].mainsnak.snaktype == 'value' and claim[1].mainsnak.datavalue.value.precision >= Time.PRECISION.YEAR then
                local time = Time.newFromWikidataValue(claim[1].mainsnak.datavalue.value)
                local age = Time.age(time, self)
				if time.precision < self.precision then
					age = (age - 1) .. '-' .. age
				elseif self.precision < time.precision then
					age = age .. '-' .. (age + 1)
				end
                fd = fd .. ' ' .. mw.text.tag('span', {style = 'white-space:nowrap;'}, '(' .. age .. ' évesen)')
            end
        elseif property == 'P569' then  -- születési dátum
            if not mw.wikibase.getEntityObject().claims['P570'] then
                fd = fd .. ' ' .. mw.text.tag('span', {style = 'white-space:nowrap;'}, '(' .. Time.age(self) .. ' éves)')
            end
        end
    end
    return fd
end

return Time