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