Jump to content
Wikipedia The Free Encyclopedia

Module:Date time

From Wikipedia, the free encyclopedia
Module documentation[view] [edit] [history] [purge]
This module is rated as ready for general use. It has reached a mature state, is considered relatively stable and bug-free, and may be used wherever appropriate. It can be mentioned on help pages and other Wikipedia resources as an option for new users. To minimise server load and avoid disruptive output, improvements should be developed through sandbox testing rather than repeated trial-and-error editing.
Page template-protected This module is currently protected from editing.
See the protection policy and protection log for more details. Please discuss any changes on the talk page; you may submit an edit request to ask an administrator to make an edit if it is uncontroversial or supported by consensus. You may also request that this page be unprotected.
Warning This Lua module is used on approximately 535,000 pages .
To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them.
This module depends on the following other modules:

Module:Date time provides functions for validating and formatting dates in templates such as {{Start date }}, {{End date }}, {{Start date and age }}, and {{End date and age }}.

It handles:

  • Validation of date components (year, month, day)
  • Validation of time components (hour, minute, second)
  • Timezone formatting and validation
  • Generation of appropriate hCalendar microformat markup
  • "time-ago" calculations

Returns an error message and category if validation fails.

Usage

Tracking categories

The above documentation is transcluded from Module:Date time/doc. (edit | history)
Editors can experiment in this module's sandbox (edit | diff) and testcases (edit | run) pages.
Subpages of this module.

 --[[
 Module:Date time – Date formatting and validation module.

 This module provides functions for validating and formatting dates in templates such as
 {{Start date}}, {{End date}}, {{Start date and age}}, and {{End date and age}}. 
 It handles:
 - Validation of date components (year, month, day)
 - Validation of time components (hour, minute, second)
 - Timezone formatting and validation
 - Generation of appropriate hCalendar microformat markup
 - "time-ago" calculations

 Design notes:
 - Functions are organized into helper, validation, and formatting sections
 - Error handling uses a consistent pattern with centralized error messages
 - Timezone validation supports standard ISO 8601 formats
 - Leap year calculation is cached for performance
 ]]

 require("strict")

 localp={}

 ---------------
 -- Constants --
 ---------------

 localHTML_SPACE=" "
 localHTML_NBSP=" "

 -- Error message constants
 localERROR_MESSAGES={
 integers="All values must be integers",
 has_leading_zeros="Values cannot have unnecessary leading zeros",
 missing_year="Year value is required",
 invalid_month="Value is not a valid month",
 missing_month="Month value is required when a day is provided",
 invalid_day="Value is not a valid day (Month %d has %d days)",
 invalid_hour="Value is not a valid hour",
 invalid_minute="Value is not a valid minute",
 invalid_second="Value is not a valid second",
 timezone_incomplete_date="A timezone cannot be set without a day and hour",
 invalid_timezone="Value is not a valid timezone",
 yes_value_parameter='%s must be either "yes" or "y"',
 duplicate_parameters='Duplicate parameters used: %s and %s',
 template="Template not supported",
 time_without_hour="Minutes and seconds require an hour value"
 }

 -- Template class mapping
 -- "itvstart" and "itvend" are unique classes used by the TV infoboxes,
 -- which only allow the usage of {{Start date}} and {{End date}}.
 localTEMPLATE_CLASSES={
 ["start date"]="bday dtstart published updated itvstart",
 ["start date and age"]="bday dtstart published updated",
 ["end date"]="dtend itvend",
 ["end date and age"]="dtend"
 }

 -- Templates that require "time ago" calculations
 localTIME_AGO={
 ["start date and age"]=true,
 ["end date and age"]=true
 }

 -- English month names
 localMONTHS={
 "January","February","March","April","May","June",
 "July","August","September","October","November","December"
 }

 -- Error category
 localERROR_CATEGORY="[[Category:Pages using Module:Date time with invalid values]]"

 -- Namespaces where error categories should be applied
 localCATEGORY_NAMESPACES={
 [0]=true,-- Article
 [1]=true,-- Article talk
 [4]=true,-- Wikipedia
 [10]=true,-- Template
 [100]=true,-- Portal
 [118]=true-- Draft
 }

 -- Cached leap year calculations for performance
 localleap_year_cache={}

 -- Local variables for error handling
 localhelp_link

 ----------------------
 -- Helper Functions --
 ----------------------

 --- Pads a number with leading zeros to ensure a minimum of two digits.
 -- @param value (number|string) The value to pad with leading zeros
 -- @return string The value padded to at least two digits, or nil if input is nil
 localfunctionpad_left_zeros(value)
 ifvalue==nilthen
 returnnil
 end

 localstr=tostring(value)
 returnstring.rep("0",math.max(0,2-#str))..str
 end

 --- Replaces [[U+2212]] (Unicode minus) with [[U+002D]] (ASCII hyphen) or vice versa.
 -- @param value (string) The string value to process
 -- @param to_unicode (boolean) If true, converts ASCII hyphen to Unicode minus;
 -- If false, converts Unicode minus to ASCII hyphen
 -- @return string The processed string with appropriate minus characters, or nil if input is nil
 localfunctionreplace_minus_character(value,to_unicode)
 ifnotvaluethen
 returnnil
 end

 ifto_unicodethen
 returnvalue:gsub("-","−")
 end

 returnvalue:gsub("−","-")
 end

 --- Normalizes timezone format by ensuring proper padding of hours.
 -- @param timezone (string) The timezone string to normalize
 -- @return string The normalized timezone string with properly padded hours, or nil if input is nil
 localfunctionfix_timezone(timezone)
 ifnottimezonethen
 returnnil
 end

 -- Replace U+2212 (Unicode minus) with U+002D (ASCII hyphen)
 timezone=replace_minus_character(timezone,false)

 -- Match the timezone pattern for ±H:MM format
 localsign,hour,minutes=timezone:match("^([+-])(%d+):(%d+)$")

 ifsignandhourandminutesthen
 -- Pad the hour with a leading zero if necessary
 hour=pad_left_zeros(hour)
 returnsign..hour..":"..minutes
 end

 -- If no match, return the original timezone (this handles invalid or already padded timezones)
 returntimezone
 end

 --- Checks if a timezone string is valid according to standard timezone formats.
 -- Valid timezones range from UTC-12:00 to UTC+14:00.
 -- @param timezone (string) The timezone string to validate
 -- @return boolean true if the timezone is valid, false otherwise
 localfunctionis_timezone_valid(timezone)
 -- Consolidated timezone pattern for better performance
 localvalid_patterns={
 -- Z (UTC)
 "^Z$",
 -- Full timezone with minutes ±HH:MM
 "^[+]0[1-9]:[0-5][0-9]$",
 "^[+-]0[1-9]:[0-5][0-9]$",
 "^[+-]1[0-2]:[0-5][0-9]$",
 "^[+]1[34]:[0-5][0-9]$",
 -- Whole hour timezones ±HH
 "^[+-]0[1-9]$",
 "^[+-]1[0-2]$",
 "^[+]1[34]$",
 -- Special cases
 "^[+]00:00$",
 "^[+]00$"
 }

 -- Additional checks for invalid -00 and -00:00 cases
 iftimezone=="-00"ortimezone=="-00:00"then
 returnfalse
 end

 for_,patterninipairs(valid_patterns)do
 ifstring.match(timezone,pattern)then
 returntrue
 end
 end

 returnfalse
 end

 --- Checks if a given year is a leap year.
 -- Uses a cache for better performance.
 -- @param year (number) The year to check for leap year status
 -- @return boolean true if the year is a leap year, false otherwise
 localfunctionis_leap_year(year)
 ifleap_year_cache[year]==nilthen
 leap_year_cache[year]=(year%4==0andyear%100~=0)or(year%400==0)
 end
 returnleap_year_cache[year]
 end

 --- Returns the number of days in a given month of a specified year.
 -- Handles leap years for February.
 -- @param year (number) The year to check for leap year conditions
 -- @param month (number) The month (1-12) for which to return the number of days
 -- @return number The number of days in the specified month, accounting for leap years
 localfunctionget_days_in_month(year,month)
 localdays_in_month={31,28,31,30,31,30,31,31,30,31,30,31}

 ifmonth==2andis_leap_year(year)then
 return29
 end

 returndays_in_month[month]or0
 end

 --- Checks if a given value has invalid leading zeros.
 -- @param value (string) The value to check for leading zeros
 -- @param field_type (string) Field type ("day", "month", "hour", "minute", "second")
 -- @return boolean true if the value has invalid leading zeros, false otherwise
 localfunctionhas_leading_zeros(value,field_type)
 value=tostring(value)

 -- Common checks for day and month
 iffield_type=="day"orfield_type=="month"then
 -- Reject "00" and values with leading zero followed by more than one digit
 returnvalue=="00"or
 string.match(value,"^0[0-9][0-9]$")~=nilor
 string.match(value,"^0[1-9][0-9]")~=nil
 end

 -- Checks for hour, minute, second
 iffield_type=="hour"orfield_type=="minute"orfield_type=="second"then
 -- Allow "00" and "01" to "09"
 ifvalue=="00"orstring.match(value,"^0[1-9]$")then
 returnfalse
 end

 -- Reject values starting with "0" followed by more than one digit
 returnstring.match(value,"^0[0-9][0-9]+$")~=nil
 end

 returnfalse
 end

 --- Checks if a given value is an integer.
 -- @param value (string|number) The value to check
 -- @return boolean true if the value is a valid integer, false otherwise
 localfunctionis_integer(value)
 ifnotvaluethen
 returnfalse
 end
 -- Check if the value is a number first
 localnum_value=tonumber(value)
 ifnotnum_valuethen
 returnfalse
 end
 -- Check if it's an integer by comparing floor with the original
 ifmath.floor(num_value)~=num_valuethen
 returnfalse
 end
 -- For string inputs, check for decimal point to reject values like "7."
 iftype(value)=="string"then
 -- If the string contains a decimal point, it's not an integer
 ifstring.find(value,"%.")then
 returnfalse
 end
 end
 returntrue
 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
 localfunctionget_month_name(month_number)
 month_number=tonumber(month_number)
 returnMONTHS[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
 localfunctiongenerate_error(message,add_tracking_category)
 localcategory=ERROR_CATEGORY

 ifadd_tracking_category==falsethen
 category=""
 end

 -- Get current page title object
 localarticle_title=mw.title.getCurrentTitle()

 -- Special case for testcases pages
 localis_test_page=article_title.subpageText=="testcases"
 localallow_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(notCATEGORY_NAMESPACES[article_title.namespace]andnotallow_this_test_page)
 or(is_test_pageandnotallow_this_test_page)then
 category=""
 end

 return'<strong class="error">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
 localfunctionformat_time_string(hour,minute,second)
 ifnothourthen
 return""
 end

 localtime_string=string.format("%s:%s",hour,minute)

 ifsecondandsecond~="00"andminute~="00"then
 time_string=string.format("%s:%s",time_string,second)
 end

 returntime_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
 localfunctionformat_date_string(year,month,day,date_format_dmy)
 ifnotyearthen
 return""
 end

 localdate_string
 ifmonththen
 localmonth_name=get_month_name(month)

 ifdaythen
 day=tonumber(day)
 ifdate_format_dmythen
 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

 returndate_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
 localfunctionformat_timezone(timezone)
 ifnottimezonethen
 return""
 end

 returnHTML_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
 localfunctiongenerate_h_calendar(date_time_values,classes)
 localparts={}

 ifdate_time_values.yearthen
 table.insert(parts,date_time_values.year)

 ifdate_time_values.monththen
 table.insert(parts,"-"..date_time_values.month)

 ifdate_time_values.daythen
 table.insert(parts,"-"..date_time_values.day)
 end
 end

 ifdate_time_values.hourthen
 table.insert(parts,"T"..date_time_values.hour)

 ifdate_time_values.minutethen
 table.insert(parts,":"..date_time_values.minute)

 ifdate_time_values.secondthen
 table.insert(parts,":"..date_time_values.second)
 end
 end
 end
 end

 localh_calendar_content=table.concat(parts)..(date_time_values.timezoneor"")
 localclass_span=string.format('<span class="%s">',classes)

 returnstring.format(
 '<span style="display: none;">%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
 localfunctionget_time_ago(date_time_values,br,p)
 -- Build timestamp based on available date components
 localtimestamp
 localmin_magnitude

 ifdate_time_values.daythen
 -- 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"
 elseifdate_time_values.monththen
 -- Format with padding for month if needed
 timestamp=string.format("%d-%02d",
 date_time_values.year,
 date_time_values.month)

 -- Get the current date
 localcurrent_date=os.date("*t")

 -- Compute the difference in months
 localyear_diff=current_date.year-date_time_values.year
 localmonth_diff=(year_diff*12)+(current_date.month-date_time_values.month)

 -- If the difference is less than 12 months, use "months", otherwise "years"
 ifmonth_diff<12then
 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
 localm_time_ago=require("Module:Time ago")._main
 localtime_ago=m_time_ago({timestamp,["min_magnitude"]=min_magnitude})

 -- Format the result based on br and p parameters
 ifbrthen
 time_ago=pand("<br/>("..time_ago..")")or(";<br/>"..time_ago)
 else
 time_ago=pand(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
 localfunction_validate_date_time(args)
 localtemplate_name=args.templateor"start date"
 help_link=string.format("<small>[[:Template:%s|(help)]]</small>",template_name)

 -- Store and validate date-time values
 localdate_time_values={
 year=args[1],
 month=args[2],
 day=args[3],
 hour=args[4],
 minute=args[5],
 second=args[6]
 }

 -- Validate each value
 forkey,valueinpairs(date_time_values)do
 ifvaluethen
 -- Check for integer and leading zeros
 ifnotis_integer(value)then
 returngenerate_error(ERROR_MESSAGES.integers)
 end

 ifhas_leading_zeros(tostring(value),key)then
 returngenerate_error(ERROR_MESSAGES.has_leading_zeros)
 end

 -- Convert to number
 date_time_values[key]=tonumber(value)
 end
 end

 -- Validate date components
 ifnotdate_time_values.yearthen
 returngenerate_error(ERROR_MESSAGES.missing_year)
 end

 ifdate_time_values.monthand(date_time_values.month<1ordate_time_values.month>12)then
 returngenerate_error(ERROR_MESSAGES.invalid_month)
 end

 ifdate_time_values.daythen
 ifnotdate_time_values.monththen
 returngenerate_error(ERROR_MESSAGES.missing_month)
 end

 localmax_day=get_days_in_month(date_time_values.year,date_time_values.month)
 ifdate_time_values.day<1ordate_time_values.day>max_daythen
 returngenerate_error(string.format(ERROR_MESSAGES.invalid_day,date_time_values.month,max_day))
 end
 end

 -- Validate time components
 if(date_time_values.minuteordate_time_values.second)andnotdate_time_values.hourthen
 returngenerate_error(ERROR_MESSAGES.time_without_hour)
 end

 ifdate_time_values.hourand(date_time_values.hour<0ordate_time_values.hour>23)then
 returngenerate_error(ERROR_MESSAGES.invalid_hour)
 end

 ifdate_time_values.minuteand(date_time_values.minute<0ordate_time_values.minute>59)then
 returngenerate_error(ERROR_MESSAGES.invalid_minute)
 end

 ifdate_time_values.secondand(date_time_values.second<0ordate_time_values.second>59)then
 returngenerate_error(ERROR_MESSAGES.invalid_second)
 end

 -- Timezone cannot be set without a specific date and hour
 ifargs[7]andnot(date_time_values.dayanddate_time_values.hour)then
 returngenerate_error(ERROR_MESSAGES.timezone_incomplete_date)
 elseifargs[7]andnotis_timezone_valid(args[7])then
 returngenerate_error(ERROR_MESSAGES.invalid_timezone)
 end

 -- Validate that there aren't any duplicate parameters
 ifargs.pandargs.parenthen
 returngenerate_error(string.format(ERROR_MESSAGES.duplicate_parameters,"p","paren"))
 end

 -- Validate parameters that use "y" or "yes" for values	
 localboolean_params={'df','p','paren','br'}
 for_,param_nameinipairs(boolean_params)do
 ifargs[param_name]andnot(args[param_name]=="yes"orargs[param_name]=="y")then
 returngenerate_error(string.format(ERROR_MESSAGES.yes_value_parameter,param_name))
 end
 end

 returnnil
 end

 ----------------------
 -- Public Functions --
 ----------------------

 --- Validates date-time values from template arguments.
 -- @param frame (table) The MediaWiki frame containing template arguments
 -- @return nil|string Result of date-time validation
 functionp.validate_date_time(frame)
 localget_args=require("Module:Arguments").getArgs
 localargs=get_args(frame)

 -- Sanitize inputs
 args[7]=fix_timezone(args[7])

 return_validate_date_time(args)
 end

 --- Generates a formatted date string with microformat markup.
 -- @param frame (table) The MediaWiki frame containing template arguments
 -- @return string A formatted date string, or an error message if validation fails
 functionp.generate_date(frame)
 localget_args=require("Module:Arguments").getArgs
 localargs=get_args(frame)

 -- Sanitize inputs
 args[7]=fix_timezone(args[7])

 localvalidation_error=_validate_date_time(args)
 ifvalidation_errorthen
 returnvalidation_error
 end

 localclasses=TEMPLATE_CLASSES[args.templateor"start date"]
 ifnotclassesthen
 returngenerate_error(ERROR_MESSAGES.template,false)
 end

 -- Process date-time values
 localdate_time_values={
 year=args[1],
 month=pad_left_zeros(args[2]),
 day=pad_left_zeros(args[3]),
 hour=pad_left_zeros(args[4]),
 minute=args[5]andpad_left_zeros(args[5])or"00",
 second=args[6]andpad_left_zeros(args[6])or"00",
 timezone=replace_minus_character(args[7],true)-- Restore U+2212 (Unicode minus)
 }

 -- Generate individual components
 localtime_string=format_time_string(
 date_time_values.hour,
 date_time_values.minute,
 date_time_values.second
 )

 localdate_string=format_date_string(
 date_time_values.year,
 date_time_values.month,
 date_time_values.day,
 args.df
 )

 localtimezone_string=format_timezone(date_time_values.timezone)

 localtime_ago=""
 ifTIME_AGO[args.template]then
 time_ago=get_time_ago(
 date_time_values,
 args.br,
 args.porargs.paren
 )
 end

 localh_calendar=generate_h_calendar(date_time_values,classes)

 -- Combine components
 returntime_string..date_string..timezone_string..time_ago..h_calendar
 end

 returnp

AltStyle によって変換されたページ (->オリジナル) /