if math.floor(num_value) ~= num_value then
return false
end
-- For string inputs, check for decimal point to reject values like "7."
if type(value) == "string" then
-- If the string contains a decimal point, it's not an integer
if string.find(value, "%.") then
return false
end
end
return true
end  
--- Returns the name of a month based on its numerical representation.
-- @param month_number (number) The month number (1-12)
-- @return string|nil The name of the month, or nil if invalid
local function get_month_name(month_number)
month_number = tonumber(month_number)
return MONTHS[month_number]
end  
--- Generates an error message wrapped in HTML.
-- @param message (string) The error message to format
-- @param add_tracking_category (boolean, optional) If false, omits the tracking category
-- @return string An HTML-formatted error message with help link and error category
local function generate_error(message, add_tracking_category)
local category = ERROR_CATEGORY  
if add_tracking_category == false then
category = ""
end  
-- Get current page title object
local article_title = mw.title.getCurrentTitle  
-- Special case for testcases pages
local is_test_page = article_title.subpageText == "testcases"
local allow_this_test_page = article_title.fullText == "Module talk:Date time/testcases"  
-- Remove category if the page is not in a tracked namespace or is any other testcases other than this module
if (not CATEGORY_NAMESPACES[article_title.namespace] and not allow_this_test_page)
or (is_test_page and not allow_this_test_page) then
category = ""
end  
return '<strong>Error: ' .. message .. '</strong> ' .. help_link .. category
end  
---
-- Formatting Functions --
---  
--- Formats the time portion of a datetime string.
-- @param hour (string) The hour component
-- @param minute (string) The minute component
-- @param second (string) The second component
-- @return string The formatted time string, or empty string if hour is nil
local function format_time_string(hour, minute, second)
if not hour then
return ""
end  
local time_string = string.format("%s:%s", hour, minute)  
if second and second ~= "00" and minute ~= "00" then
time_string = string.format("%s:%s", time_string, second)
end  
return time_string .. "," .. HTML_SPACE
end  
--- Formats the date portion of a datetime string based on the specified format.
-- @param year (string) The year component
-- @param month (string) The month component
-- @param day (string) The day component
-- @param date_format_dmy (string) The date format ("yes" or "y" for day-month-year, otherwise month-day-year)
-- @return string The formatted date string, or empty string if year is nil
local function format_date_string(year, month, day, date_format_dmy)
if not year then
return ""
end  
local date_string
if month then
local month_name = get_month_name(month)  
if day then
day = tonumber(day)
if date_format_dmy then
date_string = day .. HTML_NBSP .. month_name
else
date_string = month_name .. HTML_NBSP .. day .. ","
end
date_string = date_string .. HTML_NBSP .. year
else
date_string = month_name .. HTML_NBSP .. year
end
else
date_string = year
end  
return date_string
end  
--- Formats the timezone portion of a datetime string.
-- @param timezone (string) The timezone component
-- @return string The formatted timezone string, or empty string if timezone is nil
local function format_timezone(timezone)
if not timezone then
return ""
end  
return HTML_SPACE .. (timezone == "Z" and "(UTC)" or "(" .. timezone .. ")")
end  
--- Generates an hCalendar microformat string for the given date-time values.
-- @param date_time_values (table) A table containing date and time components
-- @param classes (string) The CSS classes to apply to the microformat span
-- @return string The HTML for the hCalendar microformat
local function generate_h_calendar(date_time_values, classes)
local parts = {}  
if date_time_values.year then
table.insert(parts, date_time_values.year)  
if date_time_values.month then
table.insert(parts, "-" .. date_time_values.month)  
if date_time_values.day then
table.insert(parts, "-" .. date_time_values.day)
end
end  
if date_time_values.hour then
table.insert(parts, "T" .. date_time_values.hour)  
if date_time_values.minute then
table.insert(parts, ":" .. date_time_values.minute)  
if date_time_values.second then
table.insert(parts, ":" .. date_time_values.second)
end
end
end
end  
local h_calendar_content = table.concat(parts) .. (date_time_values.timezone or "")
local class_span = string.format('<span>', classes)  
return string.format(
'<span>%s(%s)</span>',
HTML_NBSP,
class_span .. h_calendar_content .. '</span>'
end  
--- Generates a "time ago" string for age calculation templates.
-- @param date_time_values (table) Table containing date components (year, month, day)
-- @param br (boolean) Whether to include a line break before the time ago text
-- @param p (boolean) Whether to format with parentheses around the time ago text
-- @return string Formatted "time ago" text wrapped in a noprint span
local function get_time_ago(date_time_values, br, p)
-- Build timestamp based on available date components
local timestamp
local min_magnitude  
if date_time_values.day then
-- Format with padding for month and day if needed
timestamp = string.format("%d-%02d-%02d",
date_time_values.year,
date_time_values.month,
date_time_values.day)
min_magnitude = "days"
elseif date_time_values.month then
-- Format with padding for month if needed
timestamp = string.format("%d-%02d",
date_time_values.year,
date_time_values.month)  
-- Get the current date
local current_date = os.date("*t")  
-- Compute the difference in months
local year_diff = current_date.year - date_time_values.year
local month_diff = (year_diff * 12) + (current_date.month - date_time_values.month)  
-- If the difference is less than 12 months, use "months", otherwise "years"
if month_diff < 12 then
min_magnitude = "months"
else
min_magnitude = "years"
end
else
timestamp = tostring(date_time_values.year)
min_magnitude = "years"
end  
-- Calculate time ago using Module:Time ago
local m_time_ago = require("Module:Time ago")._main
local time_ago = m_time_ago({timestamp, ["min_magnitude"] = min_magnitude})  
-- Format the result based on br and p parameters
if br then
time_ago = p and ("
(" .. time_ago .. ")") or (";
" .. time_ago)
else
time_ago = p and (HTML_SPACE .. "(" .. time_ago .. ")") or (";" .. HTML_SPACE .. time_ago)
end  
-- Wrap in noprint span
return "<span class=\"noprint\">" .. time_ago .. "</span>"
end  
---
-- Validation Functions --
---  
--- Validates the date and time values provided.
-- @param args (table) Table containing date and time values and optional parameters
-- @return nil|string Nil if validation passes, or an error message if validation fails
local function _validate_date_time(args)
local template_name = args.template or "start date"
help_link = string.format("<small>(help)</small>", template_name)  
-- Store and validate date-time values
local date_time_values = {
year = args[1],
month = args[2],
day = args[3],
hour = args[4],
minute = args[5],
second = args[6]
}  
-- Validate each value
for key, value in pairs(date_time_values) do
if value then
-- Check for integer and leading zeros
if not is_integer(value) then
return generate_error(ERROR_MESSAGES.integers)
end  
if has_leading_zeros(tostring(value), key) then
return generate_error(ERROR_MESSAGES.has_leading_zeros)
end  
-- Convert to number
date_time_values[key] = tonumber(value)
end
end  
-- Validate date components
if not date_time_values.year then
return generate_error(ERROR_MESSAGES.missing_year)
end  
if date_time_values.month and (date_time_values.month < 1 or date_time_values.month > 12) then
return generate_error(ERROR_MESSAGES.invalid_month)
end  
if date_time_values.day then
if not date_time_values.month then
return generate_error(ERROR_MESSAGES.missing_month)
end  
local max_day = get_days_in_month(date_time_values.year, date_time_values.month)