Module:Calendar date: Difference between revisions

Content deleted Content added
No edit summary
m Reverted edit by 213.147.181.177 (talk) to last version by 130.64.64.65
 
(24 intermediate revisions by 9 users not shown)
Line 1:
--[[
 
Display non-Gregorian date of a holiday datesthat usingmoves equivalentyear Gregorianto year. Date data can be obtained from multiple sources as configured in Module:Calendar date/events
 
"localfile" = local data file (eg. https://en.wikipedia.org/wiki/Module:Calendar_date/localfiles/Hanukkah)
"calculator" = user-supplied date calculator
"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 eventdata -- Data structure from ~/localfiles/<holiday name>
local track = {} -- Tracking category container
 
--[[--------------------------< inlineError >-----------------------
Line 15 ⟶ 24:
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 22 ⟶ 31:
--[[--------------------------< 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
end
else
return mw.text.trim(arg)
end
end
local function trimArg2(arg)
if arg == nil then
return nil
end
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, ']]'});
end
return table.concat ({'[[', link, ']]'});
end
else -- no_link
if display and ('' ~= display) then -- if there is display text
return display; -- return that
end
return link or ''; -- return the target article name or empty string
end
end
 
--[[--------------------------< createTracking >-----------------------
 
Return data in track[] ie. tracking categories
 
]]
Line 62 ⟶ 93:
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
 
--[[--------------------------< verifyDateisValidDate >----------------------------------------------------
 
Returns true if date is after 31 December 1899 , not after 2100, and represents a valid date
Given the date arg, return true if within date range 2000-2050 else return false
(29 February 2017 is not a valid date). Applies Gregorian leapyear rules. All arguments are required.
 
]]
 
local function verifyDateisValidDate (dateyear, month, day)
 
local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
if not date or date == "" then
local month_length
return nil
local y, m, d
end
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
if tonumber(date) > 1999 and tonumber(date) < 2051 then
return "true"false
end
else
return nil
return true
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 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)
end
return mw.getContentLanguage():formatDate (formatInfobox[df], date)
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
 
local datesplit = {}
datesplit = mw.text.split(origdate, "-")
datesplit[1], datesplit[2], datesplit[3] = tonumber(datesplit[1]), tonumber(datesplit[2]), tonumber(datesplit[3])
local now = os.time{year = datesplit[1], month = datesplit[2], day = datesplit[3]}
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
 
]]
local function renderHoli(jsoncfg,holidayeventdata,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
local numRecords = tableLength(json.items)
if cfg.datatype == "calculator" then
local hits = 0
if cfg.datasource then
local matchdate = "^" .. date
local startdate,enddate,outoffset,endoutoffset = nilcalcdate
enddate = dateOffset(startdate, cfg.days - 1)
local starttitle,endtitle = ""
else
return inlineError("holiday", 'invalid calculator result', tname )
end
 
-- read dates from localfile -- it assumes dates are in chrono order, need a more flexible method
-- Get first and last date of holiday
elseif cfg.datatype == "localfile" then
for i = 1, numRecords do
local numRecords = tableLength(eventdata) -- Get first and last date of holiday
if mw.ustring.find( json.items[i].date, matchdate ) then
for i = 1, numRecords do
if hits == 0 then
if mw.ustring.find( eventdata[i].date, matchdate ) then
startdate = json.items[i].date
if hits == 0 then
starttitle = json.items[i].title
startdate = eventdata[i].date
hits = 1
hits = 1
end
end
if hits >= tonumber(json.days) then
if hits >= tonumber(cfg.days) then
enddate = json.items[i].date
enddate = eventdata[i].date
endtitle = json.items[i].title
break
end
hits = hits + 1
end
end
end
-- Verify data isand special OKconditions
if startdate == nil or enddate == nil then
if mwcfg.ustring.find(name starttitle,== "ChanukahHanukkah" )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)
-- Work around Hanukkah bug (template doesn't support cross-year boundary)
if cfg.startoffset then
-- if mw.ustring.find( endtitle, "Chanukah" ) then
startdate = dateOffset(startdate, cfg.startoffset)
-- local oset = 0
if startdate ~= enddate then
-- if not mw.ustring.find( endtitle, "Chanukah: 8th Day" ) then
enddate = dateOffset(enddate, cfg.startoffset)
-- if mw.ustring.find( endtitle, "Chanukah: 5 Candles" ) then
else
-- oset = 4
cfg.days = (cfg.days == "1") and "2"
-- elseif mw.ustring.find( endtitle, "Chanukah: 6 Candles" ) then
end
-- oset = 3
end
-- elseif mw.ustring.find( endtitle, "Chanukah: 7 Candles" ) then
-- oset = 2
-- elseif mw.ustring.find( endtitle, "Chanukah: 8 Candles" ) then
-- oset = 1
-- end
-- enddate = dateOffset(enddate, oset)
-- end
-- end
 
-- Generate start-date offset (ie. holiday starts the evening before the given date)
if json.startoffset then
startdate = dateOffset(startdate, json.startoffset)
if startdate ~= enddate then
enddate = dateOffset(enddate, json.startoffset)
else
if json.days == "1" then
json.days = "2"
end
end
end
-- Generate end-date outside-Irael offset (ie. outside Israel the holiday ends +1 day later)
endoutoffset = if jsoncfg.endoutoffset thenand dateOffset(enddate, cfg.endoutoffset)
endoutoffset = dateOffset(enddate, json.endoutoffset)
end
 
-- Format dates into df format
local year, month, day = startdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
local datesplit = {}
startdate = makeDate(year, month, day, df, format)
datesplit = mw.text.split(startdate, "-")
year, month, day = enddate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
startdate = makeDate(datesplit[1], datesplit[2], datesplit[3], df, format)
enddate = makeDate(year, month, day, df, format)
datesplit = mw.text.split(enddate, "-")
if startdate == nil or enddate == nil then return nil end
enddate = makeDate(datesplit[1], datesplit[2], datesplit[3], df, format)
if startdate == nil or enddate == nil then return nil end
 
-- Add "outside of Israel" notices
if endoutoffset then
year, month, datesplitday = mw.text.split(endoutoffset,:match "('(%d%d%d%d)-"(%d%d)-(%d%d)')
local leader = ((format == "infobox") and "<br>") or " "
local leader = " "
endoutoffset = leader .. "(" .. makeDate(year, month, day, df, "infobox") .. " outside of Israel)"
if format == "infobox" then leader = "<br>" end
end
endoutoffset = leader .. "(" .. makeDate(datesplit[1], datesplit[2], datesplit[3], df, "infobox") .. " outside of Israel)"
if not endoutoffset then
end
endoutoffset = ""
if not endoutoffset then
end
endoutoffset = ""
end
 
--- generateDetermine format string
format = if ((format == "infobox") thenand " –<br>") or " – "
 
format = " –<br>"
--- Determine pre-pended text string eg. "sunset, <date>"
else
local prepend1 = (cfg.prepend1 and (cfg.prepend1 .. ", ")) or ""
format = " – "
local prepend2 = (cfg.prepend2 and (cfg.prepend2 .. ", ")) or ""
end
 
-- return output
if startdate == enddate or jsoncfg.days == "1" then -- single date
return json.prepend1 .. startdate .. endoutoffset .. cite
end
else
return json.prepend1 .. startdate .. format .. json.prepend2 .. enddate .. endoutoffset .. cite
end
end
 
Line 295 ⟶ 298:
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'missing holiday argument"', tname) .. createTracking()
end
end
 
--- Determine date
date = trimArg(args.year) -- required
if not date then
return inlineError("year", "Missing'missing year argument"', tname) .. createTracking()
elseif not verifyDateisValidDate(date, "01", "01") then
return inlineError("year", "Invalid'invalid year"', tname) .. createTracking()
end
 
--- Determine format type
format = trimArg(args.format)
if not format or format ~= "infobox" then
format = "none"
end
elseif format ~= "infobox" then
format = "none"
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
 
-- Load configuration file
--- Determine df - priority to |df in template, otherwise df in datafile, otherwise default to dmy
local eventsfile = mw.loadData ('Module:Calendar date/events')
df = trimArg(args.df)
if eventsfile.hebrew_calendar[mw.ustring.upper(holiday)] then
if not df then
cfg = eventsfile.hebrew_calendar[mw.ustring.upper(holiday)]
if json.df then
elseif eventsfile.christian_events[mw.ustring.upper(holiday)] then
df = json.df
cfg = eventsfile.christian_events[mw.ustring.upper(holiday)]
else
elseif eventsfile.carnivals[mw.ustring.upper(holiday)] then
df = "dmy"
cfg = eventsfile.carnivals[mw.ustring.upper(holiday)]
end
elseif eventsfile.chinese_events[mw.ustring.upper(holiday)] then
end
cfg = eventsfile.chinese_events[mw.ustring.upper(holiday)]
if df ~= "mdy" and df ~= "dmy" and df ~= "iso" then
elseif eventsfile.misc_events[mw.ustring.upper(holiday)] then
df = "dmy"
cfg = eventsfile.misc_events[mw.ustring.upper(holiday)]
end
else
return inlineError("holiday", 'unknown holiday ' .. holiday, tname) .. createTracking()
end
 
-- If datatype = localfile
--- Determine pre-pended text eg. "sunset, <date>"
if cfg.datatype == "localfile" then
if not json.prepend1 then
local eventfile = nil
json.prepend1 = ""
eventfile = mw.loadData(cfg.datasource)
else
if eventfile.event then
json.prepend1 = json.prepend1 .. ", "
eventdata = eventfile.event
end
else
if not json.prepend2 then
return inlineError("holiday", 'unknown holiday file ' .. cfg.datasource .. '</span>', tname) .. createTracking()
json.prepend2 = ""
end
else
json.prepend2 = json.prepend2 .. ", "
end
 
-- If datatype = calculator
-- Render
elseif cfg.datatype == "calculator" then
local rend = renderHoli(json,holiday,date,df,format,tname)
calcdate = frame:preprocess(cfg.datasource:gsub("YYYY", date))
if not rend then
local year, month, day = calcdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
rend = '<span style="font-size:100%" class="error citation-comment">Error in [[:Template:' .. tname .. ']]: Unknown problem. Please report on template talk page.</span>'
if not isValidDate(year, month, day) then
track["Category:Webarchive template errors"] = 1
return inlineError("holiday", 'invalid calculated date ' .. calcdate, tname) .. createTracking()
end
end
else
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
df = trimArg(args.df)
if not df then
df = (cfg.df and cfg.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
if (cite ~= "no") then
cite = ""
if cfg.citeurl and cfg.accessdate and cfg.source and cfg.name then
local citetitle = cfg.citetitle
if citetitle == nil then
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>')
end
else
cite = ""
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