--[[
Display Gregorian date of a holiday that moves year to year. Date data can be obtained from multiple sources as configured in ~Module:Calendar date/Configuration.jsevents
"localfile" = local JSON data file (eg. https://en.wikipedia.org/wiki/Template:Calendar_date/holidayslocalfiles/Hanukkah.js)
"calculator" = user-supplied date calculator (eg. )
"wikidata" = <tbd> for holidays with their own date entity page such as https://www.wikidata.org/wiki/Q51224536
]]
require('strict')
local p = {}
local cfg -- Data structure from ~/events
local cfg
local eventdata -- Data structure from ~/localfiles/<holiday name>
local track = {} -- Tracking category container
--[[--------------------------< inlineError >-----------------------
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
--[[-------------------------< make_wikilink >----------------------------------------------------
Makes a wikilink; when both link and display text is provided, returns a wikilink in the form [ [L|D] ]; if only
link is provided, returns a wikilink in the form [ [L] ]; if neither are provided or link is omitted, returns an
empty string.
]]
local function make_wikilink (link, display, no_link)
if nil == no_link then
if link and ('' ~= link) then
if display and ('' ~= display) then
return table.concat ({'[[', link, '|', display, ']]'});
else
return table.concat ({'[[', link, ']]'});
end
end
else -- no_link
if display and ('' ~= display) then -- if there is display text
return display; -- return that
else
return link or ''; -- return the target article name or empty string
end
end
end
--[[--------------------------< createTracking >-----------------------
Return data in track[] ie. tracking categories
]]
local function createTracking()
local sandout = ""{};
if tableLength(track) > 0 then
for key, _ in pairs(track) do -- loop through table
table.insert (out, make_wikilink (key)) -- and convert category names to links
sand = sand .. "[[" .. key .. "]]"
end
end
return table.concat (out) -- concat into one big string; empty string if table is empty
return sand
end
--[[--------------------------< isValidDate >----------------------------------------------------
Returns true if date is after 31 December 1899 , not after 2100, and represents a valid date (29 February 2017 is not a valid date). Applies Gregorian leapyear rules. All arguments are required.
]]
local function isValidDate (year, month, day)
local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
local month_length
local y, m, d
local today = os.date ('*') -- fetch a table of current date parts
if not year or year == '' or not month or month == '' or not day or day == '' then
return false -- something missing
end
y = tonumber (year)
m = tonumber (month)
d = tonumber (day)
if 1900 > y or 2100 < y or 1 > m or 12 < m then -- year and month are within bounds
return false
end
if (2==m) then -- if February
month_length = 28 -- then 28 days unless
if (0==(y%4) and (0~=(y%100) or 0==(y%400))) then -- is a leap year?
month_length = 29 -- if leap year then 29 days in February
end
else
month_length=days_in_month[m];
end
if 1 > d or month_length < d then -- day is within bounds
return false
end
-- here when date parts represent a valid date
return true
-- return os.time({['hour']=0, ['day']=d, ['month']=m, ['year']=y}) <= os.time() -- date at midnight must be less than or equal to current date/time
return true
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
]]
local 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
]]
local function renderHoli(cfg,eventdata,calcdate,date,df,format,tname,cite)
local hits = 0
local matchdate = "^" .. date
local startdate,enddate,outoffset,endoutoffset = nil
local starttitle,endtitle = ""
-- user-supplied date calculator
if cfg.datatype == "calculator" then
if cfg.datasource then
startdate = calcdate
enddate = dateOffset(startdate, cfg.days - 1)
else
return inlineError("holiday", 'Invalidinvalid calculator result', tname )
end
-- read dates from localfile -- it assumes dates in the json are in chrono order, needsneed a more flexible method
elseif cfg.datatype == "localfile" then
local numRecords = tableLength(eventdata) -- Get first and last date of holiday
for i = 1, numRecords do
if mw.ustring.find( eventdata[i].date, matchdate ) then
if hits == 0 then
startdate = eventdata[i].date
hits = 1
end
if hits >= tonumber(cfg.days) then
enddate = eventdata[i].date
break
end
hits = hits + 1
end
end
end
-- Verify data isand special OKconditions
if startdate == nil or enddate == nil then
if cfg.holidayname == "Hanukkah" and startdate and not enddate then -- Hanukkah bug, template doesn't support cross-year boundary
enddate = dateOffset(startdate, 8)
elseif cfg.datatype == "localfile" and cfg.days > "1" and startdate then
else
enddate = dateOffset(startdate, cfg.days - 1)
return nil
elseif startdate and not enddate then
end
return inlineError("year", 'cannot find enddate', tname) .. createTracking()
end
else
return inlineError("holiday", 'cannot find startdate and enddate', tname) .. createTracking()
end
end
-- Generate start-date offset (ie. holiday starts the evening before the given date)
if cfg.startoffset then
startdate = dateOffset(startdate, cfg.startoffset)
if startdate ~= enddate then
enddate = dateOffset(enddate, cfg.startoffset)
else
cfg.days = (cfg.days == "1") and "2"
end
end
-- Generate end-date outside-Irael offset (ie. outside Israel the holiday ends +1 day later)
endoutoffset = cfg.endoutoffset and dateOffset(enddate, cfg.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
--- Determine format string
format = ((format == "infobox") and " –<br>") or " – "
--- Determine pre-pended text string eg. "sunset, <date>"
local prepend1 = (cfg.prepend1 and (cfg.prepend1 .. ", ")) or ""
local prepend2 = (cfg.prepend2 and (cfg.prepend2 .. ", ")) or ""
-- return output
if startdate == enddate or cfg.days == "1" then -- single date
return prepend1 .. startdate .. endoutoffset .. cite
else
return prepend1 .. startdate .. format .. prepend2 .. enddate .. endoutoffset .. cite
end
end
--[[--------------------------< calendardate >-----------------------
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
local calcdate = ""
--- Determine holiday
track = {} -- global tracking-category table
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 holidaydate
holiday date = trimArg(args.holidayyear) -- required
if not holidaydate then
return inlineError("year", 'missing year argument', tname) .. createTracking()
holiday = trimArg(args.event) -- event alias
elseif not isValidDate(date, "01", "01") then
if not holiday then
return inlineError("holidayyear", "Missing'invalid holiday argument"year', tname) .. createTracking()
end
end
--- Determine dateformat type
date format = trimArg(args.yearformat) -- required
if not dateformat then
format = "none"
return inlineError("year", "Missing year argument", tname) .. createTracking()
elseif notformat isValidDate(date, "01",~= "01infobox") then
format = "none"
return inlineError("year", "Invalid year", tname) .. createTracking()
end
end
-- Load configuration file
--- Determine format type
local eventsfile = mw.loadData ('Module:Calendar date/events')
format = trimArg(args.format)
if eventsfile.hebrew_calendar[mw.ustring.upper(holiday)] then
if not format then
cfg = eventsfile.hebrew_calendar[mw.ustring.upper(holiday)]
format = "none"
elseif eventsfile.christian_events[mw.ustring.upper(holiday)] then
elseif format ~= "infobox" then
cfg = eventsfile.christian_events[mw.ustring.upper(holiday)]
format = "none"
elseif eventsfile.carnivals[mw.ustring.upper(holiday)] then
end
cfg = eventsfile.carnivals[mw.ustring.upper(holiday)]
elseif eventsfile.chinese_events[mw.ustring.upper(holiday)] then
cfg = eventsfile.chinese_events[mw.ustring.upper(holiday)]
elseif eventsfile.misc_events[mw.ustring.upper(holiday)] then
cfg = eventsfile.misc_events[mw.ustring.upper(holiday)]
else
return inlineError("holiday", 'unknown holiday ' .. holiday, tname) .. createTracking()
end
-- If datatype = localfile
-- Load configuration file
if cfg.datatype == "localfile" then
eventsfile = mw.loadData ('Module:Calendar date/Events')
local eventfile = nil
if eventsfile.hebrew_calendar[holiday] then
eventfile = mw.loadData(cfg.datasource)
cfg = eventsfile.hebrew_calendar[holiday]
if eventfile.event then
elseif eventsfile.misc_events[holiday] then
eventdata = eventfile.event
cfg = eventsfile.misc_events[holiday]
else
return inlineError("holiday", '{{Calendar date}} – unknown holiday file ' .. holidaycfg.datasource .. '</span>', tname) .. createTracking()
end
-- If datatype = localfile calculator
if elseif cfg.datatype == "localfilecalculator" then
calcdate = frame:preprocess(cfg.datasource:gsub("YYYY", date))
eventfile = mw.loadData ('Module:Calendar date/localfiles/' .. holiday)
local year, month, day = calcdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
if eventfile.event then
if not isValidDate(year, month, day) then
eventdata = eventfile.event
return inlineError("holiday", 'invalid calculated date ' .. calcdate, tname) .. createTracking()
else
end
return inlineError("holiday", '{{Calendar date}} – unknown holiday file Module:Calendar date/localfiles/' .. holiday .. '</span>', tname) .. createTracking()
else
end
return inlineError("holiday", 'unknown "datatype" in configuration', tname) .. createTracking()
end
--- Determine df - priority to |df in template, otherwise df in datafile, otherwise default to dmy
-- If datatype = calculator
df = trimArg(args.df)
elseif cfg.datatype == "calculator" then
if not df then
calcdate = frame:preprocess(cfg.datasource:gsub("YYYY", date))
df = (cfg.df and cfg.df) or "dmy"
local year, month, day = calcdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
end
if not isValidDate(year, month, day) then
if df ~= "mdy" and df ~= "dmy" and df ~= "iso" then
return inlineError("holiday", '{{Calendar date}} – invalid calculated date ' .. calcdate, tname) .. createTracking()
df = "dmy"
end
end
else
return inlineError("holiday", '{{Calendar date}} – unknown "datatype" in configuration', tname) .. createTracking()
end
-- Determine citation
--- Determine df - priority to |df in template, otherwise df in datafile, otherwise default to dmy
df cite = trimArgtrimArg2(args.dfcite)
if not dfcite then
if (cite ~= "no") then
df = (cfg.df and cfg.df) or "dmy"
cite = ""
end
if dfcfg.citeurl ~=and "mdy"cfg.accessdate and df ~= "dmy"cfg.source and df ~= "iso"cfg.name then
local citetitle = cfg.citetitle
df = "dmy"
if citetitle == nil then
end
citetitle = 'Dates for ' .. cfg.name
end
cite = frame:preprocess('<ref name="' .. holiday .. ' dates">{{cite web |url=' .. cfg.citeurl .. ' |title=' .. citetitle .. ' |publisher=' .. cfg.source .. '|accessdate=' .. cfg.accessdate .. '}}</ref>')
elseif cfg.source then
cite = frame:preprocess('<ref name="' .. holiday .. ' dates">' .. cfg.source:gsub("YYYY", date) .. '</ref>')
else
cite = ""
end
else
cite = ""
end
else
cite = ""
end
-- Render
-- Determine citation
local rend = renderHoli( cfg,eventdata,calcdate,date,df,format,tname,cite)
cite = trimArg2(args.cite)
if not if citerend then
rend = '<span style="font-size:100%" class="error citation-comment">Error in [[:Template:' .. tname .. ']]: Unknown problem. Please report on template talk page.</span>'
cite = ""
track["Category:Webarchive template errors"] = 1
if cfg.datatype == "localfile" then
end
if cfg.citeurl and cfg.accessdate and cfg.source and cfg.holiday then
cite = frame:preprocess('<ref name="' .. holiday .. ' dates">{{cite web |url=' .. cfg.citeurl .. ' |title=Dates for ' .. cfg.name .. ' |publisher=' .. cfg.source .. ' |via=[[Template:' .. tname .. '|' .. tname .. ']] and [[Module:' .. tname .. '/localfiles/' .. holiday .. '|' .. holiday .. ']] |accessdate=' .. cfg.accessdate .. '}}</ref>')
end
elseif cfg.datatype == "calculator" then
cite = (cfg.source and (frame:preprocess('<ref name="' .. holiday .. ' dates">' .. cfg.source .. '</ref>')))
end
else
cite = ""
end
-- Render
local rend = renderHoli( cfg,eventdata,calcdate,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
|