Module:Infobox/dates/sandbox

-- This module provides functions to format date ranges according to MOS:DATERANGE.

local p = {}

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

-- Define constants for reuse throughout the module

local DASH = '–' -- en dash

local DASH_BREAK = ' –
' -- en dash with line break

local DEFAULT_ERROR_CATEGORY = 'Pages with incorrectly formatted date ranges'

local MONTHS = {

January = 1, February = 2, March = 3, April = 4,

May = 5, June = 6, July = 7, August = 8,

September = 9, October = 10, November = 11, December = 12

}

-- =============================================

-- Template validation

-- =============================================

-- Template should be moved eventually to the infobox television season module.

--- Validates date formats in infobox templates.

-- @param frame Frame object from Wikipedia

-- @return Error category string if validation fails, nil otherwise

function p.start_end_date_template_validation(frame)

local args = getArgs(frame)

local error_category = args.error_category or DEFAULT_ERROR_CATEGORY

local start_date = args.first_aired or args.released or args.airdate or args.release_date or args.airdate_overall

if start_date then

if not start_date:find("dtstart") then

return error_category

end

end

local end_date = args.last_aired

if end_date then

if not end_date:find("dtend") and end_date ~= "present" then

return error_category

end

end

return nil -- Return nil if validation passes

end

-- =============================================

-- Helper functions

-- =============================================

--- Replace non-breaking spaces with regular spaces.

-- @param value String to process

-- @return Processed string with regular spaces

local function replace_space(value)

if value then

return value:gsub(" ", " ")

end

return value

end

--- Extract the hidden span portion from text if it exists.

-- @param text Input text that may contain a span element

-- @return The span portion or empty string

local function extract_span(text)

if not text then

return ""

end

local span_start = string.find(text, "

if span_start then

return string.sub(text, span_start)

end

return ""

end

--- Extract visible part (before any span).

-- @param text Input text

-- @return Visible portion of the text

local function extract_visible(text)

if not text then

return ""

end

return text:match("^(.-)

end

--- Parse date components from visible text.

-- @param visible_text The visible portion of a date string

-- @return Table with date components and format

local function parse_date(visible_text)

if not visible_text then

return {

prefix = "",

month = nil,

day = nil,

year = nil,

suffix = "",

format = nil

}

end

local date_format = "mdy" -- Default format

local prefix, month, day, year, suffix

-- Try MDY format first (e.g., "January 15, 2020")

prefix, month, day, year, suffix = string.match(visible_text, '(.-)(%u%a+)%s(%d+),%s(%d+)(.*)')

-- If MDY failed, try DMY format (e.g., "15 January 2020")

if year == nil then

date_format = "dmy"

prefix, day, month, year, suffix = string.match(visible_text, '(.-)(%d%d?)%s(%u%a+)%s(%d+)(.*)')

end

-- If month and year only (e.g., "April 2015")

if year == nil then

month, year = visible_text:match('(%u%a+)%s(%d%d%d%d)')

prefix, suffix, day = "", "", nil

end

-- If year only (e.g., "2015")

if year == nil then

year = visible_text:match('(%d%d%d%d)')

prefix, suffix, month, day = "", "", nil, nil

end

-- Handle "present" case

if visible_text:find("present") then

year = "present"

prefix, suffix, month, day = "", "", nil, nil

end

-- Set default empty strings for optional components

suffix = suffix or ''

prefix = prefix or ''

return {

prefix = prefix,

month = month,

day = day,

year = year,

suffix = suffix,

format = date_format

}

end

--- Get month number from name.

-- @param month_name Name of the month

-- @return Number corresponding to the month or nil if invalid

local function get_month_number(month_name)

return month_name and MONTHS[month_name]

end

--- Format date range for same year according to Wikipedia style.

-- @param date1 First date components

-- @param date2 Second date components

-- @param span1 First date span HTML

-- @param span2 Second date span HTML

-- @return Formatted date range string

local function format_same_year(date1, date2, span1, span2)

-- Both dates have just year, no month or day

if date1.month == nil and date2.month == nil then

return date1.prefix .. date1.year .. span1 .. DASH .. date2.year .. span2

end

-- Both dates have month and year, but no day

if date1.day == nil and date2.day == nil then

return date1.prefix .. date1.month .. span1 .. DASH .. date2.month .. ' ' .. date1.year .. span2

end

-- Same month and year

if date1.month == date2.month then

if date1.format == "dmy" then

-- Format: d1–d2 m1 y1 (5–7 January 1979)

return date1.prefix .. date1.day .. span1 .. DASH .. date2.day .. ' ' .. date1.month .. ' ' .. date1.year .. span2

else

-- Format: m1 d1–d2, y1 (January 5–7, 1979)

return date1.prefix .. date1.month .. ' ' .. date1.day .. span1 .. DASH .. date2.day .. ', ' .. date1.year .. span2

end

else

-- Different months, same year

if date1.format == "dmy" then

-- Format: d1 m1 – d2 m2 y1 (3 June –
18 August 1952)

return date1.prefix .. date1.day .. ' ' .. date1.month .. span1 .. DASH_BREAK .. date2.day .. ' ' .. date2.month .. ' ' .. date1.year .. span2

else

-- Format: m1 d1 – m2 d2, y1 (June 3 –
August 18, 1952)

return date1.prefix .. date1.month .. ' ' .. date1.day .. span1 .. DASH_BREAK .. date2.month .. ' ' .. date2.day .. ', ' .. date1.year .. span2

end

end

end

--- Format date range with "present" as the end date

-- @param date1 Start date components

-- @param span1 Start date span HTML

-- @return Formatted date range string with "present" as end date

local function format_present_range(date1, span1)

-- Year only

if date1.month == nil then

return date1.prefix .. date1.year .. span1 .. DASH .. "present"

end

-- Month and year, no day

if date1.day == nil then

return date1.prefix .. date1.month .. ' ' .. date1.year .. span1 .. " " .. DASH .. " present"

end

-- Full date (with line break)

if date1.format == "dmy" then

return date1.prefix .. date1.day .. ' ' .. date1.month .. ' ' .. date1.year .. span1 .. DASH_BREAK .. "present"

else

return date1.prefix .. date1.month .. ' ' .. date1.day .. ', ' .. date1.year .. span1 .. DASH_BREAK .. "present"

end

end

--- Format date range for different years.

-- @param date1 First date components

-- @param date2 Second date components

-- @param visible_text1 Visible text of first date

-- @param visible_text2 Visible text of second date

-- @param span1 First date span HTML

-- @param span2 Second date span HTML

-- @return Formatted date range string for different years

local function format_different_years(date1, date2, visible_text1, visible_text2, span1, span2)

-- If both entries are just years, use simple dash without line break

if date1.month == nil and date2.month == nil then

return visible_text1 .. span1 .. DASH .. visible_text2 .. span2

end

-- If one of them has a month or day, use dash with line break

return visible_text1 .. span1 .. DASH_BREAK .. visible_text2 .. span2

end

--- Validate that date2 is after date1

-- @param date1 First date components

-- @param date2 Second date components

-- @return Boolean indicating if date range is valid

local function validate_date_range(date1, date2)

-- Skip validation if one date is just a year or if second date is "present"

if not date1.month or not date2.month or date2.year == "present" then

return true

end

local month1_number = get_month_number(date1.month)

local month2_number = get_month_number(date2.month)

-- If invalid month names, consider validation failed

if not month1_number or not month2_number then

return false

end

-- Convert year strings to numbers

local year1 = tonumber(date1.year)

local year2 = tonumber(date2.year)

if not year1 or not year2 then

return false

end

-- If years are different, comparison is simple

if year1 < year2 then

return true

elseif year1 > year2 then

return false

end

-- Same year, compare months

if month1_number < month2_number then

return true

elseif month1_number > month2_number then

return false

end

-- Same year and month, compare days if available

if date1.day and date2.day then

local day1 = tonumber(date1.day)

local day2 = tonumber(date2.day)

if not day1 or not day2 then

return false

end

return day1 <= day2

end

-- Same year and month, no days to compare

return true

end

-- =============================================

-- Main function

-- =============================================

--- Format date ranges according to Wikipedia style.

-- @param frame Frame object from Wikipedia

-- @return Formatted date range string

function p.dates(frame)

local args = getArgs(frame)

-- Handle missing or empty arguments cases

if not args[1] and not args[2] then

return ''

elseif not args[1] then

return args[2] or ''

elseif not args[2] then

return args[1] or ''

end

-- Get spans from original inputs

local span1 = extract_span(args[1])

local span2 = extract_span(args[2])

-- Get visible parts only

local visible_text1 = extract_visible(args[1])

local visible_text2 = extract_visible(args[2])

-- Clean up spaces

visible_text1 = replace_space(visible_text1)

visible_text2 = replace_space(visible_text2)

-- Parse dates

local date1 = parse_date(visible_text1)

local date2 = parse_date(visible_text2)

-- Handle unparsable dates (fallback to original format)

if date1.year == nil or (date2.year == nil and not string.find(visible_text2 or "", "present")) then

return (args[1] or ) .. DASH .. (args[2] or )

end

-- Handle "present" as end date

if (visible_text2 and visible_text2:find("present")) or date2.year == "present" then

return format_present_range(date1, span1)

end

-- Validate date range

if not validate_date_range(date1, date2) then

return 'Invalid date range'

end

-- Format based on whether years are the same

if date1.year == date2.year then

return format_same_year(date1, date2, span1, span2)

else

-- Different years

return format_different_years(date1, date2, visible_text1, visible_text2, span1, span2)

end

end

return p