Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

Jump to content

Module:Calendar

Permanently protected module
From Wikipedia, the free encyclopedia

local getArgs = require('Module:Arguments').getArgs
local calendrical = require('Module:Calendrical Calculations')

local p = {}
local monthNames = {'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'}

local falseValues = {
	['0'] = true,
	['false'] = true,
	['no'] = true,
	['n'] = true,
	['off'] = true
}

local function callCalendrical(frame, functionName, ...)
	local childArgs = {}
	for i = 1, select('#', ...) do
		childArgs[i] = tostring(select(i, ...))
	end

	local childFrame = frame:newChild { title = 'Module:Calendrical Calculations', args = childArgs }
	local func = calendrical[functionName]

	return func(childFrame)
end

local function parseDateTriple(value, functionName)
	value = mw.text.trim(tostring(value))
	local year, month, day = value:match('^(-?%d+)%-(%d+)%-(%d+)$')

	if not year then
		error(string.format('Module:Calendrical Calculations returned an unexpected value from %s: %s', functionName, value), 0)
	end

	return { year = tonumber(year), month = tonumber(month), day = tonumber(day) }
end

local function booleanArg(value, default)
	if value == nil then
		return default
	end

	value = mw.ustring.lower(mw.text.trim(tostring(value)))
	if value == '' then
		return default
	end

	return not falseValues[value]
end

local function firstDefined(args, ...)
	for i = 1, select('#', ...) do
		local value = args[select(i, ...)]
		if value ~= nil then
			return value
		end
	end
	return nil
end

local function firstNonblank(args, ...)
	for i = 1, select('#', ...) do
		local value = args[select(i, ...)]
		if value ~= nil and mw.text.trim(tostring(value)) ~= '' then
			return value
		end
	end
	return nil
end

local function sourceDateFromArgs(args)
	local year = tonumber(args[1])
	local month = tonumber(args[2])
	local day = tonumber(args[3])

	if not year or not month or not day or year % 1 ~= 0 or month % 1 ~= 0 or day % 1 ~= 0 then
		error('The year, month and day must all be integers', 0)
	end

	return { year = year, month = month, day = day }
end

-- The Julian functions in Module:Calendrical Calculations do not use a year zero, but #time and this module do
local function toCalendricalJulianYear(year)
	if year <= 0 then
		return year - 1
	end
	return year
end

local function fromCalendricalJulianYear(year)
	if year < 0 then
		return year + 1
	end
	return year
end

local function normalizeFormat(value)
	value = mw.ustring.upper(mw.text.trim(tostring(value or '')))
	if value == 'MDY' then
		return 'MDY'
	end
	return 'DMY'
end

local function normalizeCalendar(value, default)
	value = mw.ustring.lower(mw.text.trim(tostring(value or '')))
	if value == '' then
		return default
	end
	if value == 'g' or value == 'gregorian' then
		return 'g'
	end
	if value == 'j' or value == 'julian' then
		return 'j'
	end
	if value == 'a' or value == 'as' or value == 'annunciation' or value == 'annunciation style' then
		return 'a'
	end
	if value == 'n' or value == 'nativity' or value == 'nativity style' then
		return 'n'
	end
	error(string.format('Unknown calendar code: %s', value), 0)
end

local function normalizeYearMarker(value)
	if value == nil then
		return nil
	end

	value = mw.text.trim(tostring(value))
	if value == '' or value == '0' then
		return nil
	end
	if value == '1' then
		return 'year'
	end
	return value
end

local function optionsFromArgs(args)
	return {
		dateFormat = normalizeFormat(args.format or args[4]),
		showYear = booleanArg(firstDefined(args, 'showyear', 'year'), true),
		styleLink = booleanArg(firstDefined(args, 'stylelink', 'link'), true),
		linkDay = booleanArg(args.wd, false),
		linkMonth = booleanArg(args.wm, false),
		linkYear = booleanArg(args.wy, false),
		squareBrackets = booleanArg(args.sb, true),
		yearMarker = normalizeYearMarker(args.ym)
	}
end

local function yearText(year)
	if year <= 0 then
		return string.format('%d BC', 1 - year)
	end
	return tostring(year)
end

local function formatDay(date, shouldLink)
	local display = tostring(date.day)
	if not shouldLink then
		return display
	end

	local target = string.format('%s %d', monthNames[date.month], date.day)
	return string.format('[[%s|%s]]', target, display)
end

local function formatMonth(date, shouldLink)
	local month = monthNames[date.month]
	if shouldLink then
		return string.format('[[%s]]', month)
	end
	return month
end

local function formatYear(year, shouldLink, marker)
	local display = yearText(year)
	if shouldLink then
		display = string.format('[[%s]]', display)
	end
	if marker then
		display = string.format('%s %s', display, marker)
	end
	return display
end

local function formatWithoutYear(date, dateFormat, links)
	local day = formatDay(date, links.day)
	local month = formatMonth(date, links.month)

	if dateFormat == 'MDY' then
		return string.format('%s&nbsp;%s', month, day)
	end
	return string.format('%s&nbsp;%s', day, month)
end

local function formatWithYear(date, dateFormat, links, marker)
	local withoutYear = formatWithoutYear(date, dateFormat, links)
	local year = formatYear(date.year, links.year, marker)

	if dateFormat == 'MDY' then
		return string.format('%s, %s', withoutYear, year)
	end
	return string.format('%s %s', withoutYear, year)
end

local function styleLabel(abbreviation, article, linked)
	if linked then
		return string.format('[[%s|%s]]', article, abbreviation)
	end
	return abbreviation
end

local function dateTooltip(text, calendar)
	if calendar == 'a' then
		return string.format('<span title="Annunciation Style date">%s</span>', text)
	end
	if calendar == 'n' then
		return string.format('<span title="Nativity Style date">%s</span>', text)
	end
	return text
end

local function wrapAlternative(text, style, squareBrackets)
	if squareBrackets then
		return string.format('&#91;%s %s&#93;', style, text)
	end
	return string.format('(%s %s)', style, text)
end

local noLinks = { day = false, month = false, year = false }

local function gregorianLinks(options)
	return {
		day = options.linkDay,
		month = options.linkMonth,
		year = options.linkYear
	}
end

local function formatConversion(source, converted, options, abbreviation, article, sourceIsGregorian, outputIsGregorian, sourceCalendar, outputCalendar)
	local links = gregorianLinks(options)
	local sourceLinks = sourceIsGregorian and links or noLinks
	local convertedLinks = outputIsGregorian and links or noLinks
	local style = styleLabel(abbreviation, article, options.styleLink)

	if source.year ~= converted.year then
		local sourceText
		local convertedText

		if options.showYear then
			sourceText = formatWithYear(source, options.dateFormat, sourceLinks, options.yearMarker)
			convertedText = formatWithYear(converted, options.dateFormat, convertedLinks, options.yearMarker)
		else
			sourceText = formatWithoutYear(source, options.dateFormat, sourceLinks)
			convertedText = formatWithoutYear(converted, options.dateFormat, convertedLinks)
		end
		sourceText = dateTooltip(sourceText, sourceCalendar)
		convertedText = dateTooltip(convertedText, outputCalendar)

		return string.format('%s %s', sourceText, wrapAlternative(convertedText, style, options.squareBrackets))
	end

	local sourceText = dateTooltip(formatWithoutYear(source, options.dateFormat, sourceLinks), sourceCalendar)
	local convertedText = dateTooltip(formatWithoutYear(converted, options.dateFormat, convertedLinks), outputCalendar)
	local result = string.format('%s %s', sourceText, wrapAlternative(convertedText, style, options.squareBrackets))

	if options.showYear then
		local linkSharedYear = options.linkYear and (sourceIsGregorian or outputIsGregorian)
		result = result .. (options.dateFormat == 'MDY' and ', ' or ' ') .. formatYear(source.year, linkSharedYear, options.yearMarker)
	end

	return result
end

local function isBeforeAnnunciationNewYear(month, day)
	return month < 3 or (month == 3 and day < 25)
end

local function isAtOrAfterNativityNewYear(month, day)
	return month == 12 and day >= 25
end

local function outputCalendarFromArgs(args, sourceCalendar)
	local value = firstNonblank(args, 'out', 'output', 'to')
	local default = sourceCalendar == 'g' and 'j' or 'g'
	return normalizeCalendar(value, default)
end

local function copyDate(date)
	return { year = date.year, month = date.month, day = date.day }
end

local function calendricalSource(date, calendar)
	local converted = copyDate(date)

	if calendar == 'a' then
		if isBeforeAnnunciationNewYear(converted.month, converted.day) then
			converted.year = converted.year + 1
		end
		calendar = 'j'
	elseif calendar == 'n' then
		if isAtOrAfterNativityNewYear(converted.month, converted.day) then
			converted.year = converted.year - 1
		end
		calendar = 'j'
	end

	if calendar == 'j' then
		converted.year = toCalendricalJulianYear(converted.year)
		return converted, 'julian'
	end
	return converted, 'gregorian'
end

local function calendricalOutput(date, calendar)
	if calendar == 'g' then
		return date
	end

	date.year = fromCalendricalJulianYear(date.year)
	if calendar == 'a' and isBeforeAnnunciationNewYear(date.month, date.day) then
		date.year = date.year - 1
	elseif calendar == 'n' and isAtOrAfterNativityNewYear(date.month, date.day) then
		date.year = date.year + 1
	end
	return date
end

local function convertDate(frame, source, sourceCalendar, outputCalendar)
	local calculationSource, sourceName = calendricalSource(source, sourceCalendar)
	local outputName = outputCalendar == 'g' and 'gregorian' or 'julian'
	local result = callCalendrical(frame, 'convert', sourceName, outputName, calculationSource.year, calculationSource.month, calculationSource.day)
	local converted = parseDateTriple(result, 'convert')
	return calendricalOutput(converted, outputCalendar)
end

local function renderConversion(frame, args, forcedSourceCalendar, forcedOutputCalendar)
	local source = sourceDateFromArgs(args)
	local sourceCalendar = forcedSourceCalendar or normalizeCalendar(firstNonblank(args, 'cal', 'calendar'), 'j')
	local outputCalendar = forcedOutputCalendar or outputCalendarFromArgs(args, sourceCalendar)
	local converted = convertDate(frame, source, sourceCalendar, outputCalendar)
	local outputIsGregorian = outputCalendar == 'g'

	return formatConversion(source, converted, optionsFromArgs(args), outputIsGregorian and 'N.S.' or 'O.S.', outputIsGregorian and 'New Style' or 'Old Style', sourceCalendar == 'g', outputIsGregorian, sourceCalendar, outputCalendar)
end

function p.convert(frame)
	return renderConversion(frame, getArgs(frame))
end

return p