Jump to content

Module:Infobox calendar date

Permanently protected module
From Wikipedia, the free encyclopedia

require('strict')

local getArgs = require('Module:Arguments').getArgs
local InfoboxBuilder = require('Module:InfoboxBuilder').InfoboxBuilder
local InfoboxImage = require('Module:InfoboxImage')

local p = {}

local function trim(value)
	if value == nil then
		return nil
	end
	value = mw.text.trim(tostring(value))
	if value == '' then
		return nil
	end
	return value
end

local function yes(value)
	value = trim(value)
	if not value then
		return false
	end
	return value == 'true'
end

local function callTime(frame, format, date)
	return frame:callParserFunction {
		name = '#time',
		args = { format, date }
	}
end

local function expand(frame, title, args)
	return frame:expandTemplate {
		title = title,
		args = args or {}
	}
end

local function dateArgs(date)
	return {
		tostring(date.year),
		tostring(date.month),
		tostring(date.day)
	}
end

local function renderTemplate(frame, date, title, extraArgs)
	local args = dateArgs(date)
	if extraArgs then
		for key, value in pairs(extraArgs) do
			args[key] = value
		end
	end
	return expand(frame, title, args)
end

local function wikilink(target, display)
	if display and display ~= target then
		return string.format('[[%s|%s]]', target, display)
	end
	return string.format('[[%s]]', target)
end

local function yearLink(year, alwaysShowEra, commonEra, julian)
	year = tonumber(year)
	local displayYear = math.abs(year)
	local target = displayYear
	local prefix = ''
	local suffix = ''
	if year <= 0 then
		if commonEra then
			target = displayYear + 1 .. ' BC'
		else
			target = displayYear .. ' BC'
			displayYear = julian and displayYear or displayYear + 1 -- The Gregorian calendar needs adjustment for BC years, but the Julian calendar doesn't
		end
	end

	if commonEra then
		if year < 0 then
			suffix = ' ' .. wikilink('Common Era', 'BCE')
		else
			if alwaysShowEra then suffix = ' ' .. wikilink('Common Era', 'CE') end
		end
	else
		if year <= 0 then
			suffix = ' ' .. wikilink('Before Christ', 'BC')
		else
			if alwaysShowEra then prefix = wikilink('Anno Domini', 'AD') .. ' ' end
		end
	end
	return prefix .. wikilink(target, displayYear) .. suffix
end

local function renderMainDate(frame, date)
	local weekday = callTime(frame, 'l', date.timestamp)
	local day = date.day
	local month = callTime(frame, 'F', date.timestamp)
	local year = date.year
	return string.format("'''" .. '%s, %s %s' .. "'''" , wikilink(weekday), wikilink(day .. ' ' .. month), yearLink(year, false, true))
end

local calendars = {
	{
		key = 'akan',
		label = '[[Akan calendar|Akan]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'AKANCALENDAR')
		end
	},
	{
		key = 'armenian',
		label = '[[Armenian calendar|Armenian]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'ARMENIANDATE', { format = 'DMY' })
		end,
		earliest = { 552, 7, 13 }
	},
	{
		key = 'aztec',
		label = '[[Aztec calendar|Aztec]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'AZTECCALENDAR')
		end
	},
	{
		key = 'babylonian',
		label = '[[Babylonian calendar|Babylonian]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'BABYLONIANDATE', { format = 'DMY' }) .. ' [[Seleucid era|SE]]'
		end,
		earliest = { -310, 3, 29 }
	},
	{
		key = 'pawukon',
		label = '[[Pawukon calendar|Balinese pawukon]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'BALINESEPAWUKONCALENDAR')
		end
	},
	{
		key = 'bengali',
		label = '[[Bengali calendar|Bengali]]',
		render = function(frame, date)
			return string.format('%s, %s %s', renderTemplate(frame, date, 'BENGALIDATE'), wikilink('Bengali calendar', 'BS'), renderTemplate(frame, date, 'BENGALIYEAR'))
		end,
		earliest = { 594, 4, 3 }
	},
	{
		key = 'chinese',
		label = '[[Chinese calendar|Chinese]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'CHINESECALENDAR', { show_ganzhi = 'true', show_jieqi = 'true' })
		end
	},
	{
		key = 'common',
		label = '[[Common Era]]',
		render = function(frame, date)
			local day = date.day
			local month = callTime(frame, 'F', date.timestamp)
			local year = date.year
			return string.format('%s %s', wikilink(day .. ' ' .. month), yearLink(year, true, true))
		end
	},
	{
		key = 'coptic',
		label = '[[Coptic calendar|Coptic]]',
		render = function(frame, date)
			local target = renderTemplate(frame, date, 'COPTICDATE', { format = 'MD' })
			local display = renderTemplate(frame, date, 'COPTICDATE', { format = 'DM' })
			local year = renderTemplate(frame, date, 'COPTICYEAR')
			return string.format('[[%s|%s]], %s %s', target, display, wikilink('Era of the Martyrs', 'AM'), year)
		end,
		earliest = { 284, 8, 29 }
	},
	{
		key = 'egyptian',
		label = '[[Egyptian calendar|Egyptian]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'EGYPTIANDATE', { format = 'DMY' }) .. ' [[Nabonassar era|NE]]'
		end,
		earliest = { -746, 2, 18 }
	},
	{
		key = 'ethiopian',
		label = '[[Ethiopian calendar|Ethiopian]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'ETHIOPIANDATE', { format = 'DMY' }) .. ' Amätä Məhrät'
		end,
		earliest = { 8, 8, 27 }
	},
	{
		key = 'french',
		label = '[[French Republican calendar|French Republican]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'FRENCHREVOLUTIONARYDATE')
		end,
		earliest = { 1792, 9, 22 }
	},
	{
		key = 'gregorian',
		label = '[[Gregorian calendar|Gregorian]]',
		render = function(frame, date)
			local day = date.day
			local month = callTime(frame, 'F', date.timestamp)
			local year = date.year
			return string.format('%s, %s', wikilink(day .. ' ' .. month), yearLink(year, true, false))
		end
	},
	{
		key = 'hebrew',
		label = '[[Hebrew calendar|Hebrew]]',
		render = function(frame, date)
			local day = callTime(frame, 'xjj', date.timestamp)
			local month = callTime(frame, 'xjF', date.timestamp)
			local monthNumber = tonumber(callTime(frame, 'xjn', date.timestamp))
			local year = callTime(frame, 'xjY', date.timestamp)
			local monthTarget = month
			if monthNumber == 10 then
				monthTarget = 'Tammuz (Hebrew month)'
			elseif monthNumber == 11 then
				monthTarget = 'Av (month)'
			end
			local value = string.format('%s %s, %s %s', day, wikilink(monthTarget, month), wikilink('Anno Mundi', 'AM'), year)

			local omer = trim(expand(frame, 'Sefirat HaOmer', { month, day }))
			if omer and mw.ustring.lower(omer) ~= 'none' then
				value = value .. '<br />' .. wikilink('Sefirat HaOmer', 'Omer') .. ' ' .. omer
			end
			return value
		end,
		earliest = { 0, 1, 1 } -- This is the earliest date that can be passed to MediaWiki's time function
	},
	{
		key = 'hindu',
		label = '[[Hindu calendar|Hindu]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'HINDUCALENDAR')
		end,
		earliest = { 79, 3, 13 }
	},
	{
		key = 'icelandic',
		label = '[[Icelandic calendar|Icelandic]]',
		render = function(frame, date)
			return string.format('%s, %s', renderTemplate(frame, date, 'ICELANDICDATE/weekday'), renderTemplate(frame, date, 'ICELANDICDATE'))
		end,
		earliest = { 1, 4, 19 }
	},
	{
		key = 'islamic',
		label = '[[Islamic calendar|Islamic]]',
		render = function(frame, date)
			local day = callTime(frame, 'xmj', date.timestamp)
			local month = callTime(frame, 'xmF', date.timestamp)
			local year = callTime(frame, 'xmY', date.timestamp)
			return string.format('%s %s, %s %s<br />(using %s)', day, wikilink(month), wikilink('Hijri era', 'AH'), year, wikilink('Tabular Islamic calendar', 'tabular method'))
		end,
		earliest = { 622, 7, 16 }
	},
	{
		key = 'iso',
		label = '[[ISO week date]]',
		render = function(frame, date)
			return callTime(frame, 'o-\\WW-N', date.timestamp)
		end,
		earliest = { 0, 1, 1 } -- This is the earliest date that can be passed to MediaWiki's time function
	},
	{
		key = 'japanese',
		label = '[[Japanese calendar|Japanese]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'JAPANESECALENDAR', { format = 'DMY', show_sekki = 'true' })
		end,
		earliest = { 645, 1, 1 }
	},
	{
		key = 'julian',
		label = '[[Julian calendar|Julian]]',
		render = function(frame, date)
			local target = renderTemplate(frame, date, 'JULIANCALENDAR', { format = 'MD' })
			local display = renderTemplate(frame, date, 'JULIANCALENDAR', { format = 'DM' })
			local year = renderTemplate(frame, date, 'JULIANYEAR')
			local annoMundi = renderTemplate(frame, date, 'ANNOMUNDI')
			return string.format('[[%s (Eastern Orthodox liturgics)|%s]], %s (%s&nbsp;%s)', target, display, yearLink(year, true, false, true), wikilink('Anno Mundi', 'AM'), annoMundi)
		end,
		earliest = { -9999, 1, 1 }
	},
	{
		key = 'julianday',
		label = '[[Julian day]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'JD')
		end,
		earliest = { -4712, 1, 1 }
	},
	{
		key = 'korean',
		label = '[[Korean calendar|Korean]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'KOREANCALENDAR', { format = 'DMY' })
		end,
		earliest = { -2332, 2, 15 }
	},
	{
		key = 'maya',
		label = '[[Maya calendar|Maya]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'MAYANCALENDAR')
		end,
		earliest = { -9999, 1, 1 }
	},
	{
		key = 'roman',
		label = '[[Roman calendar|Roman]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'ROMANDATE', { format = 'DMY' }) .. ' [[Ab urbe condita|AUC]]'
		end,
		earliest = { -9999, 1, 1 }
	},
	{
		key = 'solar',
		label = '[[Solar Hijri calendar|Solar Hijri]]',
		render = function(frame, date)
			local day = callTime(frame, 'xij', date.timestamp)
			local month = callTime(frame, 'xiF', date.timestamp)
			local year = callTime(frame, 'xiY', date.timestamp)
			local monthTarget = trim(expand(frame, 'IranianMonth', { month })) or month
			return string.format('%s %s, SH %s', day, wikilink(monthTarget, month), year)
		end,
		earliest = { -9999, 1, 1 }
	},
	{
		key = 'tibetan',
		label = '[[Tibetan calendar|Tibetan]]',
		render = function(frame, date)
			return renderTemplate(frame, date, 'TIBETANDATE')
		end,
		earliest = { -9999, 1, 1 }
	}
}

local function normaliseCalendar(value)
	value = trim(value)
	if not value then
		return nil
	end
	value = mw.ustring.lower(value)
	value = mw.ustring.gsub(value, '[_%-]+', ' ')
	value = mw.ustring.gsub(value, '%s+', ' ')
	return value
end

local function detectDefaultCalendar()
	local title = mw.ustring.lower(mw.title.getCurrentTitle().text)
	local bestMatch
	local bestMatchLength = 0

	for _, calendar in ipairs(calendars) do
		local phrase = calendar.key
		if mw.ustring.find(title, phrase, 1, true) then
			local phraseLength = mw.ustring.len(phrase)
			if phraseLength > bestMatchLength then
				bestMatch = calendar.key
				bestMatchLength = phraseLength
			end
		end
	end

	return bestMatch
end

local function dateIsBefore(date, limit)
	if not limit then
		return false
	end
	if date.year ~= limit[1] then
		return date.year < limit[1]
	end
	if date.month ~= limit[2] then
		return date.month < limit[2]
	end
	return date.day < limit[3]
end

local function dateIsAfter(date, limit)
	if not limit then
		return false
	end
	if date.year ~= limit[1] then
		return date.year > limit[1]
	end
	if date.month ~= limit[2] then
		return date.month > limit[2]
	end
	return date.day > limit[3]
end

local function calendarIsAvailable(calendar, date)
	return not dateIsBefore(date, calendar.earliest) and not dateIsAfter(date, calendar.latest)
end

local function makeOtherCalendars(rows, expanded)
	if #rows == 0 then
		return nil
	end

	local outer = mw.html.create('div')
		:css('width', '100%')
		:css('text-align', 'left')
		:css('border-top', '1px solid var(--border-color-base, #a2a9b1)')
		:css('padding-top', '0.5em')
		:css('margin-top', '0.5em')

	local content = outer
	if not expanded then
		outer:addClass('mw-collapsible mw-collapsed')
		outer:tag('div'):css('text-align', 'center'):css('font-weight', 'bold'):wikitext('Other calendars')
		content = outer:tag('div'):addClass('mw-collapsible-content')
	end

	local tableNode = content:tag('table')
		:css('width', '100%')
		:css('background', 'transparent')
		:css('color', 'inherit')
		:css('margin-top', '0.5em')

	for _, row in ipairs(rows) do
		local tr = tableNode:tag('tr')
		local th = tr:tag('th')
			:attr('scope', 'row')
			:css('text-align', 'left')
			:css('font-weight', 'normal')
		th:wikitext("'''" .. row.label .. "'''")
		tr:tag('td')
			:wikitext(row.value)
	end

	return tostring(outer) .. '<hr />'
end

local function defaultAbove(frame)
	local note = expand(frame, 'Nobold', { '(at [[UTC+00:00|UTC+00]])' })
	return 'Today<br />' .. expand(frame, 'Small', { note })
end

function p.main(frame)
	local args = getArgs(frame)
	local yearText = trim(args[1])
	local monthText = trim(args[2])
	local dayText = trim(args[3])
	local year = tonumber(yearText)
	local month = tonumber(monthText)
	local day = tonumber(dayText)
	if not year or not month or not day then
		return expand(frame, 'Error', { 'Invalid or missing year, month or day' })
	end
	local date = { year = year, month = month, day = day, timestamp = yearText .. '-' .. monthText .. '-' .. dayText }

	local defaultCalendar = normaliseCalendar(args.default_calendar)
	if not defaultCalendar then
		defaultCalendar = detectDefaultCalendar()
	end
	local defaultCalendar2 = normaliseCalendar(args.default_calendar2)

	local selected = {}
	if defaultCalendar then
		selected[defaultCalendar] = true
	end
	if defaultCalendar2 then
		selected[defaultCalendar2] = true
	end

	local rowsByKey = {}
	local otherRows = {}

	for _, calendar in ipairs(calendars) do
		if calendarIsAvailable(calendar, date) then
			local value = trim(calendar.render(frame, date))
			if value then
				local row = { label = calendar.label, value = value }
				rowsByKey[calendar.key] = row
				if not selected[calendar.key] then
					otherRows[#otherRows + 1] = row
				end
			end
		end
	end

	local builder = InfoboxBuilder.new()
		:title(trim(args.name))
		:above(trim(args.above) or defaultAbove(frame))

	if trim(args.image) then
		local imageFrame = frame:newChild { args = { image = args.image, upright = '1.1' } }
		builder:image { image = InfoboxImage.InfoboxImage(imageFrame), caption = args.caption }
	end

	builder:subheader(renderMainDate(frame, date))

	local addedPrimary = {}
	local function addPrimary(key)
		if not key or addedPrimary[key] then
			return
		end
		addedPrimary[key] = true
		local row = rowsByKey[key]
		if row then
			builder:row { label = row.label, data = row.value }
		end
	end

	addPrimary(defaultCalendar)
	addPrimary(defaultCalendar2)

	local other = makeOtherCalendars(otherRows, yes(args.all))
	if other then
		builder:row { data = other }
	end
	builder:below('[' .. expand(frame, 'Purge', { 'refresh' }) .. ']')

	return builder:build()
end

return p