มอดูล: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/Module: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 |' .. 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  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 formatdf then
		return nil
	end
	local date = table.concat ({year, month, day})               -- assemble iso format date
	if format ~= "infobox" then
		return mw.getContentLanguage():formatDate (formatFulldf, date)
	else
		return mw.getContentLanguage():formatDate (formatInfoboxdf, 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( eventdatai.date, matchdate ) then
				if hits == 0 then
					startdate = eventdatai.date
					hits = 1
				end
				if hits >= tonumber(cfg.days) then
					enddate = eventdatai.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_calendarmw.ustring.upper(holiday) then
		cfg = eventsfile.hebrew_calendarmw.ustring.upper(holiday)
	elseif eventsfile.christian_eventsmw.ustring.upper(holiday) then
		cfg = eventsfile.christian_eventsmw.ustring.upper(holiday)
	elseif eventsfile.carnivalsmw.ustring.upper(holiday) then
		cfg = eventsfile.carnivalsmw.ustring.upper(holiday)
	elseif eventsfile.chinese_eventsmw.ustring.upper(holiday) then
		cfg = eventsfile.chinese_eventsmw.ustring.upper(holiday)
	elseif eventsfile.misc_eventsmw.ustring.upper(holiday) then
		cfg = eventsfile.misc_eventsmw.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