Jump to content

Module:Date time

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Gonnym (talk | contribs) at 10:36, 27 February 2025 (fix bug when year and day are used without a month). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

require("strict")

local p = {}

-- Declare `helpLink` and `errorCategory` as local variables outside of the functions
local helpLink
local errorCategory

-- This function checks if a given year is a leap year.
-- A year is a leap year if:
-- 1. It is divisible by 4, and
-- 2. It is not divisible by 100, unless
-- 3. It is also divisible by 400.
--
-- These rules ensure that a leap year occurs every 4 years, except for years divisible by 100, 
-- unless they are also divisible by 400 (e.g., the year 2000 was a leap year, but 1900 was not).
--
-- Parameters:
--	year (number): The year to check for leap year status.
local function isLeapYear(year)
    return (year % 4 == 0 and year % 100 ~= 0) or (year % 400 == 0)
end

-- This function returns the number of days in a given month of a specified year.
-- It handles leap years for the month of February.
--
-- The function uses the following logic:
-- 1. An array `daysInMonth` is defined to hold the typical number of days for each month (1 to 12).
-- 2. If the month is February (month == 2), the function checks if the year is a leap year.
--    If it's a leap year, February will have 29 days instead of 28.
-- 3. For other months, the function simply returns the days as defined in the `daysInMonth` array.
-- 4. If an invalid month is passed (i.e., not between 1 and 12), the function returns 0.
--
-- Parameters:
--	year (number): The year to check for leap year conditions.
--	month (number): The month (1-12) for which to return the number of days.
local function getDaysInMonth(year, month)
	-- Days in each month: index corresponds to the month number (1 = January, 12 = December).
	local daysInMonth = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }

	-- Check if February (month 2) and if it's a leap year to return 29 days
	if month == 2 and isLeapYear(year) then
		return 29
	end

	-- Return the number of days in the specified month, or 0 if the month is invalid
	return daysInMonth[month] or 0
end

-- This function checks if a given value has unnecessary leading zeros.
-- It rejects values like "001", "005", etc., but allows "01", "05", "09", etc.
-- The function works by converting the value to a string and checking if it starts
-- with one or more zeros followed by one or more digits. Only values with more
-- than one leading zero (e.g., "001") are considered invalid.
--
-- Parameters:
--	value (string or number): The value to check for leading zeros.
local function hasLeadingZeros(value)
	return tostring(value):match("^0%d%d") ~= nil
end

-- This function checks if a given value is an integer.
-- It returns true if the value is either nil (optional value) or a valid integer.
-- A valid integer is determined by converting the value to a number and checking if
-- the number is equal to its floor value (i.e., it has no decimal part).
-- 
-- Parameters:
--	value (string or number): The value to check if it's an integer.
local function isInteger(value)
	if not value then
		return false
	end

	-- Check if the value is nil or a valid number.
	local numValue = tonumber(value)
	return numValue and numValue == math.floor(numValue)
end

-- This helper function generates an error message wrapped in HTML.
-- It formats the error message in red text and appends both a help link 
-- and an error category tag to the message. The help link and error category 
-- are dynamic and based on the provided `templateName`.
-- 
-- Parameters:
--	message (string): The error message to format and return.
local function generateError(message)
    return '<strong class="error">Error: ' .. message .. '</strong> ' .. helpLink .. errorCategory
end

-- This function validates the date and time values provided in the `frame` argument.
-- It checks if the provided `year`, `month`, `day`, `hour`, `minute`, and `second` values 
-- are integers, within valid ranges, and if they follow proper formatting.
-- It also ensures that the `df` parameter (if provided) is either "yes" or "y".
-- Additionally, it generates error messages for invalid inputs, including a help link 
-- and an error category based on the `templateName` parameter (e.g., "start date" or "end date").
-- If all values are valid, it returns an empty string, indicating no errors.
function p.main(frame)
	local getArgs = require("Module:Arguments").getArgs
	local args = getArgs(frame)

	-- Default the templateName to "start date" if not provided.
	local templateName = args.template or "start date"
	helpLink = string.format("<small>[[:Template:%s|(help)]]</small>", templateName)
	errorCategory = string.format("[[Category:Pages using %s with invalid values]]", templateName)

	-- Store all values in a table for later processing.
	local dateTimeValues = {
		year = args[1], month = args[2], day = args[3],
		hour =  args[4], minute = args[5], second = args[6]
	}

	-- Check if all values are integers (if they exist) and convert them to numbers only if necessary.
	for key, value in pairs(dateTimeValues) do
		-- Check if value is an integer, skip conversion if not valid.
		if value and not isInteger(value) then
			return generateError("All values must be integers")
		end
		
		-- If it's an integer, convert it to a number.
		if value then
			dateTimeValues[key] = tonumber(value)
		end
	end
	
	-- A year value is always required.
	if not dateTimeValues.year then
		return generateError("Year value is required")
	end

    -- Check for leading zeros (only flag if inappropriate).
    for _, value in pairs(dateTimeValues) do
        if hasLeadingZeros(value) then
            return generateError("Values cannot have unnecessary leading zeros")
        end
    end

    -- Validate month (if provided).
    if dateTimeValues.month and (dateTimeValues.month < 1 or dateTimeValues.month > 12) then
        return generateError("Value is not a valid month")
    end

	-- Validate day (if provided).
	if dateTimeValues.day then
		-- Ensure month is also provided before checking the day.
		if not dateTimeValues.month then
			return generateError("Month value is required when a day is provided")
		end

		local maxDay = getDaysInMonth(dateTimeValues.year, dateTimeValues.month)
		if dateTimeValues.day < 1 or dateTimeValues.day > maxDay then
			return generateError(string.format("Value is not a valid day (Month %d has %d days)", dateTimeValues.month, maxDay))
		end
	end

    -- Validate hour (if provided).
    if dateTimeValues.hour and (dateTimeValues.hour < 0 or dateTimeValues.hour > 23) then
        return generateError("Value is not a valid hour")
    end

    -- Validate minute (if provided).
    if dateTimeValues.minute and (dateTimeValues.minute < 0 or dateTimeValues.minute > 59) then
        return generateError("Value is not a valid minute")
    end

    -- Validate second (if provided).
    if dateTimeValues.second and (dateTimeValues.second < 0 or dateTimeValues.second > 59) then
        return generateError("Value is not a valid second")
    end

	-- Validate df parameter (if provided).
	if args.df and not (args.df == "yes" or args.df == "y") then
		return generateError('df must be either "yes" or "y"')
	end

    -- If everything is valid, return an empty string.
    return nil
end

return p