Mô đun:Calendar date

--

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/events

"localfile" = local data file (eg. https://en.wikipedia.org/wiki/Template:Calendar_date/localfiles/Hanukkah)
"calculator" = user-supplied date calculator
"wikidata" = 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/
local track = {} -- Tracking category container

---------------------------------------------------

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 'Error in - Check &124;' .. arg .. '= ' .. msg ..

end

---------------------------------------------------

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

---------------------------------------------------

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

--[[-----------------------------------------------------------------------------

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 ({, 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

--[[-------------------------------------------------

Return data in track[] ie. tracking categories

]]

local function createTracking()

local out = {};
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
end
end
return table.concat (out) -- concat into one big string; empty string if table is empty
end

--------------------------------------------------------------------------------

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 m or 12 d or month_length -----------------------

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

---------------------------------------------------

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

---------------------------------------------------

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", 'invalid calculator result', tname )
end

-- read dates from localfile -- it assumes dates are in chrono order, need 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 and special conditions
if startdate nil or enddate nil then
if cfg.name == "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
enddate = dateOffset(startdate, cfg.days - 1)
elseif startdate and not enddate then
return inlineError("year", 'cannot find enddate', tname) .. createTracking()
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 "") 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 " –") or " – "

--- Determine pre-pended text string eg. "sunset, "
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

---------------------------------------------------

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
local calcdate = ""

--- 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 isValidDate(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

-- Load configuration file
local eventsfile = mw.loadData ('Module:Calendar date/events')
if eventsfile.hebrew_calendar[mw.ustring.upper(holiday)] then
cfg = eventsfile.hebrew_calendar[mw.ustring.upper(holiday)]
elseif eventsfile.christian_events[mw.ustring.upper(holiday)] then
cfg = eventsfile.christian_events[mw.ustring.upper(holiday)]
elseif eventsfile.carnivals[mw.ustring.upper(holiday)] then
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
if cfg.datatype == "localfile" then
local eventfile = nil
eventfile = mw.loadData(cfg.datasource)
if eventfile.event then
eventdata = eventfile.event
else
return inlineError("holiday", 'unknown holiday file ' .. cfg.datasource .. , tname) .. createTracking()
end

-- If datatype = calculator
elseif cfg.datatype == "calculator" then
calcdate = frame:preprocess(cfg.datasource:gsub("YYYY", date))
local year, month, day = calcdate:match ('(%d%d%d%d)-(%d%d)-(%d%d)')
if not isValidDate(year, month, day) then
return inlineError("holiday", 'invalid calculated date ' .. calcdate, tname) .. createTracking()
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()
elseif cfg.source then
cite = frame:preprocess()
else
cite = ""
end
else
cite = ""
end
else
cite = ""
end

-- Render
local rend = renderHoli( cfg,eventdata,calcdate,date,df,format,tname,cite)
if not rend then
rend = 'Error in :Template:' .. tname .. ': Unknown problem. Please report on template talk page.'
track["Category:Webarchive template errors"] = 1
end

return rend .. createTracking()

end

return p