Edge Rewrite
// HTMLRewriter · presentation

This page was redesigned at the edge.

Cloudflare fetched the original article and streamed it through HTMLRewriter to apply an entirely new visual system without rebuilding the source page.

// request.cf · coarse context

A page that knows where it met you.

Only coarse request metadata is shown. This demo does not display or persist visitor IP addresses.

Country
US
Cloudflare location
CMH
Connection
HTTP/2
Language
Not provided

Ray ID: a21d24ccccda983d

Jump to content

Module:Countdown-ymd/sandbox

From Wikipedia, the free encyclopedia
-- This module powers {{countdown-ymd}}.
require('strict')
local get_args = require ('Module:Arguments').getArgs;
local cfg = mw.loadData ('Module:Countdown-ymd/Config');


--[[--------------------------< S U B S T I T U T E >----------------------------------------------------------

Substitutes $1, $2, etc in <message> with data from <data_t>. Returns plain-text substituted string when
<data_t> not nil; returns <message> else.

]]

local function substitute (message, data_t)
	return data_t and mw.message.newRawMessage (message, data_t):plain() or message;
end


--[[--------------------------< F O R M A T _ U N I T >--------------------------------------------------------

Concatenates a singular or plural label to a time/date unit: unit label or unit labels.  If unit is 0 or less, returns nil

]]

local function format_unit (unit, label)
	if 1 > unit then															-- less than one so don't display
		return nil;																-- return nil so the result of this call isn't stuffed into results table by table.insert
	elseif 1 == unit then
		label = cfg.labels_single_t[label];										-- only one unit so use singular label
	else
		label = cfg.labels_plural_t[label];										-- multiple units so use plural label
	end

	return substitute ('$1 $2', {unit, label});									-- render the formatted unit
end


--[[--------------------------< U T C _ O F F S E T >----------------------------------------------------------

Returns offset from UTC in seconds.  If 'utc offset' parameter is out of range or malformed, returns 0.

TODO: Return a success/fail flag so we can emit an error message?

]]

local function utc_offset (offset)
	local sign, utc_offset_hr, utc_offset_min;
	
	if offset:match('^[%+%-]%d%d:%d%d$') then									-- formal style: sign, hours colon minutes all required
		sign, utc_offset_hr, utc_offset_min = offset:match('^([%+%-])(%d%d):(%d%d)$');
	elseif offset:match('^[%+%-]?%d?%d:?%d%d$') then							-- informal: sign and colon optional, 1- or 2-digit hours, and minutes 
		sign, utc_offset_hr, utc_offset_min = offset:match('^([%+%-]?)(%d?%d):?(%d%d)$');
	elseif offset:match('^[%+%-]?%d?%d$') then									-- informal: sign optional, 1- or 2-digit hours only
		sign, utc_offset_hr = offset:match('^([%+%-]?)(%d?%d)$');
		utc_offset_min = 0;														-- because not included in parameter, set it to 0 minutes
	else
		return 0;																-- malformed so return 0 seconds
	end
	
	utc_offset_hr = tonumber (utc_offset_hr);
	utc_offset_min = tonumber (utc_offset_min);
	
	if 12 < utc_offset_hr or 59 < utc_offset_min then							-- hour and minute range checks
		return 0;
	end
	
	if '-' == sign then
		sign = -1;																-- negative west offset
	else
		sign = 1;																-- + or sign omitted east offset
	end
	utc_offset_hr = sign * (utc_offset_hr * 3600);								-- utc offset hours * seconds/hour
	utc_offset_min = sign * (utc_offset_min * 60);								-- utc offset minutes * seconds/minute
	return utc_offset_hr + utc_offset_min;										-- return the UTC offset adjustment in seconds
end


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

return true if <year> is a leap year; nil else

]]

local function is_leap_year (year)
	if (0 == (year%4) and (0 ~= (year%100) or 0 == (year%400))) then
		return true;
	end
end


--[[--------------------------< D A Y S _ I N _ Y E A R _ G E T >----------------------------------------------

used to calculate end when duration unit is specified in years

returns number of days in a year measured from <month> in <year> to <month> in <year>+1.  When <month> is 1 or 2
(January or February) and <year> is a leap year, returns 366; when <year> is not a leap year, returns 365.

When <month> is 3 to 12 (March to December) and <year> is a leap year, returns 365 because 29 February has passed;
when <year> is not a leap year and <year>+1 is a leap year, returns 366 because of 29 February <year>+1. returns
365 else.

]]

local function days_in_year_get (year, month)
	if 3 > tonumber (month) then												-- january or february
		if is_leap_year (year, month) then
			return 366;															-- include feb 29
		else
			return 365;
		end
	else																		-- march - december
		if is_leap_year (year, month) then
			return 365;															-- march this year (a leap year) to march next year is 365 days
		elseif is_leap_year (year+1, month) then
			return 366;															-- march this year (normal) to march next year (a leap year) is 366 days because feb 29 next year
		else
			return 365;
		end
	end
end


--[[--------------------------< D A Y S _ I N _ M O N T H _ G E T >--------------------------------------------

Returns the number of days in the month where month is a number 1–12 and year is four-digit Gregorian calendar.
Accounts for leap year.

]]

local function days_in_month_get (year, month)
	local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	
	year = tonumber (year);														-- force these to be numbers just in case
	month = tonumber (month);

	if (2 == month) then														-- if February
		return is_leap_year (year) and 29 or 28;								-- is <year> a leap year?
	end
	return days_in_month [month];
end


--[[--------------------------< D U R A T I O N _ G E T >------------------------------------------------------

Returns duration of event in seconds.  If the units are not defined, assumes seconds.  If the units are
defined but not one of years, months, weeks, days, hours, minutes, seconds then returns zero.

combinations (n months m days) not supported

|duration=<number><space><unit>

supported units are years, months, weeks, days, hours, minutes, seconds

TODO: why are we even considering seconds and minutes?  It would be better to specify the date/time that the
		event ends

]]

local function duration_get (duration, unit, year, month)
	local number = duration:match ('^%d+');										-- duration
	if not unit then
		unit = duration:match ('%d+%s+(%a*)');									-- if there is a unit, get that
	end
	
	if not number then
		return 0;																-- duration not properly specified
	elseif not unit then
		return number;															-- unit not defined, assume seconds
	end
	
	if unit:match (cfg.unit_patterns_t.year) then
		local days = days_in_year_get (year, month);							-- the number of days in the year specified by |year= and |month=
		number = number - 1;
		for i=1, number do														-- loop through the rest of the years
			days = days + days_in_year_get (year + i, month);					-- get and accumulate each of the remaining years' days
		end
		return days * 86400;													-- convert <days> to seconds
	elseif unit:match (cfg.unit_patterns_t.month) then
		local days = days_in_month_get (year, month);							-- the number of days in the month specified by |month=
		number = number - 1;													-- <number> becomes the number of months still to accumulate
		for i=1, number do														-- loop through the rest of the months
			days = days + days_in_month_get (year, month + i);					-- get and accumulate each of the remainin months' days
		end
		return days * 86400;													-- convert <days> to seconds
	elseif unit:match (cfg.unit_patterns_t.week) then
		return number * 7 * 86400;
	elseif unit:match (cfg.unit_patterns_t.day) then
		return number * 86400;
	elseif unit:match (cfg.unit_patterns_t.hour) then
		return number * 3600;
	elseif unit:match (cfg.unit_patterns_t.minute) then
		return number * 60;
	elseif unit:match (cfg.unit_patterns_t.second) then
		return number;
	else
		return 0;																-- unknown unit
	end
end


--[[--------------------------< D I F F _ T I M E >------------------------------------------------------------

calculates the difference between two times; returns a table of the differences in diff.hour, diff.min, and diff.sec

the calculation is <a> - <b> = diff

]]

local function diff_time (a, b)
	local diff = {}
	diff.sec = a.sec - b. sec;
	if diff.sec < 0 then
		diff.sec = diff.sec + 60;												-- borrow from minutes
		a.min = a.min - 1;
		if a.min < 0 then
			a.min = a.min + 60;													-- borrow from hours
			a.hour = a.hour - 1;
		end
	end
	diff.min = a.min - b.min;
	if diff.min < 0 then
		diff.min = diff.min + 60;												-- borrow from hours
		a.hour = a.hour - 1;
	end
	diff.hour = a.hour - b.hour;
	return diff;
end


--[[--------------------------< D I F F _ D A T E >------------------------------------------------------------

calculates the difference between two dates; returns a table of the difference in diff.year, diff.month, and diff.day

]]

local function diff_date (a, b)
	local diff = {}
	diff.day = a.day - b.day;
	if diff.day < 0 then
		a.month = a.month - 1;
		if a.month < 1 then	
			a.year = a.year - 1;												-- borrow a month from years
			a.month = a.month + 12;
		end
		diff.day = diff.day + days_in_month_get (a.year, a.month);				-- borrow all of the days from the *previous* month
	end

	diff.month = a.month - b.month;
	if diff.month < 0 then
		a.year = a.year - 1;													-- borrow a month from years
		diff.month = diff.month + 12;
	end
	diff.year = a.year - b.year;
	return diff;
end


--[[--------------------------< I S _ V A L I D _ D A T E _ T I M E >------------------------------------------

Validate date/time.  Also, determine if we have all of the necessary date/time componants.  Minimal required
date/time is |year=.

For dates, these are required (all other variations emit an error message):
	|year= or
	|year= |month= or
	|year= |month= |day=
	
If time is included, these are required (all other variations emit an error message):
	|hour= or
	|hour= |minute= or
	|hour= |minute= |second=

]]

local function is_valid_date_time (year, month, day, hour, minute, second)
	if not year or (not month and day) then										-- must have YMD, YM, or Y
		return false;
	end

	year = tonumber (year);
	if not year or 1582 > year or 9999 < year then return false; end			-- must be four digits in gregorian calander
	
	if month then
		month = tonumber (month);
		if not month or 1 > month or 12 < month then return false; end			-- 1 to 12
	end
	if day then
		day = tonumber (day);
		if not day or 1 > day or days_in_month_get (year, month) < day then		-- 1 to 28, 29, 30, or 31 depending on month
			return false;
		end
	end

	if ((minute or second) and not hour) or (not minute and second) then		-- must have H:M:S or H:M or H or none at all
			return false;
	end
	
	if hour then
		hour = tonumber (hour);
		if not hour or 0 > hour or 23 < hour then return false; end				-- 0 to 23
	end
	if minute then
		minute = tonumber (minute);
		if not minute or 0 > minute or 59 < minute then return false; end		-- 0 to 59
	end
	if second then
		second = tonumber (second);
		if not second or 0 > second or 59 < second then return false; end		-- 0 to 59
	end
	
	return true;

end


--[[--------------------------< M A I N >----------------------------------------------------------------------

Supported parameters:
	date and time parameters:
		|year= (required), |month=, |day=
		|hour=, |minute=, |second=
		|utc offset=
		|duration=

	presentation parameters:
		|color=
		
	wrapping-text parameters:
		|event lead= – text ahead of countdown text while event is in progress; countdown text is time to end of event; default is static_text.ends
		|event tail= – text that follows countdown text while event is in progress; default is empty string
		|expired= - display text when event is in the past; default is static_text.passed; when |duration= is set then default is static_text.ended
		|lead= – text ahead of countdown text; default is static_text.begins; overridden by |event lead= while event is in progress;
		|tail= – text that follows countdown text; default is empty string; overridden by |event tail= while event is in progress

]]

local function main(frame)
	local args_t = get_args (frame);

	for param, _ in pairs (args_t) do
		if not cfg.known_params_t[param] then									-- unique {{lang}} parameters
			return substitute (cfg.static_text_t.unknown, {param});				-- <param> not found so abandon
		end
	end

	if false == is_valid_date_time (args_t.year, args_t.month, args_t.day, args_t.hour, args_t.minute, args_t.second) then	-- validate our inputs; minimal requirement is |year=
		return  cfg.static_text_t.invalid;
	end
																				-- convert event time parameters to seconds; use default January 1 @ 0h for defaults if not provided
	local event = os.time({year=args_t.year, month=args_t.month or 1, day=args_t.day or 1, hour=args_t.hour or 0, min=args_t.minute, sec=args_t.second});	-- convert to seconds
	if args_t['utc offset'] then
		event = event - utc_offset(args_t['utc offset']);						-- adjust event time to UTC from local time
	end
	
	if cfg.keywords_t.none == args_t.expired then								-- TODO: why is this here?  what purpose does it serve?
		args_t.expired = '';
	end
	
	if event < os.time () then													-- if event time is in the past
		if not args_t.duration then
			return args_t.expired or cfg.static_text_t.passed;					-- if the event start time has passed, we're done
		else
			event = event + duration_get (args_t.duration, args_t['duration unit'], args_t.year, args_t.month);	-- calculate event ending time
			if event < os.time () then
				return args_t.expired or args_t.eventend or cfg.static_text_t.ended;	-- if the event start time + duration has passed, we're done
			end
		end
	else																		-- here when event has not yet started or occured
		if not args_t.lead then
			if args_t.duration then
				args_t.lead = cfg.static_text_t.begins;							-- default lead text when |duration= set but |lead= not set
			else
				args_t.lead = cfg.static_text_t.eventtime;						-- default lead text when |duration= and |lead= not set
			end
		end
		args_t.duration = nil;													-- event not yet started; unset so that we render text around the countdown correctly
	end
	
	local today = os.date ('*t');												--fetch table of current date time parameters from the server
	local event_time = os.date ('*t', event);									--fetch table of event date time parameters from the server
	local hms_til_start = diff_time (event_time, today)							-- table of time difference (future time - current time)
	if hms_til_start.hour < 0 then												-- will be negative if we need to borrow hours from day
		hms_til_start.hour = hms_til_start.hour + 24;							-- borrow a day's worth of hours from event start date
		event_time.day = event_time.day - 1;
	end

	local ymd_til_start = diff_date (event_time, today)							-- table of date difference (future date - current date)

	local result = {}															-- results table with some formatting; values less than one are not added to the table 
	table.insert (result, format_unit (ymd_til_start.year, 'year'));			-- add date parameters
	table.insert (result, format_unit (ymd_til_start.month, 'month'));
	table.insert (result, format_unit (ymd_til_start.day, 'day'));
	
	local count = #result;														-- zero if less than 24 hours to event; when less than 24 hours display all non-zero time units
	table.insert (result, format_unit (hms_til_start.hour, 'hour'));			-- always include hours if it is not zero
	if args_t.hour or 0 == count then											-- if event start hour provided in template, show non-zero minutes
		table.insert (result, format_unit (hms_til_start.min, 'minute'));
	end
	if (args_t.minute and args_t.hour) or 0 == count then						-- if event start hour and minute provided in template, show non-zero seconds
		table.insert (result, format_unit (hms_til_start.sec, 'second'));
	end
	
	result = table.concat (result, ', ');
	result = mw.ustring.gsub(result, '(%d+)', substitute (cfg.presentation_t.digit_style, {args_t.color or 'blue'}));

	local refresh_link
	if 'no' == args_t.refresh then
		refresh_link = ''
	else
		refresh_link = mw.title.getCurrentTitle():fullUrl ({action = 'purge'});
		refresh_link = substitute (cfg.presentation_t.refresh_style, {refresh_link});
	end
	
	if not args_t.duration then													-- will be nil if event hasn't started yet or |duration= not specified
		args_t.lead = args_t.lead or cfg.static_text_t.eventtime;						-- use default begins text
	else
		if not args_t['event lead'] and  args_t.eventstart then					-- |eventstart= from orig {{countdown}} replaces countdown text
			args_t.lead = args_t.eventstart;
			result = '';
		else
			args_t.lead = args_t['event lead'] or cfg.static_text_t.ends;		-- event has started use |event lead= text or default <ends> text
			args_t.tail = args_t['event tail'];									-- and use |event tail= text
		end
--		args_t.lead = args_t['event lead'] or args_t.eventstart or cfg.static_text_t.ends;	-- event has started use |event lead= text or default <ends> text
--		args_t.tail = args_t['event tail'];										-- and use |event tail= text
	end

	if cfg.keywords_t.none == args_t.lead then									-- here, if either args_t.lead and args_t['event lead'] were set to keyword'none'
		args_t.lead = '';														-- set lead text to empty string
	elseif args_t.lead then
		args_t.lead = args_t.lead .. ' ';										-- add a space
	end

	if args_t.tail then
		args_t.tail = ' ' .. args_t.tail;										-- add a space
	else
		args_t.tail = '';														-- empty string for concatenation
	end
	
	return args_t.lead .. result .. args_t.tail .. '.' .. refresh_link;			-- TODO: create a |ps= parameter? default to '.'; accept |ps=none ? if not lead and not tail then no default dot?
end


--[[--------------------------< E X P O R T S >----------------------------------------------------------------
]]

return {
	main = main,
	}