Seba na module şıma şenê yû pela dokumani vırazê Modul:Time/dok

require 'Modul:No globals'

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 eq(t1, t2)
	return (t1.calendar == t2.calendar and t1.year == t2.year
		and t1.month == t2.month and t1.day == t2.day)
end

local function lt(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

local function le(t1, t2)
	return (t1 == t2 or t1 < t2)
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,
		__eq = eq,
		__lt = lt,
		__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'
}

--[[
	Calculate time diff in years between two dates.
	
	@param time1 The former date
	@param time2 The latter date; current date if not given
	
	@return number The number of years between the two dates
	@return string The diff in human-readable format, may be "n-n+1" if
	 the exact number cannot be determined due to the different precisions
]]
function Time.age(time1, time2)
	-- Use current date if latter date not given
	if time2 == nil then
		time2 = Time.newFromIso8601(mw.getContentLanguage():formatDate('c', nil, true), true)
	end
	
	local age = time2.year - time1.year
	
	-- There is no year 0
	if time1.year < 0 and time2.year > 0 then
		age = age - 1
	end
	if time1.precision > Time.PRECISION.MONTH then
		if time2.precision > Time.PRECISION.MONTH then
			if time2.month < time1.month then
				age = age - 1
			elseif time2.month == time1.month and time2.day < time1.day then
				age = age - 1
			end
			return age, tostring(age)
		elseif time2.precision == Time.PRECISION.MONTH then
			if time2.month == time1.month then
				return age - 1, (age-1) .. '-' .. age
			end
			if time2.month < time1.month then
				age = age - 1
			end
			return age, tostring(age)
		end
	elseif time1.precision == Time.PRECISION.MONTH then
		if time2.precision > Time.PRECISION.YEAR then
			if time2.month == time1.month then
				return age, (age-1) .. '-' .. age
			end
			if time2.month < time1.month then
				age = age - 1
			end
			return age, tostring(age)
		end
	end
	return age, (age-1) .. '-' .. 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.ceil(-1 * self.year / 100) .. '. század'
		else
			fd = math.ceil(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.ceil(-1 * self.year / 1000) .. '. évezred'
		else
			fd = math.ceil(self.year / 1000) .. '. évezred'
		end
		if options.link ~= 'nem' then fd = '[[' .. fd .. ']]' end
	else
		fd = tostring(self.year)
	end
	
	if options['életkor'] == 'igen' and self.precision >= Time.PRECISION.YEAR then
		local property = string.upper(options.property)
		if property == 'P570' then  -- halálozási dátum
			local claim = mw.wikibase.getEntity():getBestStatements('P569')[1]
			if claim and claim.mainsnak.snaktype == 'value' and claim.mainsnak.datavalue.value.precision >= Time.PRECISION.YEAR then
				local time = Time.newFromWikidataValue(claim.mainsnak.datavalue.value)
				local _, age = Time.age(time, self)
				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.getEntity().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