Module:Calendar date/sandbox
Appearance
![]() | This is the module sandbox page for Module:Calendar date (diff). |
![]() | This module is rated as alpha. It is ready for third-party input, and may be used on a few pages to see if problems arise, but should be watched. Suggestions for new features or changes in their input and output mechanisms are welcome. |
![]() | This module depends on the following other modules: |
This module implements Template:Calendar date (talk · links · edit).
Usage
{{#invoke:Calendar date|function_name}}
--[[
Display Gregorian date of a holiday that moves year to year. Date data can be obtained from multiple sources as configured in ~/Configuration.js
"localfile" = local JSON data file (eg. https://en.wikipedia.org/wiki/Template:Calendar_date/holidays/Hanukkah.js)
"calculator" = user-supplied date calculator (eg. )
"wikidata" = for holidays with their own date entity page such as https://www.wikidata.org/wiki/Q51224536
]]
local p = {}
--[[--------------------------< inlineError >-----------------------
Critical error. Render output completely in red. Add to tracking category.
]]
local function inlineError(arg, msg, tname)
track["Category:Calendar date template errors"] = 1
return '<span style="font-size:100%" class="error citation-comment">Error in {{' .. tname .. '}} - Check <code style="color:inherit; border:inherit; padding:inherit;">|' .. arg .. '=</code> ' .. msg .. '</span>'
end
--[[--------------------------< trimArg >-----------------------
trimArg returns nil if arg is "" while trimArg2 returns 'true' if arg is ""
trimArg2 is for args that might accept an empty value, as an on/off switch like nolink=
]]
local function trimArg(arg)
if arg == "" or arg == nil then
return nil
else
return mw.text.trim(arg)
end
end
local function trimArg2(arg)
if arg == nil then
return nil
else
return mw.text.trim(arg)
end
end
--[[--------------------------< tableLength >-----------------------
Given a 1-D table, return number of elements
]]
local function tableLength(T)
local count = 0
for _ in pairs(T) do count = count + 1 end
return count
end
--[[--------------------------< createTracking >-----------------------
Return data in track[] ie. tracking categories
]]
local function createTracking()
local sand = ""
if tableLength(track) > 0 then
for key,_ in pairs(track) do
sand = sand .. "[[" .. key .. "]]"
end
end
return sand
end
--[[--------------------------< verifyDate >-----------------------
Given the date arg, return true if within date range 2000-2050 else return false
]]
local function verifyDate(date)
if not date or date == "" then
return nil
end
if tonumber(date) > 1999 and tonumber(date) < 2100 then
return "true"
else
return nil
end
end
--[[--------------------------< makeDate >-----------------------
Given a zero-padded 4-digit year, 2-digit month and 2-digit day, return a full date in df format
df = mdy, dmy, iso, ymd
]]
local function makeDate(year, month, day, df, format)
local formatFull = {
['dmy'] = 'j F Y',
['mdy'] = 'F j, Y',
['ymd'] = 'Y F j',
['iso'] = 'Y-m-d'
}
local formatInfobox = {
['dmy'] = 'j F',
['mdy'] = 'F j',
['ymd'] = 'F j',
['iso'] = 'Y-m-d'
}
if not year or year == "" or not month or month == "" or not day or day == "" and format[df] then
return nil
end
local date = table.concat ({year, month, day}) -- assemble iso format date
if format ~= "infobox" then
return mw.getContentLanguage():formatDate (formatFull[df], date)
else
return mw.getContentLanguage():formatDate (formatInfobox[df], date)
end
end
--[[--------------------------< dateOffset >-----------------------
Given a 'origdate' in ISO format, return the date offset by number of days in 'offset'
eg. given "2018-02-01" and "-1" it will return "2018-01-30"
On error, return origdate
]]
function dateOffset(origdate, offset)
local year, month, day = origdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
local now = os.time{year = year, month = month, day = day}
local newdate = os.date("%Y-%m-%d", now + (tonumber(offset) * 24 * 3600))
return newdate and newdate or origdate
end
--[[--------------------------< renderHoli >-----------------------
Render the data
]]
function renderHoli(isAvail,it,jsoncfg,jsonlocal,date,df,format,tname,cite,usercalc)
local hits = 0
local matchdate = "^" .. date
local startdate,enddate,outoffset,endoutoffset = nil
local starttitle,endtitle = ""
-- user-supplied date calculator
if isAvail == "calculator" then
if jsoncfg.main.event[it].datasource then
startdate = jsoncfg.main.event[it].datasource
enddate = dateOffset(startdate, jsoncfg.main.event[it].days - 1)
else
return inlineError("holiday", 'Invalid calculator result', tname )
end
-- read dates from localfile -- it assumes dates in the json are in chrono order, needs a more flexible method
elseif isAvail == "localfile" then
local numRecords = tableLength(jsonlocal.items) -- Get first and last date of holiday
for i = 1, numRecords do
if mw.ustring.find( jsonlocal.items[i].date, matchdate ) then
if hits == 0 then
startdate = jsonlocal.items[i].date
hits = 1
end
if hits >= tonumber(jsoncfg.main.event[it].days) then
enddate = jsonlocal.items[i].date
break
end
hits = hits + 1
end
end
end
-- Verify data is OK
if startdate == nil or enddate == nil then
if jsoncfg.main.event[it].holiday == "Hanukkah" and startdate and not enddate then -- Hanukkah bug, template doesn't support cross-year boundary
enddate = dateOffset(startdate, 8)
else
return nil
end
end
-- Generate start-date offset (ie. holiday starts the evening before the given date)
if jsoncfg.main.event[it].startoffset then
startdate = dateOffset(startdate, jsoncfg.main.event[it].startoffset)
if startdate ~= enddate then
enddate = dateOffset(enddate, jsoncfg.main.event[it].startoffset)
else
jsoncfg.main.event[it].days = (jsoncfg.main.event[it].days == "1") and "2"
end
end
-- Generate end-date outside-Irael offset (ie. outside Israel the holiday ends +1 day later)
endoutoffset = jsoncfg.main.event[it].endoutoffset and dateOffset(enddate, jsoncfg.main.event[it].endoutoffset)
-- Format dates into df format
local year, month, day = startdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
startdate = makeDate(year, month, day, df, format)
year, month, day = enddate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
enddate = makeDate(year, month, day, df, format)
if startdate == nil or enddate == nil then return nil end
-- Add "outside of Israel" notices
if endoutoffset then
year, month, day = endoutoffset:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
local leader = ((format == "infobox") and "<br>") or " "
endoutoffset = leader .. "(" .. makeDate(year, month, day, df, "infobox") .. " outside of Israel)"
end
if not endoutoffset then
endoutoffset = ""
end
format = ((format == "infobox") and " –<br>") or " – "
-- return output
if startdate == enddate or jsoncfg.main.event[it].days == "1" then -- single date
return jsoncfg.main.event[it].prepend1 .. startdate .. endoutoffset .. cite
else
return jsoncfg.main.event[it].prepend1 .. startdate .. format .. jsoncfg.main.event[it].prepend2 .. enddate .. endoutoffset .. cite
end
end
--[[--------------------------< isHolidayAvail >-----------------------
Given the Configuration.js and name of a holiday, return value of jsoncfg.main.event[it].datatype and index number
Return nil if no holiday found
]]
function isHolidayAvail(jsoncfg, holiday)
local numRecords = tableLength(jsoncfg.main.event)
for it = 1, numRecords do
if jsoncfg.main.event[it].holiday:lower() == holiday:lower() then
if jsoncfg.main.event[it].datatype:lower() == "calculator" then
return "calculator", it
elseif jsoncfg.main.event[it].datatype:lower() == "localfile" or jsoncfg.main.event[it].datatype:lower() == "local file" then
return "localfile", it
else
return nil, nil
end
end
end
return nil, nil
end
--[[--------------------------< calendardate >-----------------------
Main function
]]
function p.calendardate(frame)
local pframe = frame:getParent()
local args = pframe.args
local tname = "Calendar date" -- name of calling template. Change if template rename.
local holiday = nil -- name of holiday
local date = nil -- date of holiday (year)
local df = nil -- date format (mdy, dmy, iso - default: iso)
local format = nil -- template display format options
local cite = nil -- leave a citation at end
track = {} -- global tracking-category table
--- Determine holiday
holiday = trimArg(args.holiday) -- required
if not holiday then
holiday = trimArg(args.event) -- event alias
if not holiday then
return inlineError("holiday", "Missing holiday argument", tname) .. createTracking()
end
end
--- Determine date
date = trimArg(args.year) -- required
if not date then
return inlineError("year", "Missing year argument", tname) .. createTracking()
elseif not verifyDate(date) then
return inlineError("year", "Invalid year", tname) .. createTracking()
end
--- Determine format type
format = trimArg(args.format)
if not format then
format = "none"
elseif format ~= "infobox" then
format = "none"
end
eventsfile = mw.loadData ('Module:Calendar date/Events')
--- Parse JSON files
local cfgfp = mw.title.makeTitle( 'Template', tname .. '/Configuration.json' )
if not cfgfp.exists then
return inlineError("holiday", 'File missing: Template:' .. tname .. '/Configuration.json', tname) .. createTracking()
end
local jsoncfg, jsonlocal = nil
jsoncfg = mw.text.jsonDecode( cfgfp:getContent() )
if not jsoncfg then
return inlineError("holiday", 'Error in file: Template:' .. tname .. '/Configuration.json', tname) .. createTracking()
end
local isAvail, it = isHolidayAvail( jsoncfg, holiday )
if isAvail then
if not jsoncfg.main.event[it].datasource then
return inlineError("holiday", 'Template:' .. tname .. '/Configuration.json misconfigured: missing datasource', tname) .. createTracking()
end
if isAvail == "localfile" then -- dates are contained in a local JSON file
jsoncfg.main.event[it].datasource = jsoncfg.main.event[it].datasource:gsub("^[Tt]emplate%:", '')
local version = mw.title.makeTitle( 'Template', jsoncfg.main.event[it].datasource )
if not version.exists then
return inlineError("holiday", 'File missing: Template:' .. jsoncfg.main.event[it].datasource, tname) .. createTracking()
end
jsonlocal = (version.isRedirect and mw.text.jsonDecode( version.redirectTarget:getContent() )) or mw.text.jsonDecode( version:getContent() )
elseif isAvail == "calculator" then -- dates are calculated with a user-provided calculator
jsoncfg.main.event[it].datasource = frame:preprocess(jsoncfg.main.event[it].datasource:gsub("YYYY", date))
else
return inlineError("holiday", 'Unknown datatype', tname )
end
else
return inlineError("holiday", 'Unknown holiday', tname )
end
--- Determine df - priority to |df in template, otherwise df in datafile, otherwise default to dmy
df = trimArg(args.df)
if not df then
df = (jsoncfg.main.event[it].df and jsoncfg.main.event[it].df) or "dmy"
end
if df ~= "mdy" and df ~= "dmy" and df ~= "iso" then
df = "dmy"
end
-- Determine citation
cite = trimArg2(args.cite)
if cite then
cite = ""
if isAvail == "localfile" then
if jsoncfg.main.event[it].citeurl and jsoncfg.main.event[it].accessdate and jsoncfg.main.event[it].source and jsoncfg.main.event[it].holiday then
cite = frame:preprocess('<ref name="' .. holiday .. ' dates">{{cite web |url=' .. jsoncfg.main.event[it].citeurl .. ' |title=Dates for ' .. jsoncfg.main.event[it].holiday .. ' |publisher=' .. jsoncfg.main.event[it].source .. ' |via=[[Template:' .. tname .. '|' .. tname .. ']] and [[Template:' .. tname .. '/holidays/' .. holiday .. '.js|' .. holiday .. '.js]] |accessdate=' .. jsoncfg.main.event[it].accessdate .. '}}</ref>')
end
elseif isAvail == "calculator" then
cite = (jsoncfg.main.event[it].source and (frame:preprocess('<ref name="' .. holiday .. ' dates">' .. jsoncfg.main.event[it].source .. '</ref>')))
end
else
cite = ""
end
--- Determine pre-pended text eg. "sunset, <date>"
jsoncfg.main.event[it].prepend1 = (jsoncfg.main.event[it].prepend1 and (jsoncfg.main.event[it].prepend1 .. ", ")) or ""
jsoncfg.main.event[it].prepend2 = (jsoncfg.main.event[it].prepend2 and (jsoncfg.main.event[it].prepend2 .. ", ")) or ""
-- Render
local rend = renderHoli( isAvail,it,jsoncfg,jsonlocal,date,df,format,tname,cite)
if not rend then
rend = '<span style="font-size:100%" class="error citation-comment">Error in [[:Template:' .. tname .. ']]: Unknown problem. Please report on template talk page.</span>'
track["Category:Webarchive template errors"] = 1
end
return rend .. createTracking()
end
return p