Jump to content

Module:Calendar widget

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Trappist the monk (talk | contribs) at 21:57, 2 July 2019. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

--[[
Module to create Calendar widget

--]]
require('Module:No globals');
local getArgs = require ('Module:Arguments').getArgs;

local lang_obj = mw.language.getContentLanguage();

local daysinmonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
local dayname = {'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'}
local dayabbr = {}
for i, v in ipairs(dayname) do
	dayabbr[i] = v:sub(1, 2)
end

local iso_dayname = {'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'}
local iso_dayabbr = {}
for i, v in ipairs(iso_dayname) do
	iso_dayabbr[i] = v:sub(1, 2)
end

local monthname = {}
local monthabbr = {}
if 0 == #monthname then
	for m = 1, 12 do
		monthname[m] = lang_obj:formatDate ("F", '2019-' .. m);					-- table of long month names
		monthabbr[m] = lang_obj:formatDate ("M", '2019-' .. m);					-- table of abbreviated month names
	end
end

local function is_leap(year)
	return '1' == lang_obj:formatDate ('L', tostring(year));
end

--[[
returns 1 to 7; 1 == Sunday; 1 == Monday when iso true

TODO: error check inputs
--]]
local function day_of_week (year, month, day, iso)
	return
		iso and lang_obj:formatDate ('N', year .. '-' .. month .. '-' .. day) or	-- 1 = monday
		lang_obj:formatDate ('w', year .. '-' .. month .. '-' .. day) + 1;			-- 1 = sunday
end

-- year and month both numeric:
local function monthstart(year, month, iso)
	return day_of_week(year, month, 1, iso)
end


--[[--------------------------< R E P E A T _ T A G S >--------------------------------------------------------

create <tag> ... </tag>... string to be included into another tag as :wikitext(...)

items is a table of items, each of which will be wrapped in <tag>...</tag>
options is a table of optional class, css, and attribute settings for these tags
	options.attr is a table of attribute / value pairs: {['attribute'] = 'value', ...}
	options.css is a table of attribute / value pairs: {['attribute'] = 'value', ...}

]]

local function repeat_tags (tag, items, options)
	local tags = {};															-- table of <tag>...</tag> tag strings
	local opt_attr = options.attr or {};										-- if options not supplied, use empty table
	local opt_css = options.css or {};
	
	for i, item in ipairs (items) do
		local repeat_tag = mw.html.create (tag);								-- new td object
		repeat_tag
			:addClass (options.class)
			:attr (opt_attr)
			:css (opt_css)
			:wikitext (item)													-- get a calendar for month number mnum
			:done()																-- close <td>
		table.insert (tags, tostring (repeat_tag));								-- make a string of this object
	end
	return table.concat (tags);													-- concatenate them all together
end


--[[--------------------------< G E T _ R O W _ D A T E S >----------------------------------------------------

gets a week (row) of calendar dates each in its own <td>...</td>; inserts iso week number <td> tag ahead of column 1
when props.iso_wk true.

]]

local function get_row_dates (firstday, mnum, row, props)
	local options = {['class']='mcal'};											-- table of otions for these td tags
	local td_items = {};														-- table of <td>...</td> tag strings
	local result = {};
	
	for col = 1, 7 do
		local dom = 7 * (row-1) + col + 1 - firstday							-- calculate day of month for row/col position
		
		if props.iso_wk and 1 == col then										-- when column 1, insert iso week number <td> ahead of first 'dom'
			local iso_wk = lang_obj:formatDate ('W', props.year .. '-' .. mnum .. '-' .. ((1 > dom) and 1 or dom));
			local td_tag = mw.html.create ('td');								-- new td object for iso week number
			td_tag
				:addClass ('mcal_iso')											-- which has it's own class
				:wikitext (iso_wk)
				:done();
			table.insert (result, tostring (td_tag));							-- save it
		end
		
		if dom < 1 or dom > daysinmonth[mnum] then dom = "&nbsp;" end			-- before or after month, blank cell
		table.insert (td_items, dom);
	end

	table.insert (result, repeat_tags ('td', td_items, options));				-- get the get the <td> tags for the dates part of the calendar
	return table.concat (result);
end


--[[--------------------------< W E E K _ D A Y _ H D R >------------------------------------------------------

create header row of day-of-week abbreviations

]]

local function week_day_hdr (props)
	local options = {['class']='mcal'};											-- table of otions for these td tags
	local headers = {};

	if props.iso_wk then
		local iso_week = mw.html.create ('th');									-- new th object for the iso wkeek header
		iso_week
			:addClass (options.class)
			:wikitext ('Wk')
			:done()
		
		table.insert (headers, tostring (iso_week));
	end
	
	table.insert (headers, repeat_tags ('th', props.iso and iso_dayabbr or dayabbr, options))
	return table.concat (headers);
end


--[[--------------------------< D I S P L A Y M O N T H >------------------------------------------------------

generate the html to display a month calendar

]]

local function display_month (props, mnum)
	if props.leap then daysinmonth[2] = 29 end
	local firstday = day_of_week (props.year, mnum, 1, props.iso);					-- get first day number of the first day of the month; 1 == Sunday
	local hdr_year = props.show_year and props.year or '';

	local month_cal = mw.html.create ('table');
	month_cal
		:addClass ('mcal')
		:tag ('tr')																-- for month name header
			:addClass ('mcalhdr')
				:tag ('th')
					:addClass ('mcal')
					:attr ('colspan', props.iso_wk and '8' or '7')
					:wikitext (monthname[mnum] .. ' ' .. hdr_year)
					:done()														-- close <th>
			:done()																-- close <tr>
		:tag ('tr')																-- for weekday header
			:addClass ('mcalhdr')
			:wikitext (week_day_hdr (props))
			:done()																-- close <tr>

	local numrows = math.ceil ((firstday + daysinmonth[mnum] - 1) / 7);			-- calculate number of rows needed for this calendar
	for row = 1, numrows do
		month_cal
			:tag ('tr')															-- for this week
			:addClass ('mcal')
			:wikitext (get_row_dates (firstday, mnum, row, props));				-- get dates for this week
	end
	month_cal:done()															-- close <table>
--mw.log (tostring (month_cal))
	return tostring (month_cal)
end


--[[--------------------------< G E T _ R O W _ C A L E N D A R S >--------------------------------------------

create <td> ... </td>... string to be included into <tr>...</tr> as :wikitext(...)

]]

local function get_row_calendars (cols, row_num, props)
	local mnum;																	-- month number
	local options = {['class']='ycal'};											-- table of otions for these td tags
	local td_items = {};														-- table of <td>...</td> tag strings
	
	for col_num = 1, cols do
		mnum = cols * (row_num - 1) + col_num									-- calculate month number from row and column values
		if mnum < 13 then														-- some sort of error return if ever 13+?
			table.insert (td_items, display_month (props, mnum));				-- get a calendar for month number mnum
		end
	end
	return repeat_tags ('td', td_items, options)
end


--[[--------------------------< D I S P L A Y _ Y E A R >------------------------------------------------------

create a twelve-month calendar; default is 4 columns × 3 rows

]]

local function display_year(props)
	local year = props.year
	local rows = props.rows
	local cols = props.cols or 4
	local mnum;
	
	if rows then
		cols = math.ceil(12 / rows)
	else
		rows = math.ceil(12 / cols)
	end

	local year_cal = mw.html.create('table');
	year_cal
		:addClass('ycal')
		:tag('tr')
			:addClass('ycalhdr')
				:tag('th')
					:addClass('ycal')
					:attr('colspan', cols)
					:wikitext(year)
					:done()														-- close <th>
			:done()																-- close <tr>

	for row_num = 1, rows do
		year_cal
			:tag('tr')
			:addClass ('ycal')
			:wikitext(get_row_calendars (cols, row_num, props))					-- get calendars for this row each wrapped in <td>...</td> tags as wikitext for this <tr>...</tr>
	end
	year_cal:done()																-- close <table>
--mw.log (tostring (year_cal))
	return tostring (year_cal)
end


--[[--------------------------< _ C A L E N D A R >------------------------------------------------------------

]]

local function _calendar (props)
	if props.month then
		props.show_year = true;													-- show year in individual month calendars
		return display_month (props, props.month);
	else
		return display_year (props)
	end
end

--------------------------------------------------
--[[
Sakamoto's method: ISO date
returns day name or nil if bad arguments

iso here refers to the yyyy-mm-dd date format, not to iso day of week where 1 = monday

--]]
local function dayofweek (frame)
	local isodate = mw.text.trim(frame.args[1] or "")
	local y, m, d = isodate:match("(%d+)%p(%d+)%p(%d+)")
	local dow = day_of_week(y, m, d)
	if not dow then return "" end
	return dayname[dow]
end

--[[
isleap returns "leap" if passed a leap year
otherwise returns nothing
]]
local function isleap (frame)
	if is_leap(frame.args[1]) then return "leap" end
	return ""
end

--[[
Main entry point for widget
--]]
local function calendar(frame)
	local args=getArgs (frame);
	local cal_props = {};														-- separate calendar properties table to preserve arguments as originally provided
	local this_year_num = tonumber (lang_obj:formatDate ('Y'));
	local this_month_num = tonumber (lang_obj:formatDate ('n'));

	cal_props.year = args.year and tonumber(args.year) or this_year_num;
	cal_props.leap = is_leap (cal_props.year)

	if args.month then
		local mnum = tonumber(args.month)
		if not mnum then														-- month provided as some sort of text string
			if args.month == "current" then
				cal_props.month = this_month_num
				cal_props.year = this_year_num
			elseif args.month == "last" then
				mnum = this_month_num - 1
				if mnum == 0 then
					cal_props.month = 12										-- december last year
					cal_props.year = this_year_num - 1							-- last year
				else
					cal_props.month = mnum;										-- previous month
				end
			elseif args.month == "next" then
				mnum = this_month_num + 1
				if mnum == 13 then
					cal_props.month = 1											-- january next year
					cal_props.year = this_year_num + 1								-- next year
				else
					cal_props.month = mnum;										-- next month
				end
			else
				local good
				good, cal_props.month = pcall (lang_obj.formatDate, lang_obj, 'n', args.month);
				
				if not good then
					cal_props.month = this_month_num
				else
					cal_props.month = tonumber (cal_props.month)
				end
			end
		else
			cal_props.month =  (13 > mnum and 0 < mnum) and mnum or this_month_num;	-- month provided as a number
		end
	end
	
	cal_props.iso_wk = 'yes' == args['iso-wk'] and args['iso-wk']:lower();			-- show iso format with week numbers when true
	cal_props.iso = 'yes' == (args.iso and args.iso:lower()) or cal_props.iso_wk;	-- iso format without week number unless cal_props.iso_wk true; always true when cal_props.iso_wk true
	
	
-- TODO: add all other args{} from template or invoke to cal_props{} modified as appropriate
	return _calendar (cal_props);
end


--[[--------------------------< E X P O R T E D   F U N C T I O N S >------------------------------------------
]]

return {
	calendar = calendar,
	dayofweek = dayofweek,
	isleap = isleap,
	}