Editing Module:Date
The edit can be undone. Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
Latest revision | Your text | ||
Line 1: | Line 1: | ||
-- Date functions for use by other modules. | -- Date functions for implementing templates and for use by other modules. | ||
-- I18N and time zones are not supported. | -- I18N and time zones are not supported. | ||
local MINUS = '−' -- Unicode U+2212 MINUS SIGN | local MINUS = '−' -- Unicode U+2212 MINUS SIGN | ||
local function collection() | local function collection() | ||
Line 30: | Line 12: | ||
self[self.n] = item | self[self.n] = item | ||
end, | end, | ||
join = | join = function (self, sep) | ||
return table.concat(self, sep) | |||
end, | |||
} | } | ||
end | end | ||
local function strip_to_nil(text) | local function strip_to_nil(text) | ||
-- If text is a string, return its | -- Return nil if text is nil or is an empty string after trimming. | ||
-- Otherwise return text (convenient when | -- If text is a non-blank string, return its content after trimming. | ||
-- Otherwise return text (convenient when accessed via another module). | |||
if type(text) == 'string' then | if type(text) == 'string' then | ||
local result = text:match("^%s*(.-)%s*$") | |||
if result == '' then | |||
return nil | |||
end | |||
return result | |||
end | |||
if text == nil then | |||
return nil | |||
end | end | ||
return text | return text | ||
end | |||
local function number_name(number, singular, plural, sep) | |||
-- Return the given number, converted to a string, with the | |||
-- separator (default space) and singular or plural name appended. | |||
plural = plural or (singular .. 's') | |||
sep = sep or ' ' | |||
return tostring(number) .. sep .. ((number == 1) and singular or plural) | |||
end | end | ||
Line 54: | Line 53: | ||
local function days_in_month(year, month, calname) | local function days_in_month(year, month, calname) | ||
-- Return number of days (1..31) in given month (1..12). | -- Return number of days (1..31) in given month (1..12). | ||
local month_days = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } | |||
if month == 2 and is_leap_year(year, calname) then | if month == 2 and is_leap_year(year, calname) then | ||
return 29 | return 29 | ||
end | end | ||
return | return month_days[month] | ||
end | end | ||
Line 77: | Line 63: | ||
-- Return jd, jdz from a Julian or Gregorian calendar date where | -- Return jd, jdz from a Julian or Gregorian calendar date where | ||
-- jd = Julian date and its fractional part is zero at noon | -- jd = Julian date and its fractional part is zero at noon | ||
-- jdz = | -- jdz = similar, but fractional part is zero at 00:00:00 | ||
-- http://www.tondering.dk/claus/cal/julperiod.php#formula | -- http://www.tondering.dk/claus/cal/julperiod.php#formula | ||
-- Testing shows this works for all dates from year -9999 to 9999! | -- Testing shows this works for all dates from year -9999 to 9999! | ||
Line 83: | Line 69: | ||
-- 1 January 4713 BC = (-4712, 1, 1) Julian calendar | -- 1 January 4713 BC = (-4712, 1, 1) Julian calendar | ||
-- 24 November 4714 BC = (-4713, 11, 24) Gregorian calendar | -- 24 November 4714 BC = (-4713, 11, 24) Gregorian calendar | ||
if not date.isvalid then | |||
return 0, 0 -- always return numbers to simplify usage | |||
end | |||
local floor = math.floor | |||
local offset | local offset | ||
local a = floor((14 - date.month)/12) | local a = floor((14 - date.month)/12) | ||
local y = date.year + 4800 - a | local y = date.year + 4800 - a | ||
if date. | if date.calname == 'Julian' then | ||
offset = floor(y/4) - 32083 | offset = floor(y/4) - 32083 | ||
else | else | ||
Line 92: | Line 82: | ||
end | end | ||
local m = date.month + 12*a - 3 | local m = date.month + 12*a - 3 | ||
local | local date_part = date.day + floor((153*m + 2)/5) + 365*y + offset | ||
local time_part, zbias | |||
if date.hastime then | if date.hastime then | ||
time_part = (date.hour + (date.minute + date.second / 60) /60) / 24 - 0.5 | |||
zbias = 0 | |||
else | |||
time_part = 0 | |||
zbias = -0.5 | |||
end | end | ||
return jd, jd | local jd = date_part + time_part | ||
return jd, jd + zbias | |||
end | end | ||
local function set_date_from_jd(date) | local function set_date_from_jd(date) | ||
-- Set the fields of table date from its Julian date field. | -- Set the fields of table date from its Julian date field. | ||
-- http://www.tondering.dk/claus/cal/julperiod.php#formula | -- http://www.tondering.dk/claus/cal/julperiod.php#formula | ||
-- This handles the proleptic Julian and Gregorian calendars. | -- This handles the proleptic Julian and Gregorian calendars. | ||
-- Negative Julian dates are not defined but they work. | -- Negative Julian dates are not defined but they work. | ||
local calname = date. | local floor = math.floor | ||
local | local calname = date.calname | ||
if calname == ' | local jd = date.jd | ||
local limits -- min/max limits for date ranges −9999-01-01 to 9999-12-31 | |||
elseif calname == ' | if calname == 'Julian' then | ||
limits = { -1931076.5, 5373557.49999 } | |||
elseif calname == 'Gregorian' then | |||
limits = { -1930999.5, 5373484.49999 } | |||
else | else | ||
limits = { 1, 0 } -- impossible | |||
end | end | ||
if not (limits[1] <= jd and jd <= limits[2]) then | |||
if not ( | date.isvalid = false | ||
return | return | ||
end | end | ||
date.isvalid = true | |||
local jdn = floor(jd) | local jdn = floor(jd) | ||
if date.hastime then | if date.hastime then | ||
local time = jd - jdn | local time = jd - jdn | ||
if time >= 0.5 then | local hour | ||
if time >= 0.5 then | |||
jdn = jdn + 1 | jdn = jdn + 1 | ||
time = time - 0.5 | time = time - 0.5 | ||
hour = 0 | |||
else | else | ||
hour = 12 | |||
end | end | ||
date. | time = floor(time * 24 * 3600 + 0.5) -- number of seconds after hour | ||
date.second = time % 60 | |||
time = floor(time / 60) | |||
date.minute = time % 60 | |||
date.hour = hour + floor(time / 60) | |||
else | else | ||
date.second = 0 | date.second = 0 | ||
Line 149: | Line 152: | ||
date.month = m + 3 - 12*floor(m/10) | date.month = m + 3 - 12*floor(m/10) | ||
date.year = 100*b + d - 4800 + floor(m/10) | date.year = 100*b + d - 4800 + floor(m/10) | ||
end | end | ||
Line 201: | Line 160: | ||
return | return | ||
end | end | ||
local y = numbers. | local y = numbers.y or numbers[1] | ||
local m = numbers. | local m = numbers.m or numbers[2] | ||
local d = numbers. | local d = numbers.d or numbers[3] | ||
local H = numbers. | local H = numbers.H or numbers[4] | ||
local M = numbers. | local M = numbers.M or numbers[5] or 0 | ||
local S = numbers. | local S = numbers.S or numbers[6] or 0 | ||
if not (y and m and d) then | |||
if y and m and d then | return | ||
end | |||
if not (-9999 <= y and y <= 9999 and 1 <= m and m <= 12 and | |||
1 <= d and d <= days_in_month(y, m, date.calname)) then | |||
return | return | ||
end | end | ||
if | if H then | ||
date.hastime = true | |||
else | else | ||
H = 0 | |||
end | end | ||
if not (0 <= H and H <= 23 and | |||
if | 0 <= M and M <= 59 and | ||
0 <= S and S <= 59) then | |||
return | return | ||
end | end | ||
date.year = y -- -9999 to 9999 ('n BC' → year = 1 - n) | date.year = y -- -9999 to 9999 ('n BC' → year = 1 - n) | ||
date.month = m -- 1 to 12 | date.month = m -- 1 to 12 | ||
date.day = d -- 1 to 31 | date.day = d -- 1 to 31 | ||
date.hour = H -- 0 to 59 | date.hour = H -- 0 to 59 | ||
date.minute = M -- 0 to 59 | date.minute = M -- 0 to 59 | ||
date.second = S -- 0 to 59 | date.second = S -- 0 to 59 | ||
date.isvalid = true | |||
if type(options) == 'table' then | if type(options) == 'table' then | ||
for _, k in ipairs({ 'am', 'era | for _, k in ipairs({ 'am', 'era' }) do | ||
if options[k] then | if options[k] then | ||
date.options[k] = options[k] | date.options[k] = options[k] | ||
Line 273: | Line 200: | ||
end | end | ||
local function make_option_table( | local function make_option_table(options) | ||
-- If | -- If options is a string, return a table with its settings. | ||
-- Otherwise return options (it should already be a table). | |||
-- | if type(options) == 'string' then | ||
-- Example: 'am:AM era:BC' | |||
local result = {} | |||
for item in options:gmatch('%S+') do | |||
local lhs, rhs = item:match('^(%w+):(.*)$') | |||
if type( | |||
-- Example: 'am:AM era:BC' | |||
for item in | |||
local lhs, rhs = item:match('^(%w+) | |||
if lhs then | if lhs then | ||
result[lhs] = rhs | result[lhs] = rhs | ||
end | end | ||
end | end | ||
return result | |||
end | end | ||
return options | |||
return | |||
end | end | ||
Line 351: | Line 220: | ||
-- Return date formatted as a string using codes similar to those | -- Return date formatted as a string using codes similar to those | ||
-- in the C strftime library function. | -- in the C strftime library function. | ||
if not date.isvalid then | |||
return '(invalid)' | |||
end | |||
local shortcuts = { | local shortcuts = { | ||
['%c'] = '%-I:%M %p %-d %B % | ['%c'] = '%-I:%M %p %-d %B %Y%{era}', -- date and time: 2:30 pm 1 April 2016 | ||
['%x'] = '%-d %B % | ['%x'] = '%-d %B %Y%{era}', -- date: 1 April 2016 | ||
['%X'] = '%-I:%M %p', | ['%X'] = '%-I:%M %p', -- time: 2:30 pm | ||
} | } | ||
local codes = { | local codes = { | ||
a = { field = 'dayabbr' }, | a = { field = 'dayabbr' }, | ||
Line 373: | Line 241: | ||
M = { fmt = '%02d', fmt2 = '%d', field = 'minute' }, | M = { fmt = '%02d', fmt2 = '%d', field = 'minute' }, | ||
S = { fmt = '%02d', fmt2 = '%d', field = 'second' }, | S = { fmt = '%02d', fmt2 = '%d', field = 'second' }, | ||
j = { fmt = '%03d', fmt2 = '%d', field = ' | j = { fmt = '%03d', fmt2 = '%d', field = 'doy' }, | ||
I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' }, | I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' }, | ||
p = { field = 'hour', special = 'am' }, | p = { field = 'hour', special = 'am' }, | ||
} | } | ||
options = make_option_table(options | options = make_option_table(options or date.options) | ||
local amopt = options.am | local amopt = options.am | ||
local eraopt = options.era | local eraopt = options.era | ||
local function replace_code( | local function replace_code(modifier, id) | ||
local code = codes[id] | local code = codes[id] | ||
if code then | if code then | ||
Line 388: | Line 256: | ||
end | end | ||
local value = date[code.field] | local value = date[code.field] | ||
local special = code.special | local special = code.special | ||
if special then | if special then | ||
Line 401: | Line 266: | ||
['AM'] = { 'AM', 'PM' }, | ['AM'] = { 'AM', 'PM' }, | ||
['A.M.'] = { 'A.M.', 'P.M.' }, | ['A.M.'] = { 'A.M.', 'P.M.' }, | ||
}) | })[amopt] or { 'am', 'pm' } | ||
return | return value < 12 and ap[1] or ap[2] | ||
end | end | ||
end | end | ||
if code.field == 'year' then | if code.field == 'year' then | ||
if eraopt == 'BCMINUS' or eraopt == 'BCNEGATIVE' then | |||
local sign | |||
if value >= 0 then | if value >= 0 then | ||
sign = '' | sign = '' | ||
else | else | ||
sign = eraopt == 'BCMINUS' and MINUS or '-' | |||
value = -value | value = -value | ||
end | end | ||
return sign .. string.format(fmt, value) | |||
end | |||
if value <= 0 then | |||
value = 1 - value | |||
end | end | ||
end | end | ||
return | return fmt and string.format(fmt, value) or value | ||
end | end | ||
end | end | ||
local function replace_property( | local function replace_property(id) | ||
local result = date[id] | local result = date[id] | ||
if type(result) == 'string' then | if type(result) == 'string' then | ||
return | if id == 'era' and result ~= '' then | ||
-- Assume era follows a date. | |||
return ' ' .. result | |||
end | |||
return result | |||
end | end | ||
if type(result) == 'number' then | if type(result) == 'number' then | ||
return | return tostring(result) | ||
end | end | ||
if type(result) == 'boolean' then | if type(result) == 'boolean' then | ||
return | return result and '1' or '0' | ||
end | end | ||
-- This occurs if id | -- This occurs, for example, if id is the name of a function. | ||
return nil | return nil | ||
end | |||
if shortcuts[format] then | |||
format = shortcuts[format] | |||
end | end | ||
local PERCENT = '\127PERCENT\127' | local PERCENT = '\127PERCENT\127' | ||
return (format | return (format | ||
:gsub('%%%%', PERCENT) | :gsub('%%%%', PERCENT) | ||
:gsub(' | :gsub('%%{(%w+)}', replace_property) | ||
:gsub(' | :gsub('%%(-?)(%a)', replace_code) | ||
:gsub(PERCENT, '%%') | :gsub(PERCENT, '%%') | ||
) | ) | ||
end | end | ||
local function | local function date_text(date, fmt, options) | ||
-- Return | -- Return formatted string from given date. | ||
if not | if not (type(date) == 'table' and date.isvalid) then | ||
return '(invalid)' | |||
end | end | ||
if type(fmt) | if type(fmt) ~= 'string' then | ||
fmt = '%Y-%m-%d' | |||
if date.hastime then | if date.hastime then | ||
if date.second > 0 then | |||
fmt = fmt .. ' %H:%M:%S' | |||
else | |||
fmt = fmt .. ' %H:%M' | |||
end | |||
end | end | ||
return strftime(date, fmt, options or { era = 'BCMINUS' }) | |||
end | end | ||
if fmt:find('%', 1, true) then | |||
return strftime(date, fmt, options) | return strftime(date, fmt, options) | ||
end | end | ||
local t = collection() | local t = collection() | ||
for item in fmt:gmatch('%S+') do | for item in fmt:gmatch('%S+') do | ||
local f | local f | ||
if item == 'hm' then | if item == 'hm' then | ||
f = | f = '%H:%M' | ||
elseif item == 'hms' then | elseif item == 'hms' then | ||
f = '%H:%M:%S' | f = '%H:%M:%S' | ||
elseif item == 'ymd' then | elseif item == 'ymd' then | ||
f = '%Y | f = '%Y:%m:%d%{era}' | ||
elseif item == 'mdy' then | elseif item == 'mdy' then | ||
f = '%B %-d, % | f = '%B %-d, %Y%{era}' | ||
elseif item == 'dmy' then | elseif item == 'dmy' then | ||
f = '%-d %B % | f = '%-d %B %Y%{era}' | ||
else | else | ||
return | return '(invalid format)' | ||
end | end | ||
t:add(f) | t:add(f) | ||
end | end | ||
return strftime(date, t:join(' '), options) | |||
end | end | ||
Line 549: | Line 385: | ||
} | } | ||
local function | local function month_number(text) | ||
if type(text) == 'string' then | if type(text) == 'string' then | ||
return | local month_names = { | ||
jan = 1, january = 1, | |||
feb = 2, february = 2, | |||
mar = 3, march = 3, | |||
apr = 4, april = 4, | |||
may = 5, | |||
jun = 6, june = 6, | |||
jul = 7, july = 7, | |||
aug = 8, august = 8, | |||
sep = 9, september = 9, | |||
oct = 10, october = 10, | |||
nov = 11, november = 11, | |||
dec = 12, december = 12 | |||
} | |||
return month_names[text:lower()] | |||
end | end | ||
end | end | ||
-- A table to get the current year/month/day (UTC), but only if needed. | |||
local current = setmetatable({}, { | |||
__index = function (self, key) | |||
local d = os.date('!*t') | |||
self.year = d.year | |||
self.month = d.month | |||
self.day = d.day | |||
self.hour = d.hour | |||
self.minute = d.min | |||
self.second = d.sec | |||
return rawget(self, key) | |||
local | |||
end | end | ||
}) | |||
local function | local function date_component(named, positional, component) | ||
-- Return | -- Return the first of the two arguments (named like {{example|year=2001}} | ||
-- | -- or positional like {{example|2001}}) that is not nil and is not empty. | ||
-- If both are nil, return the current date component, if specified. | |||
-- This translates empty arguments passed to the template to nil, and | |||
-- optionally replaces a nil argument with a value from the current date. | |||
named = strip_to_nil(named) | |||
if named then | |||
-- If | return named | ||
-- | |||
-- | |||
if | |||
end | end | ||
positional = strip_to_nil(positional) | |||
if | if positional then | ||
return | return positional | ||
end | end | ||
if component then | |||
return current[component] | |||
if | |||
end | end | ||
return nil | |||
end | end | ||
-- | local era_text = { | ||
-- options.era = { year<0 , year>0 } | |||
['BCMINUS'] = { MINUS , '' }, | |||
['BCNEGATIVE'] = { '-' , '' }, | |||
['BC'] = { 'BC' , '' }, | |||
['B.C.'] = { 'B.C.' , '' }, | |||
['BCE'] = { 'BCE' , '' }, | |||
['B.C.E.'] = { 'B.C.E.', '' }, | |||
['AD'] = { 'BC' , 'AD' }, | |||
['A.D.'] = { 'B.C.' , 'A.D.' }, | |||
['CE'] = { 'BCE' , 'CE' }, | |||
['C.E.'] = { 'B.C.E.', 'C.E.' }, | |||
} | |||
local function extract_date( | local function extract_date(text) | ||
-- Parse the date/time in text and return n, o where | -- Parse the date/time in text and return n, o where | ||
-- n = table of numbers with date/time fields | -- n = table of numbers with date/time fields | ||
-- o = table of options for AM/PM or AD/BC | -- o = table of options for AM/PM or AD/BC, if any | ||
-- or return nothing if date is known to be invalid. | -- or return nothing if date is known to be invalid. | ||
-- Caller determines if the values in n are valid. | -- Caller determines if the values in n are valid. | ||
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as ambiguous and undesirable. | |||
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as | |||
local date, options = {}, {} | local date, options = {}, {} | ||
local function extract_ymd(item) | local function extract_ymd(item) | ||
local ystr, mstr, dstr = item:match('^(%d%d%d%d)-(%w+)-(%d%d?)$') | |||
local | if ystr then | ||
if | local m | ||
if mstr:match('^%d%d?$') then | |||
m = tonumber(mstr) | |||
if | |||
m = tonumber( | |||
else | else | ||
m = month_number( | m = month_number(mstr) | ||
end | end | ||
if m then | if m then | ||
date. | date.y = tonumber(ystr) | ||
date. | date.m = m | ||
date. | date.d = tonumber(dstr) | ||
return true | return true | ||
end | end | ||
Line 797: | Line 479: | ||
end | end | ||
local function extract_month(item) | local function extract_month(item) | ||
-- A month must be given as a name or abbreviation; a number | -- A month must be given as a name or abbreviation; a number would be ambiguous. | ||
local m = month_number(item) | local m = month_number(item) | ||
if m then | if m then | ||
date. | date.m = m | ||
return true | return true | ||
end | end | ||
Line 806: | Line 488: | ||
local function extract_time(item) | local function extract_time(item) | ||
local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$') | local h, m, s = item:match('^(%d%d?):(%d%d)(:?%d*)$') | ||
if date. | if date.H or not h then | ||
return | return | ||
end | end | ||
Line 815: | Line 497: | ||
end | end | ||
end | end | ||
date. | date.H = tonumber(h) | ||
date. | date.M = tonumber(m) | ||
date. | date.S = tonumber(s) -- nil if empty string | ||
return true | return true | ||
end | end | ||
local ampm_options = { | |||
['am'] = 'am', | |||
['AM'] = 'AM', | |||
['a.m.'] = 'a.m.', | |||
['A.M.'] = 'A.M.', | |||
['pm'] = 'am', -- same as am | |||
['PM'] = 'AM', | |||
['p.m.'] = 'a.m.', | |||
['P.M.'] = 'A.M.', | |||
} | |||
local item_count = 0 | local item_count = 0 | ||
local index_time | local index_time | ||
local function set_ampm(item) | local function set_ampm(item) | ||
local H = date. | local H = date.H | ||
if H and not options.am and index_time + 1 == item_count then | if H and not options.am and index_time + 1 == item_count then | ||
options.am = ampm_options[item] | options.am = ampm_options[item] | ||
if item:match('^[Aa]') then | if item:match('^[Aa]') then | ||
if not (1 <= H and H <= 12) then | if not (1 <= H and H <= 12) then | ||
Line 831: | Line 523: | ||
end | end | ||
if H == 12 then | if H == 12 then | ||
date. | date.H = 0 | ||
end | end | ||
else | else | ||
Line 838: | Line 530: | ||
end | end | ||
if H <= 11 then | if H <= 11 then | ||
date. | date.H = H + 12 | ||
end | end | ||
end | end | ||
Line 844: | Line 536: | ||
end | end | ||
end | end | ||
for item in text:gsub(', | for item in text:gsub(',', ' '):gmatch('%S+') do | ||
-- Accept options in peculiar places; if duplicated, last wins. | |||
item_count = item_count + 1 | item_count = item_count + 1 | ||
if era_text[item] then | if era_text[item] then | ||
options.era = item | options.era = item | ||
elseif ampm_options[item] then | elseif ampm_options[item] then | ||
Line 861: | Line 550: | ||
end | end | ||
index_time = item_count | index_time = item_count | ||
elseif date. | elseif date.d and date.m then | ||
if date. | if date.y then | ||
return -- should be nothing more so item is invalid | return -- should be nothing more so item is invalid | ||
end | end | ||
Line 868: | Line 557: | ||
return | return | ||
end | end | ||
date. | date.y = tonumber(item) | ||
elseif date. | elseif date.d then | ||
if not extract_month(item) then | if not extract_month(item) then | ||
return | return | ||
end | end | ||
elseif date. | elseif date.m then | ||
if not | if not item:match('^(%d%d?)$') then | ||
return | return | ||
end | end | ||
date.d = tonumber(item) | |||
elseif not extract_ymd(item) then | |||
elseif extract_ymd(item) then | if item:match('^(%d%d?)$') then | ||
date.d = tonumber(item) | |||
elseif not extract_month(item) then | |||
return | |||
end | end | ||
end | end | ||
end | end | ||
return date, options | return date, options | ||
end | end | ||
-- Metatable for some operations on dates. | |||
-- For Lua 5.1, __lt does not work if the metatable is an anonymous table. | |||
local Date -- forward declaration | |||
local datemt = { | |||
__eq = function (lhs, rhs) | |||
-- | -- Return true if dates identify same date/time where, for example, | ||
-- (-4712, 1, 1, 'Julian') == (-4713, 11, 24, 'Gregorian'). | |||
return lhs.isvalid and rhs.isvalid and lhs.jdz == rhs.jdz | |||
-- Return | end, | ||
__lt = function (lhs, rhs) | |||
-- Return true if lhs < rhs. | |||
if not | if not lhs.isvalid then | ||
return true | |||
end | end | ||
if not | if not rhs.isvalid then | ||
return false | |||
end | end | ||
return lhs.jdz < rhs.jdz | |||
end, | |||
end | |||
__index = function (self, key) | __index = function (self, key) | ||
local value | local value | ||
if key == 'dayabbr' then | if key == 'dayabbr' then | ||
Line 1,058: | Line 604: | ||
value = day_info[self.dow][2] | value = day_info[self.dow][2] | ||
elseif key == 'dow' then | elseif key == 'dow' then | ||
value = (self. | value = (self.jd + 1) % 7 -- day-of-week 0=Sun to 6=Sat | ||
elseif key == 'dowiso' then | elseif key == 'dowiso' then | ||
value = (self. | value = (self.jd % 7) + 1 -- ISO day-of-week 1=Mon to 7=Sun | ||
elseif key == ' | elseif key == 'doy' then | ||
local first = Date(self.year, 1, 1, self.calname).jd | |||
value = self.jd - first + 1 -- day-of-year 1 to 366 | |||
local first = Date(self.year, 1, 1, self. | |||
value = self. | |||
elseif key == 'era' then | elseif key == 'era' then | ||
-- Era text | -- Era text from year and options. | ||
local eraopt = self.options.era | |||
local sign | |||
if self.year == 0 then | |||
sign = (eraopt == 'BCMINUS' or eraopt == 'BCNEGATIVE') and 2 or 1 | |||
else | |||
sign = self.year > 0 and 2 or 1 | |||
end | |||
value = era_text[eraopt][sign] | |||
elseif key == 'gsd' then | elseif key == 'gsd' then | ||
-- GSD = 1 from 00:00:00 to 23:59:59 on 1 January 1 AD Gregorian calendar, | -- GSD = 1 from 00:00:00 to 23:59:59 on 1 January 1 AD Gregorian calendar, | ||
-- which is from jd 1721425.5 to 1721426.49999. | -- which is JDN = 1721426, and is from jd 1721425.5 to 1721426.49999. | ||
value = floor(self.jd - 1721424.5) | value = math.floor(self.jd - 1721424.5) | ||
elseif | elseif key == 'jd' or key == 'jdz' then | ||
local jd, jdz = julian_date(self) | local jd, jdz = julian_date(self) | ||
rawset(self, 'jd', jd) | rawset(self, 'jd', jd) | ||
rawset(self, 'jdz', jdz) | rawset(self, 'jdz', jdz) | ||
return key == ' | return key == 'jd' and jd or jdz | ||
elseif key == ' | elseif key == 'is_leap_year' then | ||
value = is_leap_year(self.year, self.calname) | |||
value = is_leap_year(self.year, self. | |||
elseif key == 'monthabbr' then | elseif key == 'monthabbr' then | ||
value = month_info[self.month][1] | value = month_info[self.month][1] | ||
elseif key == 'monthname' then | elseif key == 'monthname' then | ||
value = month_info[self.month][2] | value = month_info[self.month][2] | ||
Line 1,101: | Line 642: | ||
end, | end, | ||
} | } | ||
--[[ Examples of syntax to construct a date: | --[[ Examples of syntax to construct a date: | ||
Line 1,164: | Line 649: | ||
Date('currentdate') | Date('currentdate') | ||
Date('currentdatetime') | Date('currentdatetime') | ||
LATER: Following are not yet implemented: | |||
Date('currentdate', H, M, S) current date with given time | |||
Date('1 April 1995', 'julian') parse date from text | Date('1 April 1995', 'julian') parse date from text | ||
Date('1 April 1995 AD', 'julian') using | Date('1 April 1995 AD', 'julian') AD, CE, BC, BCE (using one of these sets a flag to do same for output) | ||
Date('04:30:59 1 April 1995', 'julian') | Date('04:30:59 1 April 1995', 'julian') | ||
]] | ]] | ||
function Date(...) -- for forward declaration above | function Date(...) -- for forward declaration above | ||
-- Return a table | -- Return a table to hold a date assuming a uniform calendar always applies (proleptic). | ||
-- If invalid, return an empty table which is regarded as invalid. | |||
-- return | |||
local calendars = { julian = 'Julian', gregorian = 'Gregorian' } | local calendars = { julian = 'Julian', gregorian = 'Gregorian' } | ||
local | local result = { | ||
isvalid = false, -- false avoids __index lookup | |||
calname = 'Gregorian', -- default is Gregorian calendar | |||
hastime = false, -- true if input sets a time | hastime = false, -- true if input sets a time | ||
hour = 0, -- always set hour/minute/second so don't have to handle nil | hour = 0, -- always set hour/minute/second so don't have to handle nil | ||
minute = 0, | minute = 0, | ||
second = 0, | second = 0, | ||
month_days = function (self, month) | |||
return days_in_month(self.year, month, self.calname) | |||
return | |||
end, | end, | ||
text = | -- Valid option settings are: | ||
-- am: 'am', 'a.m.', 'AM', 'A.M.' | |||
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.' | |||
options = { am = 'am', era = 'BC' }, | |||
text = date_text, | |||
} | } | ||
local argtype, datetext | local argtype, datetext | ||
local numbers = collection() | |||
local numbers = | |||
for _, v in ipairs({...}) do | for _, v in ipairs({...}) do | ||
v = strip_to_nil(v) | v = strip_to_nil(v) | ||
Line 1,203: | Line 683: | ||
-- Ignore empty arguments after stripping so modules can directly pass template parameters. | -- Ignore empty arguments after stripping so modules can directly pass template parameters. | ||
elseif calendars[vlower] then | elseif calendars[vlower] then | ||
result.calname = calendars[vlower] | |||
else | else | ||
local num = tonumber(v) | local num = tonumber(v) | ||
if not num and argtype == 'setdate' and | if not num and argtype == 'setdate' and numbers.n == 1 then | ||
num = month_number(v) | num = month_number(v) | ||
end | end | ||
Line 1,247: | Line 693: | ||
argtype = 'setdate' | argtype = 'setdate' | ||
end | end | ||
numbers:add(num) | |||
if argtype == 'juliandate' then | |||
if type(v) == 'string' then | if type(v) == 'string' then | ||
if v:find('.', 1, true) then | if v:find('.', 1, true) then | ||
result.hastime = true | |||
end | end | ||
elseif num ~= floor(num) then | elseif num ~= math.floor(num) then | ||
-- The given value was a number. The time will be used | -- The given value was a number. The time will be used | ||
-- if the fractional part is nonzero. | -- if the fractional part is nonzero. | ||
result.hastime = true | |||
end | end | ||
end | end | ||
elseif argtype then | elseif argtype then | ||
return | return {} | ||
elseif type(v) == 'string' then | elseif type(v) == 'string' then | ||
if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then | if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then | ||
Line 1,274: | Line 715: | ||
end | end | ||
else | else | ||
return | return {} | ||
end | end | ||
end | end | ||
end | end | ||
if argtype == 'datetext' then | if argtype == 'datetext' then | ||
if | if not (numbers.n == 0 and | ||
return | set_date_from_numbers(result, | ||
extract_date(datetext))) then | |||
return {} | |||
end | end | ||
elseif argtype == 'juliandate' then | elseif argtype == 'juliandate' then | ||
if numbers.n == 1 then | |||
result.jd = numbers[1] | |||
set_date_from_jd(result) | |||
return | else | ||
return {} | |||
end | end | ||
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then | elseif argtype == 'currentdate' or argtype == 'currentdatetime' then | ||
result.year = current.year | |||
result.month = current.month | |||
result.day = current.day | |||
if argtype == 'currentdatetime' then | if argtype == 'currentdatetime' then | ||
result.hour = current.hour | |||
result.minute = current.minute | |||
result.second = current.second | |||
result.hastime = true | |||
end | end | ||
result.calname = 'Gregorian' -- ignore any given calendar name | |||
result.isvalid = true | |||
elseif argtype == 'setdate' then | elseif argtype == 'setdate' then | ||
if | if not set_date_from_numbers(result, numbers) then | ||
return | return {} | ||
end | end | ||
else | else | ||
return {} | |||
end | end | ||
setmetatable( | return setmetatable(result, datemt) | ||
end | end | ||
local function | local function DateDiff(date1, date2) | ||
-- Return a | -- Return a table to with the difference between the two given dates. | ||
-- Difference is negative if the second date is older than the first. | |||
-- | -- TODO Replace with something using Julian dates? | ||
-- | -- Who checks for isvalid()? | ||
-- | -- Handle calname == 'Julian' | ||
local calname = 'Gregorian' -- TODO fix | |||
local isnegative | |||
if date2 < date1 then | |||
isnegative = true | |||
date1, date2 = date2, date1 | |||
end | end | ||
-- It is known that date1 <= date2. | |||
local y1, m1 = date1.year, date1.month | |||
local y2, m2 = date2.year, date2.month | |||
local years, months, days = y2 - y1, m2 - m1, date2.day - date1.day | |||
if days < 0 then | |||
days = days + days_in_month(y1, m1, calname) | |||
months = months - 1 | |||
end | end | ||
if | if months < 0 then | ||
months = months + 12 | |||
years = years - 1 | |||
end | end | ||
return { | |||
years = years, | |||
months = months, | |||
days = days, | |||
isnegative = isnegative, | |||
age_ym = function (self) | |||
-- Return text specifying difference in years, months. | |||
local sign = self.isnegative and MINUS or '' | |||
local mtext = number_name(self.months, 'month') | |||
local result | |||
if self.years > 0 then | |||
local ytext = number_name(self.years, 'year') | |||
if self.months == 0 then | |||
result = ytext | |||
else | |||
result = ytext .. ', ' .. mtext | |||
if | |||
if | |||
end | end | ||
else | else | ||
if self.months == 0 then | |||
sign = '' | |||
end | end | ||
result = mtext | |||
end | end | ||
end | return sign .. result | ||
end, | |||
} | |||
end | |||
local function message(msg, nocat) | |||
-- Return formatted message text for an error. | |||
-- Can append "#FormattingError" to URL of a page with a problem to find it. | |||
local anchor = '<span id="FormattingError" />' | |||
local category | |||
if not nocat and mw.title.getCurrentTitle():inNamespaces(0, 10) then | |||
-- Category only in namespaces: 0=article, 10=template. | |||
category = '[[Category:Age error]]' | |||
else | |||
category = '' | |||
end | |||
return anchor .. | |||
'<strong class="error">Error: ' .. | |||
msg .. | |||
'</strong>' .. | |||
category .. '\n' | |||
end | |||
local function age_days(frame) | |||
-- Return age in days between two given dates, or | |||
-- between given date and current date. | |||
-- This code implements the logic in [[Template:Age in days]]. | |||
-- Like {{Age in days}}, a missing argument is replaced from the current | |||
-- date, so can get a bizarre mixture of specified/current y/m/d. | |||
local args = frame:getParent().args | |||
local date1 = Date( | |||
date_component(args.year1 , args[1], 'year' ), | |||
date_component(args.month1, args[2], 'month'), | |||
date_component(args.day1 , args[3], 'day' ) | |||
) | |||
local date2 = Date( | |||
date_component(args.year2 , args[4], 'year' ), | |||
date_component(args.month2, args[5], 'month'), | |||
date_component(args.day2 , args[6], 'day' ) | |||
) | |||
if not (date1.isvalid and date2.isvalid) then | |||
return message('Need valid year, month, day') | |||
end | |||
local sign = '' | |||
local result = date2.jd - date1.jd | |||
if result < 0 then | |||
sign = MINUS | |||
result = -result | |||
end | end | ||
return sign .. tostring(result) | |||
end | |||
local function age_ym(frame) | |||
-- Return age in years and months between two given dates, or | |||
-- between given date and current date. | |||
local args = frame:getParent().args | |||
local fields = {} | |||
for i = 1, 6 do | |||
fields[i] = strip_to_nil(args[i]) | |||
end | end | ||
local | local date1, date2 | ||
if | if fields[1] and fields[2] and fields[3] then | ||
date1 = Date(fields[1], fields[2], fields[3]) | |||
end | end | ||
if | if not (date1 and date1.isvalid) then | ||
return message('Need valid year, month, day') | |||
return | |||
end | end | ||
if | if fields[4] and fields[5] and fields[6] then | ||
date2 = Date(fields[4], fields[5], fields[6]) | |||
if not date2.isvalid then | |||
return message('Second date should be year, month, day') | |||
end | end | ||
else | |||
date2 = Date('currentdate') | |||
end | end | ||
return | return DateDiff(date1, date2):age_ym() | ||
end | end | ||
local function | local function gsd_ymd(frame) | ||
-- Return Gregorian serial date of the given date, or the current date. | |||
-- Like {{Gregorian serial date}}, a missing argument is replaced from the | |||
-- current date, so can get a bizarre mixture of specified/current y/m/d. | |||
-- This also accepts positional arguments, although the original template does not. | |||
-- The returned value is negative for dates before 1 January 1 AD despite | |||
-- the fact that GSD is not defined for earlier dates. | |||
local args = frame:getParent().args | |||
local date = Date( | |||
date_component(args.year , args[1], 'year' ), | |||
date_component(args.month, args[2], 'month'), | |||
date_component(args.day , args[3], 'day' ) | |||
) | |||
if date.isvalid then | |||
return tostring(date.gsd) | |||
end | end | ||
return message('Need valid year, month, day') | |||
return | |||
end | end | ||
-- | local function ymd_from_jd(frame) | ||
-- Return formatted date from a Julian date. | |||
-- The result is y-m-d or y-m-d H:M:S if input includes a fraction. | |||
-- The word 'Julian' is accepted for the Julian calendar. | |||
local args = frame:getParent().args | |||
local date = Date('juliandate', args[1], args[2]) | |||
if date.isvalid then | |||
return date:text() | |||
end | |||
return message('Need valid Julian date number') | |||
end | |||
function | local function ymd_to_jd(frame) | ||
-- Return a | -- Return Julian date (a number) from a date (y-m-d), or datetime (y-m-d H:M:S), | ||
-- or the current date ('currentdate') or current datetime ('currentdatetime'). | |||
-- The word 'Julian' is accepted for the Julian calendar. | |||
local args = frame:getParent().args | |||
local date = Date(args[1], args[2], args[3], args[4], args[5], args[6], args[7]) | |||
if date.isvalid then | |||
-- | return tostring(date.jd) | ||
-- The | |||
local | |||
end | end | ||
return message('Need valid year/month/day or "currentdate"') | |||
return | |||
end | end | ||
return { | return { | ||
age_days = age_days, | |||
age_ym = age_ym, | |||
_Date = Date, | _Date = Date, | ||
days_in_month = days_in_month, | |||
gsd = gsd_ymd, | |||
JULIANDAY = ymd_to_jd, | |||
ymd_from_jd = ymd_from_jd, | |||
ymd_to_jd = ymd_to_jd, | |||
} | } |