Module:Calendar
Appearance
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 %s', month, day)
end
return string.format('%s %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('[%s %s]', 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