Jump to content

Module:Countdown-ymd/sandbox

From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Trappist the monk (talk | contribs) at 13:16, 16 April 2015. The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.
-- This module powers {{countdown}}.
require('Module:No globals')

local p = {}
local static_text = {
	['event'] = 'Time to event: ',
	['begins'] = 'Event begins in ',											-- when |duration= is set
	['ends'] = 'Event ends in ',												-- when |duration= is set
	['ended'] = 'Event has ended.',												-- when |duration= is set
	['passed'] = 'Event time has passed.',
	}

local getArgs = require('Module:Arguments').getArgs

--[[--------------------------< 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
		return unit .. ' ' .. label;											-- only one unit so return singular label
	else
		return unit .. ' ' .. label .. 's';										-- multiple units so return plural label
	end
end


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

Returns length of event duration in seconds.  If 'duration unit' not defined, assumes seconds.  If 'duration unit'
defined but not one of day, days, hour, hours, minute, minutes, second, seconds then returns zero.

]]

local function duration (duration, unit)
	if not unit then
		return duration;														-- unit not defined, assume seconds
	end
	
	if unit:match ('days?') then
		return duration * 86400;
	elseif unit:match ('hours?') then
		return duration * 3600;
	elseif unit:match ('minutes?') then
		return duration * 60;
	elseif unit:match ('seconds?') then
		return duration;
	else
		return 0;
	end
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.

]]

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


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

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 get_days_in_month (year, month)
	local days_in_month = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	local month_length;

	if (2 == month) then														-- if February
		month_length = 28;														-- then 28 days unless
		if (0 == (year%4) and (0 ~= (year%100) or 0 == (year%400))) then		-- is year a leap year?
			return 29;															-- if leap year then 29 days in February
		end
	end
	return month_length;
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

]]

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 + get_days_in_month (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 >------------------------------------------



]]

local function is_valid_date_time (year, month, day, hour, minute, second)
	if not year:match ('^%d%d%d%d$') then return false; end						-- must be four digits
	if not day:match ('^%d%d?$') then return false; end							-- must be one or two digits

	local month_length = 0 + get_days_in_month (year, month);						-- nil if invalid month
--	if not month_length or month_length < day or 1 > day then return false; end	-- must be valid month and valid day for that month
	if not month_length then return false; end	-- must be valid month and valid day for that month
--	if hour then
--		if not hour:match ('%d%d?') then return false; end						-- must be one or two digits
--		if 23 < hour then return false; end										-- 0 - 23 hours
--	end
--	if minute then
--		if not minute:match ('%d%d?') then return false; end					-- must be one or two digits
--		if 59 < minute then return false; end									-- 0 - 59 minutes
--	end
--	if second then
--		if not second:match ('%d%d?') then return false; end					-- must be one or two digits
--		if 59 < second then return false; end									-- 0 - 59 seconds
--	end
	
	return true;

end
------------------------------< M A I N >----------------------------------------------------------------------

function p.main(frame)
	local args = getArgs(frame)

	if not (args.year and args.month and args.day) then							-- exit with error message if we don't have these minimal parameters
		return  '<span style="font-size:100%" class="error">Error: year, month, and day must be specified</span>'
	end
	
	if ((args.minute or args.second) and not args.hour) or (not args.minute and args.second) then	-- exit with error message if we don't have these minimal parameters
		return  '<span style="font-size:100%" class="error">Error: missing time parameter</span>'
	end

--	if false == is_valid_date_time (args.year, args.month, args.day, args.hour, args.minute, args.second) then
--		return  '<span style="font-size:100%" class="error">Error: invalid date and/or time: ' .. args.year .. ' ' .. args.month .. ' ' .. get_days_in_month (args.year, args.month) or 'nil' ..'</span>'
--	end

	local event = os.time({year=args.year, month=args.month, day=args.day, hour=args.hour or 0, min=args.minute, sec=args.second});	-- convert to seconds
	if args['utc offset'] then
		event = event - utc_offset(args['utc offset']);							-- adjust event time
	end
	
	if event < os.time () then
		if not args.duration then
			return args.event or static_text.passed;							-- if the event start time has passed, we're done
		else
			event = event + duration (args.duration, args['duration unit']);
			if event < os.time () then
				return args.event or static_text.ended;							-- if the event start time + duration has passed, we're done
			end
		end
	else
		args.duration = nil;														-- 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.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.minute and args.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+)', '<span style="color: ' .. (args.color or '#F00') .. '; font-weight: bold;">%1</span>')
	local refreshLink = mw.title.getCurrentTitle():fullUrl{action = 'purge'}
	refreshLink = mw.ustring.format(' <small><span class="plainlinks"> ([%s refresh])</span></small>', refreshLink)

	if 'none' == args.event then
		args.event = '';														-- keyword none sets prefix to empty string
	elseif not args.event then													-- if not defined
		if not args.duration then												-- will be nil if event hasn't started yet or |duration= not specified
			args.event = static_text.begins;									-- use default begin text
		else
			args.event = static_text.ends;										-- use default end text
		end
	else
		args.event = args.event .. ' ';											-- add a space
	end

	return args.event .. result .. refreshLink;
end

return p