Module:Calendar date: Difference between revisions

Content deleted Content added
from sandbox
No edit summary
Line 1:
--[[
 
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 >-----------------------
Line 21 ⟶ 15:
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;">&#124;' .. arg .. '=</code> ' .. msg .. '</span>'
 
end
Line 28 ⟶ 22:
--[[--------------------------< 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
 
]]
Line 94 ⟶ 62:
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
 
]]
Line 188 ⟶ 166:
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
 
Line 301 ⟶ 278:
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