Module:DatetimeUtil

local LibraryUtil = require 'libraryUtil'

local MONTHS_FULL = {'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'} local MONTHS_SHORT = {'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'}

local PDT_RANGES = { [2017] = {1489312800, 1509872400}, -- Sun Mar 12 10:00:00 2017 - Sun Nov 5 09:00:00 2017 [2018] = {1520762400, 1541322000}, -- Sun Mar 11 10:00:00 2018 - Sun Nov 4 09:00:00 2018 [2019] = {1552212000, 1572771600}, -- Sun Mar 10 10:00:00 2019 - Sun Nov 3 09:00:00 2019 [2020] = {1583661600, 1604221200}, -- Sun Mar 8 10:00:00 2020 - Sun Nov  1 09:00:00 2020 [2021] = {1615716000, 1636275600}, -- Sun Mar 14 10:00:00 2021 - Sun Nov 7 09:00:00 2021 [2022] = {1647165600, 1667725200}, -- Sun Mar 13 10:00:00 2022 - Sun Nov 6 09:00:00 2022 [2023] = {1678615200, 1699174800}, -- Sun Mar 12 10:00:00 2023 - Sun Nov 5 09:00:00 2023 [2024] = {1710064800, 1730624400}, -- Sun Mar 10 10:00:00 2024 - Sun Nov 3 09:00:00 2024 [2025] = {1741514400, 1762074000}, -- Sun Mar 9 10:00:00 2025 - Sun Nov  2 09:00:00 2025 }

local get_dst_ranges = function (s, e)	mw.log('local PDT_RANGES = {') for year = s, e do		-- 2nd Sunday in March, 02:00 PST local datetime = {year = year, month = 3, day = 1, hour = 10} while os.date('!%w', os.time(datetime)) ~= '0' do			datetime.day = datetime.day + 1 end datetime.day = datetime.day + 7 local pdt_start = os.time(datetime)

-- 1st Sunday in November, 02:00 PDT datetime.month = 11 datetime.day = 1 datetime.hour = 9 while os.date('!%w', os.time(datetime)) ~= '0' do			datetime.day = datetime.day + 1 end local pdt_end = os.time(datetime)

mw.log(('\t[%d] = {%d, %d}, -- %s - %s'):format(year, pdt_start, pdt_end, os.date('!%c', pdt_start), os.date('!%c', pdt_end))) end mw.log('}') end

local is_iso8601 = function (str) return type(str) == 'string' and string.find(str, '^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z$') ~= nil end

local to_iso8601 = function (t) return os.date('!%Y-%m-%dT%H:%M:%SZ', t) end

local from_iso8601 = function (str) local y, m, d, hh, mm, ss = string.match(str, '^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z$') if ss then return os.time {year = y, month = m, day = d, hour = hh, min = mm, sec = ss} end end

local parse_iso8601 = function (str) local y, m, d, hh, mm, ss = string.match(str, '^(%d+)-(%d+)-(%d+)T(%d+):(%d+):(%d+)Z$') if ss then return tonumber(y), tonumber(m), tonumber(d), tonumber(hh), tonumber(mm), tonumber(ss) end end

local make_iso8601 = function (y, m, d, hh, mm, ss) return os.date('!%Y-%m-%dT%H:%M:%SZ', os.time {year = y, month = m, day = d, hour = hh or 0, min = mm or 0, sec = ss or 0}) end

local MIN_TIME = to_iso8601(0) local MAX_TIME = to_iso8601(0x7FFFFFFF)

local is_cargo = function (str) return type(str) == 'string' and string.find(str, '^(%d+)%-(%d+)%-(%d+) (%d+):(%d+):(%d+)$') ~= nil end

local to_cargo = function (t) return os.date('!%Y-%m-%d %H:%M:%S', t) end

local from_cargo = function (str) local y, m, d, hh, mm, ss = string.match(str, '^(%d+)%-(%d+)%-(%d+) (%d+):(%d+):(%d+)$') if ss then return os.time {year = y, month = m, day = d, hour = hh, min = mm, sec = ss} end

y, m, d = string.match(str, '^(%d+)%-(%d+)%-(%d+)$') if d then return os.time {year = y, month = m, day = d, hour = 0} end end

local parse_cargo = function (str) local y, m, d, hh, mm, ss = string.match(str, '^(%d+)-(%d+)-(%d+) (%d+):(%d+):(%d+)$') if ss then return tonumber(y), tonumber(m), tonumber(d), tonumber(hh), tonumber(mm), tonumber(ss) end

y, m, d = string.match(str, '^(%d+)-(%d+)-(%d+)$') if d then return tonumber(y), tonumber(m), tonumber(d), 0, 0, 0 end end

local make_cargo = function (y, m, d, hh, mm, ss) return os.date('!%Y-%m-%d %H:%M:%S', os.time {year = y, month = m, day = d, hour = hh or 0, min = mm or 0, sec = ss or 0}) end

local Template_Hover_nocargo = function (text, title) if title == nil or title == '' then return text end return tostring(mw.html.create('span'):attr('title', mw.text.decode(title)) -- mw.html automatically HTML-escapes attributes		:css('border-bottom', '0'):css('text-decoration', 'underline dotted'):css('cursor', 'help')		:wikitext(text)) end

local ht1 = function (t) LibraryUtil.checkTypeMulti('t', 1, t, {'number', 'string', 'nil'}) if t == nil or t == '' then return (Unknown datetime) end

local timestamp = type(t) == 'number' and t or from_iso8601(t) or from_cargo(t) if timestamp then local datetime = os.date('!*t', timestamp) local pdt_range = PDT_RANGES[datetime.year] -- done manually as datetime.isdst is always false local is_dst = pdt_range and timestamp >= pdt_range[1] and timestamp < pdt_range[2] local pdt_datetime = os.date('!*t', timestamp - 60 * 60 * (is_dst and 7 or 8))

-- https://developer.mozilla.org/en-US/docs/Web/HTML/Element/time: -- "If the element does not have a datetime attribute, it must not have any		-- element descendants, and the datetime value is the element's child text content." local text = tostring(mw.html.create('time'):wikitext(to_iso8601(timestamp))) local title = ('Pacific %s Time: %d/%d/%02d at %02d:%02d %s'):format(			is_dst and 'Daylight' or 'Standard',			pdt_datetime.month, pdt_datetime.day, pdt_datetime.year % 100,			(pdt_datetime.hour - 1) % 12 + 1, pdt_datetime.min, pdt_datetime.hour >= 12 and 'p.m.' or 'a.m.') return Template_Hover_nocargo(text, title) else mw.addWarning('Invalid timestamp: ' .. t)		return require 'Module:Error'.error(t) end end

local ht_range = function (t1, t2) return ('%s – %s'):format(t1 ~= nil and ht1(t1) or , t2 ~= nil and ht1(t2) or ) end

local ht = function (args) return ht1(tonumber(args[1]) or args[1]) end

local p = require 'Module:MakeMWModule'.makeMWModule {ht = ht} return { MIN_TIME = MIN_TIME, MAX_TIME = MAX_TIME, MONTHS_FULL = MONTHS_FULL, MONTHS_SHORT = MONTHS_SHORT,

is_iso8601 = is_iso8601, to_iso8601 = to_iso8601, from_iso8601 = from_iso8601, parse_iso8601 = parse_iso8601, make_iso8601 = make_iso8601,

is_cargo = is_cargo, to_cargo = to_cargo, from_cargo = from_cargo, parse_cargo = parse_cargo, make_cargo = make_cargo,

HT = p.ht, HT_ = p.ht_, ht = ht1, ht_range = ht_range, }