Module:Time
Apparence
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" "j.' or '[[F j.|F" "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