--[[
Display non-Gregorian holiday dates using equivalent Gregorian date
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 = {}
local cfg
local eventdata
--[[--------------------------< 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 outsand = {};""
if tableLength(track) > 0 then
for key, _ in pairs(track) do -- loop through table
sand = sand .. "[[" .. key .. "]]"
table.insert (out, make_wikilink (key)) -- and convert category names to links
end
end
return sand
return table.concat (out) -- concat into one big string; empty string if table is empty
end
--[[--------------------------< isValidDateverifyDate >----------------------------------------------------
Given the date arg, return true if within date range 2000-2100 else return false
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 verifyDate(year, month, daydate)
if not date or date == "" then
local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
return nil
local month_length
end
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 tonumber(date) > 1999 and tonumber(date) < 2100 then
if 1 > d or month_length < d then -- day is within bounds
return false"true"
else
end
return nil
end
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 zmonth = month -- month with leading 0
month = month:match("0*(%d+)") -- month without leading 0
if tonumber(month) < 1 or tonumber(month) > 12 then
return year
end
local nmonth = os.date("%B", os.time{year=2000, month=month, day=1} ) -- month in name form
if not nmonth then
return year
end
local zday = day
day = zday:match("0*(%d+)")
if tonumber(day) < 1 or tonumber(day) > 31 then
if df == "mdy" or df == "dmy" then
return nmonth .. " " .. year
elseif df == "iso" then
return year .. "-" .. zmonth
elseif df == "ymd" then
return year .. " " .. nmonth
else
return nmonth .. " " .. year
end
end
if format ~= "infobox" then
if df == "mdy" then
return nmonth .. " " .. day .. ", " .. year -- September 1, 2016
elseif df == "dmy" then
return day .. " " .. nmonth .. " " .. year -- 1 September 2016
elseif df == "iso" then
return year .. "-" .. zmonth .. "-" .. zday -- 2016-09-01
elseif df == "ymd" then
return year .. " " .. nmonth .. " " .. day -- 2016 September 1
else
return nmonth .. " " .. day .. ", " .. year -- September 1, 2016
end
else
if df == "mdy" then
return nmonth .. " " .. day -- September 1
elseif df == "dmy" then
return day .. " " .. nmonth -- 1 September
elseif df == "iso" then
return year .. "-" .. zmonth .. "-" .. zday -- 2018-09-01
elseif df == "ymd" then
return nmonth .. " " .. day -- September 1
else
return nmonth .. " " .. day -- September 1
end
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 datesplit = {}
local year, month, day = origdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
datesplit = mw.text.split(origdate, "-")
local now = os.time{year = year, month = month, day = day}
datesplit[1], datesplit[2], datesplit[3] = tonumber(datesplit[1]), tonumber(datesplit[2]), tonumber(datesplit[3])
local newdate = os.date("%Y-%m-%d", now + (tonumber(offset) * 24 * 3600))
local now = os.time{year = datesplit[1], month = datesplit[2], day = datesplit[3]}
return newdate and newdate or origdate
local newdate = os.date("%Y-%m-%d", now + (tonumber(offset) * 24 * 3600))
if not newdate then
return origdate
else
return newdate
end
end
--[[--------------------------< renderHoli >-----------------------
Render the data
]]
function renderHoli(cfgjson,eventdata,calcdateholiday,date,df,format,tname,cite)
local hitsnumRecords = 0tableLength(json.items)
local hits = 0 local matchdate = "^" .. date
local startdate,enddate,outoffset,endoutoffset = nil
local starttitle,endtitle = ""
-- user-suppliedGet first and last date calculatorof holiday
for i = 1, numRecords do
if cfg.datatype == "calculator" then
if mw.ustring.find( json.items[i].date, matchdate ) then
if cfg.datasource then
if hits == 0 then
startdate = calcdate
startdate = json.items[i].date
enddate = dateOffset(startdate, cfg.days - 1)
starttitle = json.items[i].title
else
hits = 1
return inlineError("holiday", 'Invalid calculator result', tname )
end
if hits >= tonumber(json.days) then
enddate = json.items[i].date
-- read dates from localfile -- it assumes dates are in chrono order, need a more flexible method
endtitle = json.items[i].title
elseif cfg.datatype == "localfile" then
break
local numRecords = tableLength(eventdata) -- Get first and last date of holiday
end
for i = 1, numRecords do
hits = hits + 1
if mw.ustring.find( eventdata[i].date, matchdate ) then
end
if hits == 0 then
end
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 and specialis conditionsOK
if startdate == nil or enddate == nil then
if cfg.name == "Hanukkah" andif startdatemw.ustring.find( andstarttitle, not"Chanukah" enddate) then -- Hanukkah bug, template doesn't support cross-year boundary
enddate = dateOffset(startdate, 8)
else
elseif cfg.datatype == "localfile" and cfg.days > "1" and startdate then
return nil
enddate = dateOffset(startdate, cfg.days - 1)
end
elseif startdate and not enddate then
end
return "Cannot find enddate"
else
return "Cannot find startdate and enddate"
end
end
-- Generate start-date offset (ie. holiday starts the evening before the given date)
if cfgjson.startoffset then
startdate = dateOffset(startdate, cfgjson.startoffset)
if startdate ~= enddate then
enddate = dateOffset(enddate, cfgjson.startoffset)
else
cfg.days = (cfg if json.days == "1") and "2"then
json.days = "2"
end
end
end
end
-- Generate end-date outside-Irael offset (ie. outside Israel the holiday ends +1 day later)
endoutoffset = cfgif json.endoutoffset and dateOffset(enddate, cfg.endoutoffset)then
endoutoffset = dateOffset(enddate, json.endoutoffset)
end
-- Format dates into df format
local datesplit = {}
local year, month, day = startdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
datesplit = mw.text.split(startdate, "-")
startdate = makeDate(year, month, day, df, format)
startdate = makeDate(datesplit[1], datesplit[2], datesplit[3], df, format)
year, month, day = enddate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
datesplit = mw.text.split(enddate, "-")
enddate = makeDate(year, month, day, df, format)
enddate = makeDate(datesplit[1], datesplit[2], datesplit[3], df, format)
if startdate == nil or enddate == nil then return nil end
if startdate == nil or enddate == nil then return nil end
-- Add "outside of Israel" notices
if endoutoffset then
year, month, day datesplit = mw.text.split(endoutoffset:match, ('(%d%d%d%d)"-(%d%d)-(%d%d)'")
local leader = " "
local leader = ((format == "infobox") and "<br>") or " "
if format == "infobox" then leader = "<br>" end
endoutoffset = leader .. "(" .. makeDate(year, month, day, df, "infobox") .. " outside of Israel)"
endoutoffset = leader .. "(" .. makeDate(datesplit[1], datesplit[2], datesplit[3], df, "infobox") .. " outside of Israel)"
end
end
if not endoutoffset then
if not endoutoffset then
endoutoffset = ""
endoutoffset = ""
end
end
- -- Determinegenerate format string
format = ((if format == "infobox") and " –<br>") or " – "then
format = " –<br>"
else
--- Determine pre-pended text string eg. "sunset, <date>"
format = " – "
local prepend1 = (cfg.prepend1 and (cfg.prepend1 .. ", ")) or ""
end
local prepend2 = (cfg.prepend2 and (cfg.prepend2 .. ", ")) or ""
-- return output
if startdate == enddate or cfgjson.days == "1" then -- single date
return json.prepend1 .. startdate .. endoutoffset .. cite
else
return json.prepend1 .. startdate .. format .. json.prepend2 .. enddate .. endoutoffset .. cite
end
end
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 = ""
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 isValidDateverifyDate(date, "01", "01") 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
end
--- Parse JSON file
local version = mw.title.makeTitle( 'Template', tname .. '/holidays/' .. holiday .. '.js' )
if not version.exists then
return inlineError("holiday", "File missing Template:" .. tname .. "/holidays/" .. holiday .. ".js", tname) .. createTracking()
end
local json = nil
if version.isRedirect then
json = mw.text.jsonDecode( version.redirectTarget:getContent() )
else
json = mw.text.jsonDecode( version:getContent() )
end
--- Determine df - priority to |df in template, otherwise df in datafile, otherwise default to dmy
-- Load configuration file
df = trimArg(args.df)
eventsfile = mw.loadData ('Module:Calendar date/Events')
if not df then
if eventsfile.hebrew_calendar[mw.ustring.upper(holiday)] then
if json.df then
cfg = eventsfile.hebrew_calendar[mw.ustring.upper(holiday)]
df = json.df
elseif eventsfile.misc_events[mw.ustring.upper(holiday)] then
else
cfg = eventsfile.misc_events[mw.ustring.upper(holiday)]
df = "dmy"
else
end
return inlineError("holiday", '{{Calendar date}} – unknown holiday ' .. holiday, tname) .. createTracking()
end
if df ~= "mdy" and df ~= "dmy" and df ~= "iso" then
df = "dmy"
end
-- Determine citation
-- If datatype = localfile
cite = trimArg2(args.cite)
if cfg.datatype == "localfile" then
if cite then
eventfile = mw.loadData ('Module:Calendar date/localfiles/' .. holiday)
if json.citeurl and json.accessdate and json.source and json.holiday then
if eventfile.event then
cite = frame:preprocess('<ref name="hebcal auto">{{cite web |url=' .. json.citeurl .. ' |title=Dates for ' .. json.holiday .. ' |publisher=' .. json.source .. ' |via=[[Template:' .. tname .. '|' .. tname .. ']] and [[Template:' .. tname .. '/holidays/' .. holiday .. '.js|' .. holiday .. '.js]] |accessdate=' .. json.accessdate .. '}}</ref>')
eventdata = eventfile.event
else
cite = ""
return inlineError("holiday", '{{Calendar date}} – unknown holiday file Module:Calendar date/localfiles/' .. holiday .. '</span>', tname) .. createTracking()
end
else
cite = ""
end
--- Determine pre-pended text eg. "sunset, <date>"
-- If datatype = calculator
if not json.prepend1 then
elseif cfg.datatype == "calculator" then
json.prepend1 = ""
calcdate = frame:preprocess(cfg.datasource:gsub("YYYY", date))
else
local year, month, day = calcdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
json.prepend1 = json.prepend1 .. ", "
if not isValidDate(year, month, day) then
end
return inlineError("holiday", '{{Calendar date}} – invalid calculated date ' .. calcdate, tname) .. createTracking()
if not json.prepend2 then
end
json.prepend2 = ""
else
else
return inlineError("holiday", '{{Calendar date}} – unknown "datatype" in configuration', tname) .. createTracking()
json.prepend2 = json.prepend2 .. ", "
end
end
-- Render
--- Determine df - priority to |df in template, otherwise df in datafile, otherwise default to dmy
local rend = renderHoli(json,holiday,date,df,format,tname,cite)
df = trimArg(args.df)
if not dfrend then
rend = '<span style="font-size:100%" class="error citation-comment">Error in [[:Template:' .. tname .. ']]: Unknown problem. Please report on template talk page.</span>'
df = (cfg.df and cfg.df) or "dmy"
track["Category:Webarchive template errors"] = 1
end
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 cfg.datatype == "localfile" then
if cfg.citeurl and cfg.accessdate and cfg.source and cfg.name 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
|