Vorlagenprogrammierung Diskussionen Lua Unterseiten
Modul Deutsch English

Modul: Dokumentation

Diese Seite enthält Code in der Programmiersprache Lua. Einbindungszahl Cirrus


--[=[ Time 2022-10-31
Modul for processing time information; point and span objects

Author: Vollbracht

== for use in other modules ==
* constant.PRECISIONLEVEL	table of valid precision values
			constants:
				MIN		lowest valid precision level (value for t.precision)
				MINUSE	lowest index of PRECISIONLEVEL table
				MAX		highest valid precision level
				[MINUSE] .. [MAX]	precision level name (unit)
			function
				unit(level)	unit name of smallest significant time element
* point	object representing a point of time
			fields:	year, month, day, hour, min, sec,
					precision, timezone, calendarmodel
			constructor:
				Time:new(value)		processes snack.datavalue.value e.g.
			method:
				Time:format(fmtStr)		string representation for time object
				Time:isLessthan(time)	comparator method
				Time:equals(time)		comparator method

]=]

local p = {
	constant = {
		PRECISIONLEVEL = {
			MIN = 0,		-- lowest valid precision
			MINUSE = 9,		-- lowest available index
			MAXUSE = 14,	-- highest valid precision
			MAX=15,			-- highest available index
			-- unused: 'E8', 'E7', 'E6', 'E5', 'E4',
			-- [6]='millennium', [7]='century', [8]='decade',
			[9]='year', [10]='month', [11]='day',
			[12]='hour', [13]='min', [14]='sec', [15]='msec',
		},
		DEFAULTFORMAT = {
			[6]='yyyye=( v.Chr.)', [7]='yyyye=( v.Chr.)', [9]='yyyye=( v.Chr.)',
			[9]='yyyye=( v.Chr.)', [10]='mm/yyyy', [11]='dd.mm.yyyy',
			[12]='HH Uhr', [13]='HH:MM Uhr',
			[14]='E=(+)e=(-)yyyy-mm-ddTHH:MM:SSZ'
		},
		URI = {
			GREGORIAN = "http://www.wikidata.org/entity/Q1985727",
			JULIAN = "http://www.wikidata.org/entity/Q1985786"
		},
		MONTHLENGTH = {31,28,31,30,31,30,31,31,30,31,30,31}
	},
	point = {},
	duration = {}
}

p.constant.STARTOFGREGORIAN = {
	era = '+',
	year = 1582,
	month = 8,
	day = 15,
	precision = 11,
	timezone = 0,
	calendarmodel = p.constant.URI.GREGORIAN,
	__index = function(table, key)
		return p.point[key]
	end
}
setmetatable(p.constant.STARTOFGREGORIAN, p.point)

--[[
	p.constant.PRECISIONLEVEL
	constant constructor:
]]
local precisionlevel = function()
	o = {}
	o.__index = function(table, key)
		return p.constant.PRECISIONLEVEL[key]
	end
	o.unit = function(this, level)
		if level < o.MIN then return '' end
		if level < o.MINUSE then return o[o.MINUSE] end
		if level > o.MAXUSE then return o[o.MAXUSE] end
		local result = o[i]
		if result then return result end
	end
	return o
end

---------------------- point ----------------------
-- Point of time
----------------------

local autoprecision = function(o, default)
	if o.precision then return o.precision end
	if o.sec then return 14 end
	if o.min then return 13 end
	if o.hour then return 12 end
	if o.day then return 11 end
	if o.month then return 10 end
	return default
end	

--[[
	constructor
	parameter:
		source:	either string that can be processed by
					mw.language.getContentLanguage():formatDate
				or table containing time information:
					In this case negative julianian dates are accepted with an
					offset of -1: 0 is translated to 1 BC, -1 = 2 BC, etc.
					ATTENTION: This is usual but not in wikidata
]]
function p.point:new(source)
	local o = nil
	if source then
		if type(source) == 'table' then
			o = source
			if o.year then
				if o.year <= 0 then
					o.era = '-'
					o.year = 1 - o.year
					o.calendarmodel = p.constant.URI.JULIAN
					o.BC = true
				else
					if not o.era then o.era = '+' end
					if o.BC then o.BC = false end
				end
				o.precision = autoprecision(o, 9)
			else
				o.era = 'n'
				if o.BC then o.BC = false end
				o.precision = autoprecision(o, 0)
			end
		else
			o = {}
			local fsIn = "+Y-m-d\\TH:i:s\\Z"
			local fsOut = "(.)(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z"
			local lang = mw.language.getContentLanguage()
			local success, t = pcall(lang.formatDate, lang, fsIn, source)
			o.valid = success
			if success then
			o.era, o.year, o.month, o.day, o.hour, o.min, o.sec = t:match(fsOut)
				o.year = tonumber(o.year)
				o.month = tonumber(o.month)
				o.day = tonumber(o.day)
				o.hour = tonumber(o.hour)
				o.min = tonumber(o.min)
				o.sec = tonumber(o.sec)
			else
				o.year = 0
				o.month = 0
				o.day = 0
			end
			if t:match('00:00:00Z') then o.precision = 11
			elseif t:match('00:00Z') then o.precision = 12
			elseif t:match('00Z') then o.precision = 13
			else o.precision = 14
			end
		end
		setmetatable(o, self)
		self.__index = self
		if not o.calendarmodel then
			if o < p.constant.STARTOFGREGORIAN then
				o.calendarmodel = p.constant.URI.JULIAN
				o.BC = true
			else
				o.calendarmodel = p.constant.URI.GREGORIAN
			end
		end
		return o
	else
		local d = os.date("!*t")
		for k, v in pairs(p.point) do d[k] = v end
		d.era = '+'
		d.precision = 14
		d.timezone = 0
		d.calendarmodel = p.constant.URI.GREGORIAN
		return d
	end
end

--[[
	days(pointOfTime)
	theoretical number of days since 1.1.0000
	returns:	positive integer or nil (year before christian counting)
]]
local days = function(point)
	if not point.year then return nil end
	if point.year < 1 then return nil end
	if not point.month then return nil end
	if not point.day then return nil end
	local yb = point.year - 1
	local list = {	yb, yb,
					point.year, point.year, point.year, point.year, point.year,
					point.year, point.year, point.year, point.year, point.year}
	local y = list[point.month]
	list = {0, 31, 59, 90, 120, 151, 181, 212, 242, 273, 303, 334}
	local s = math.floor(y / 4)
	mw.log('days by ')
	mw.log(point.calendarmodel)
	if point.calendarmodel == p.constant.URI.GREGORIAN then
		s = s - math.floor(y / 100)
		s = s + math.floor(y / 400)
	else
		s = s -2
	end
	return yb * 365 + s + list[point.month] + point.day
end

--[[
	pointOfTime:dayOfTheWeek
	returns:	number from 1 (sunday) to 7 (saturday) or
				nil (year before christian counting)
]]
p.point.dayOfTheWeek = function(this)
	local d = days(this)
	if d then return (days(this)+1) % 7 + 1
	else return nil end
end

--[[
	pointOfTime:setPrecision(level)
	pointOfTime.precision initialization by date string merely is a guess
]]
p.point.setPrecision = function(this, level)
	this.precision = level
end

local byDays = function(days, calendar)
	if not days then return nil end
	local o = {}
	if calendar then o.calendarmodel = calendar
	elseif days >= 577675 then o.calendarmodel = p.constant.URI.GREGORIAN
	else o.calendarmodel = p.constant.URI.JULIAN end
	o.era = '+'
	local febl = 28
	if o.calendarmodel == p.constant.URI.GREGORIAN then
		local yb = math.floor(days / 365.2425)
		days = days - math.floor(yb * 365.2425)
		o.year = yb + 1
		if (o.year % 400 == 0 or o.year % 100 > 0 and o.year % 4 == 0)
		and days > 60 then
			days = days - 1
			febl = 29
		end
	else 
		o.BC = true
		local yb = math.floor(days / 365.25)
		days = days - math.floor(yb * 365.25) + 2
		o.year = yb + 1
		if o.year % 4 == 0 and days > 60 then
			days = days - 1
			febl = 29
		end
	end
	list = {334, 303, 273, 242, 212, 181, 151, 120, 90, 59, 31, 0}
	local i = 1
	while not o.day do
		if days > list[i] then
			o.month = 13 - i
			o.day = days - list[i]
		end
		i = i + 1
	end
	list = {31, febl, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
	if o.day > list[o.month] then
		o.day = o.day - list[o.month]
		o.month = o.month + 1
	end
	if o.month > 12 then
		o.month = 1
		o.year = o.year + 1
	end
	o.precision = 11
	return p.point:new(o)
end

--[[
	pointOfTime:format(fmtStr)
	return formated date time value based on given format string
	parameters:
		fmtStr:	limited format string evaluating each appearance of keys:
				either
					%y or %Y for year
					%m for month
					%d for day of month
					%H for 24-hour hour value
					%M for minute
					%S for second
				or
					yy or yyyy for year
					m or mm for month
					d or dd for day of month
					H or HH for 24-hour hour value
					M or MM for minute
					S or SS for second
					e=(<content>)	for content string appearing
									if era is '-' (BC)
					E=(<content>)	for content string appearing
									if era is '+' (CE)
		returns	date time value string with all requested information or ''
]]
p.point.format = function(this, fmtStr)
	function d2(value)
		if value > 9 then return value end
		return '0' .. value
	end
	if not fmtStr then
		if this.precision then
			fmtStr = p.constant.DEFAULTFORMAT[this.precision]
			if not fmtStr then return '' end
		else return '' end
	end
	if fmtStr:match('%%') then
		if this.year then
			fmtStr = fmtStr:gsub('%%Y', this.year)
			if this.year < 100 then fmtStr = fmtStr:gsub('%%y', this.year)
			else fmtStr = fmtStr:gsub('%%y', d2(this.year % 100)) end
		else
			if fmtStr:match('%%[yY]') then return '' end
		end
		if this.month then fmtStr = fmtStr:gsub('%%m', d2(this.month))
		else if fmtStr:match('%%m') then return '' end end
		if this.month then fmtStr = fmtStr:gsub('%%d', d2(this.day))
		else if fmtStr:match('%%d') then return '' end end
		if this.month then fmtStr = fmtStr:gsub('%%H', d2(this.hour))
		else if fmtStr:match('%%H') then return '' end end
		if this.month then fmtStr = fmtStr:gsub('%%M', d2(this.min))
		else if fmtStr:match('%%M') then return '' end end
		if this.month then fmtStr = fmtStr:gsub('%%S', d2(this.sec))
		else if fmtStr:match('%%S') then return '' end end
	else
		if this.year then
			fmtStr = fmtStr:gsub('yyyy', this.year)
			fmtStr = fmtStr:gsub('jjjj', this.year)
			if this.year < 100 then
				fmtStr = fmtStr:gsub('yy', this.year)
				fmtStr = fmtStr:gsub('jj', this.year)
			else
				fmtStr = fmtStr:gsub('yy', d2(this.year % 100))
				fmtStr = fmtStr:gsub('jj', d2(this.year % 100))
			end
		else if fmtStr:match('[jy][jy]') then return '' end end
		if this.month then
			fmtStr = fmtStr:gsub('mm', d2(this.month))
			fmtStr = fmtStr:gsub('m', this.month)
		else if fmtStr:match('m') then return '' end end
		if this.day then
			fmtStr = fmtStr:gsub('dd', d2(this.day))
			fmtStr = fmtStr:gsub('tt', d2(this.day))
			fmtStr = fmtStr:gsub('d', this.day)
			fmtStr = fmtStr:gsub('t', this.day)
		else if fmtStr:match('[dt]') then return '' end end
		if this.hour then
			fmtStr = fmtStr:gsub('HH', d2(this.hour))
			fmtStr = fmtStr:gsub('H', this.hour)
		else if fmtStr:match('H') then return '' end end
		if this.min then
			fmtStr = fmtStr:gsub('MM', d2(this.min))
			fmtStr = fmtStr:gsub('M', this.min)
		else if fmtStr:match('M') then return '' end end
		if this.sec then
			fmtStr = fmtStr:gsub('SS', d2(this.sec))
			fmtStr = fmtStr:gsub('S', this.sec)
		else if fmtStr:match('S') then return '' end end
		if this.era then
			if this.era == '+' then
				fmtStr = fmtStr:gsub('e=%(.*%)', '')
				fmtStr = fmtStr:gsub('E=%((.*)%)', '%1')
			else
				fmtStr = fmtStr:gsub('E=%(.*%)', '')
				fmtStr = fmtStr:gsub('e=%((.*)%)', '%1')
			end
		end
	end
	return fmtStr
end

--[[
	pointOfTime.isLessthan(a, b)
	datetime compare
	parameters:
		a, b	2 pointOfTime objects
	returns:	true	if on lowest precision level a is before b
				false	if necessary properties inavailable or
						if a is same time or later than b on given
						precision level
]]
p.point.isLessthan = function(a, b)
	if not a then return false end
	if not b then return false end
	local precision = 11
	if a.precision then precision = a.precision end
	if b.precision and b.precision < precision then
		precision = b.precision
	end
	if a.era and b.era and a.era ~= 'n' and b.era ~= 'n' then
		if a.era ~= b.era then return b.era == '+' end
		if a.year and b.year then
			local na = tonumber(a.year)
			local nb = tonumber(b.year)
			if na ~= nb then return (a.era == '+') == (na < nb) end
		else return false end
	end
	for i = 10, 14 do
		if precision < i then return false end
		local aa = a[p.constant.PRECISIONLEVEL[i]]
		local ab = b[p.constant.PRECISIONLEVEL[i]]
		if aa and ab then
			local na = tonumber(aa)
			local nb = tonumber(ab)
			if na ~= nb then return na < nb end
		else return false end
		i = i + 1
	end
	return false
end
--[[
	pointOfTime.LT(a, b)
	comparator function for sort e.g.
]]
p.point.__lt = function(a, b)
	if a.time then a = a.time end
	if b.time then b = b.time end
	if not a.isLessthan then return false end
	return a:isLessthan(b)
end
--[[
	pointOfTime.LE(a, b)
	comparator function
]]
p.point.__le = function(a, b)
	if a.time then a = a.time end
	if b.time then b = b.time end
	if not b.isLessthan then return false end
	return not b:isLessthan(a)
end
--[[
	pointOfTime.GT(a, b)
	comparator function
]]
p.point.__gt = function(a, b)
	if a.time then a = a.time end
	if b.time then b = b.time end
	if not b.isLessthan then return false end
	return b:isLessthan(a)
end
--[[
	pointOfTime.LE(a, b)
	comparator function
]]
p.point.__ge = function(a, b)
	if a.time then a = a.time end
	if b.time then b = b.time end
	if not a.isLessthan then return false end
	return not a:isLessthan(b)
end

--[[
	Time.equals(a, b)
	compares any objects in terms of time properties
	parameters:	two of any kind
	returns:	true	*	if both objects represent the same time at lowest
							common precision level
						*	if both objects lack the same time properties
						*	if both are no objects but of simple data type
				false	*	if the objects represent different times at lowest
							common precision level
						*	if only one lacks a relevant time property
						*	if only one is of simple data type even if the other
							doesn't contain time properties at all
]]
p.point.equals = function(a, b) --####
	if not a and not b then return true end
	if not a then return false end
	if not b then return false end
	mw.logObject(a, '===== a')
	mw.logObject(b, '===== b')
	if type(a) ~= 'table' then return type (b) == 'table' end
	if type (b) ~= 'table' then return false end
	local precision = 11
	if a.precision then precision = a.precision end
	if b.precision and b.precision < precision then
		precision = b.precision
	end
	if a.era then
		if a.era == '+' then
			if b.era and b.era ~= '+' then return false end
		else
			if b.era then
				if b.era == '+' then return false end
			else return false end
		end
	else
		if b.era and b.era ~= '+' then return false end
	end
	if precision < 9 then precision = 9 end
	for i = 9, precision do
		local aa = a[p.constant.PRECISIONLEVEL[i]]
		local ab = b[p.constant.PRECISIONLEVEL[i]]
		if not aa ~= not ab then return false end
		if aa and aa ~= ab then return false end
		i = i + 1
	end
	return true
end
p.point.__eq = p.point.equals
p.point.__ne = function(this)
	return not p.point.equals(this)
end

--[[
	pointOfTime:gregorian()
	gregorian variant of a julian date
]]
p.point.gregorian = function(this)
	if this.calendarmodel == p.constant.URI.GREGORIAN then return this end
	local result = {}
	for k, v in pairs(this) do result[k] = v end
	setmetatable(result, self)
	self.__index = self
	local yDif = this.year - 1600
	local sdty = 0
	local sdtc = 0
	local cDif = 0
	local dDif = 0
	if this.year % 100 == 0 then
		if this.month > 2 then sdty = 1 end
		cDif = yDif / 100
		if yDif > 0 then
			
			if this.month > 2 then sdtc = 1 end
		else
			if this.month <= 2 then sdtc = -1 end
		end
	else
		if this.year % 4 == 0 and this.month > 2 then sdty = 1 end
		cDif = math.floor(yDif / 100)
	end
	local cDif = 10 + cDif - math.floor(yDif / 400)
end

---------------------- template service ----------------------
-- test, etc.
----------------------

local testLine = function(a, b, c)
	local result = '<tr style="background-color:'
	if type(b) == 'boolean' then if b then b = 'true' else b = 'false' end
	elseif b == nil then b = 'nil'
	elseif type(b) == 'table' then
		local t = b.format
		if t then b = t(b) end
	end
	if type(c) == 'boolean' then if c then c = 'true' else c = 'false' end
	elseif c == nil then c = 'nil'
	elseif type(c) == 'table' then
		local t = c.format
		if t then c = t(c) end
	end
	if b == c then result = result .. '#4f4">'
	else result = result .. '#f44">' end
	return	result .. '<td>' .. a .. '</td><td>' .. b .. '</td><td>' .. c
		..	'</td></tr>'
end

p.test = function()
	local result = '<table class="wikitable">'
	local a = p.point:new('1.3.2000')
	mw.logObject(a)
	result = result .. testLine("a:format('mm-dd')", a:format('mm-dd'), '03-01')
	local b = p.constant.STARTOFGREGORIAN
	result = result .. testLine("a, b", a, b)
	result = result .. testLine("a < b", a < b, false)
	result = result .. testLine("a > b", a > b, true)
	result = result .. testLine("a == b", a == b, false)
	result = result .. testLine("a ~= b", a ~= b, true)
	a = p.point:new('1582-08-15')
	result = result .. testLine("a, b", a, b)
	result = result .. testLine("a < b", a < b, false)
	result = result .. testLine("a == b", a == b, true)
	result = result .. testLine("a ~= b", a ~= b, false)
	a = p.point:new()
	result = result .. testLine("today", a:format('d.m.yyyy'), '2.11.2022')
	result = result .. testLine("day of the week", a:dayOfTheWeek(), 4)
	result = result .. testLine("days of gregor", days(b), 577675)
	result = result .. testLine("day of the week", b:dayOfTheWeek(), 2)
	a = p.point:new('1582-08-04')
	result = result .. testLine("days before", days(a), 577674)
	result = result .. testLine("day of the week", a:dayOfTheWeek(), 1)
	return result .. '</table>'
end

return p