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 3: | Line 3: | ||
local MINUS = '−' -- Unicode U+2212 MINUS SIGN | local MINUS = '−' -- Unicode U+2212 MINUS SIGN | ||
local Date, DateDiff, | local Date, DateDiff, datemt -- forward declarations | ||
local function is_date(t) | local function is_date(t) | ||
return type(t) == 'table' and getmetatable(t) == datemt | |||
return type(t) == 'table' and getmetatable(t) == | |||
end | end | ||
Line 30: | Line 18: | ||
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 trimmed content, or nil | -- If text is a string, return its trimmed content, or nil. | ||
-- Otherwise return text (convenient when Date fields are provided from | -- Otherwise return text (convenient when Date fields are provided from | ||
-- another module which | -- another module which is able to pass, for example, a number). | ||
if type(text) == 'string' then | if type(text) == 'string' then | ||
text = text:match('(%S.-)%s*$') | text = text:match('(%S.-)%s*$') | ||
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 58: | Line 56: | ||
end | end | ||
return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month] | return ({ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 })[month] | ||
end | end | ||
Line 83: | Line 67: | ||
-- 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 | ||
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 94: | Line 79: | ||
local jd = date.day + floor((153*m + 2)/5) + 365*y + offset | local jd = date.day + floor((153*m + 2)/5) + 365*y + offset | ||
if date.hastime then | if date.hastime then | ||
jd = jd + | jd = jd + (date.hour + (date.minute + date.second / 60) /60) / 24 - 0.5 | ||
return jd, jd | return jd, jd | ||
end | end | ||
Line 106: | Line 91: | ||
-- 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 ( | |||
return | return | ||
end | end | ||
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 150: | Line 142: | ||
date.year = 100*b + d - 4800 + floor(m/10) | date.year = 100*b + d - 4800 + floor(m/10) | ||
return true | return true | ||
end | end | ||
Line 201: | Line 150: | ||
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 | ||
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 276: | Line 192: | ||
-- If options1 is a string, return a table with its settings, or | -- If options1 is a string, return a table with its settings, or | ||
-- if it is a table, use its settings. | -- if it is a table, use its settings. | ||
-- Missing options are set from | -- Missing options are set from options2 or defaults. | ||
-- Valid option settings are: | -- Valid option settings are: | ||
-- am: 'am', 'a.m.', 'AM', 'A.M.' | -- am: 'am', 'a.m.', 'AM', 'A.M.' | ||
-- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.' | -- era: 'BCMINUS', 'BCNEGATIVE', 'BC', 'B.C.', 'BCE', 'B.C.E.', 'AD', 'A.D.', 'CE', 'C.E.' | ||
-- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour | -- Option am = 'am' does not mean the hour is AM; it means 'am' or 'pm' is used, depending on the hour. | ||
-- Similarly, era = 'BC' means 'BC' is used if year < 0. | |||
-- Similarly, era = 'BC' means 'BC' is used if year < | |||
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}. | -- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}. | ||
-- BCNEGATIVE is similar but displays a hyphen. | -- BCNEGATIVE is similar but displays a hyphen. | ||
local result = { | local result = {} | ||
if type(options1) == 'table' then | if type(options1) == 'table' then | ||
result | result = options1 | ||
elseif type(options1) == 'string' then | elseif type(options1) == 'string' then | ||
-- Example: 'am:AM era:BC' | -- Example: 'am:AM era:BC' | ||
for item in options1:gmatch('%S+') do | for item in options1:gmatch('%S+') do | ||
local lhs, rhs = item:match('^(%w+) | local lhs, rhs = item:match('^(%w+):(.+)$') | ||
if lhs then | if lhs then | ||
result[lhs] = rhs | result[lhs] = rhs | ||
Line 303: | Line 215: | ||
local defaults = { am = 'am', era = 'BC' } | local defaults = { am = 'am', era = 'BC' } | ||
for k, v in pairs(defaults) do | for k, v in pairs(defaults) do | ||
result[k] = result[k] or options2[k] or v | |||
end | end | ||
return result | return result | ||
end | end | ||
local era_text = { | local era_text = { | ||
Line 345: | Line 237: | ||
local function get_era_for_year(era, year) | local function get_era_for_year(era, year) | ||
return (era_text[era | return (era_text[era or 'BC'] or {})[year > 0 and 2 or 1] or '' | ||
end | end | ||
Line 373: | Line 265: | ||
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' }, | ||
Line 388: | Line 280: | ||
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 290: | ||
['AM'] = { 'AM', 'PM' }, | ['AM'] = { 'AM', 'PM' }, | ||
['A.M.'] = { 'A.M.', 'P.M.' }, | ['A.M.'] = { 'A.M.', 'P.M.' }, | ||
}) | })[amopt] or { 'am', 'pm' } | ||
return (spaces == '' and '' or ' ') .. (value < 12 and ap[1] or ap[2]) | return (spaces == '' and '' or ' ') .. (value < 12 and ap[1] or ap[2]) | ||
end | end | ||
Line 443: | Line 332: | ||
return spaces .. (result and '1' or '0') | return spaces .. (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 | end | ||
Line 450: | Line 339: | ||
:gsub('%%%%', PERCENT) | :gsub('%%%%', PERCENT) | ||
:gsub('(%s*)%%{(%w+)}', replace_property) | :gsub('(%s*)%%{(%w+)}', replace_property) | ||
:gsub('(%s*)%%( | :gsub('(%s*)%%(-?)(%a)', replace_code) | ||
:gsub(PERCENT, '%%') | :gsub(PERCENT, '%%') | ||
) | ) | ||
Line 456: | Line 345: | ||
local function _date_text(date, fmt, options) | local function _date_text(date, fmt, options) | ||
-- Return | -- Return formatted string from given date. | ||
if not is_date(date) then | if not is_date(date) then | ||
return 'Need a date (use "date:text()" with a colon).' | |||
end | end | ||
if type(fmt) | if type(fmt) ~= 'string' then | ||
fmt = '%-d %B %-Y %{era}' | |||
if date.hastime then | if date.hastime then | ||
if date.second > 0 then | |||
fmt = '%H:%M:%S ' .. fmt | |||
else | |||
fmt = '%H:%M ' .. fmt | |||
end | |||
end | end | ||
return strftime(date, fmt, options) | |||
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-%m-%d %{era}' | f = '%Y-%m-%d %{era}' | ||
Line 511: | Line 377: | ||
f = '%-d %B %-Y %{era}' | 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 411: | ||
} | } | ||
local function | local function month_number(text) | ||
if type(text) == 'string' then | if type(text) == 'string' then | ||
local month_names = { | |||
jan = 1, january = 1, | |||
feb = 2, february = 2, | |||
mar = 3, march = 3, | |||
local | 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 date/time (UTC), but only if needed. | -- A table to get the current date/time (UTC), but only if needed. | ||
local current = setmetatable({}, { | 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) | |||
end }) | end | ||
}) | |||
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. | ||
Line 718: | Line 455: | ||
-- ('0001' to '9999'). The only way to enter year <= 0 is by specifying | -- ('0001' to '9999'). The only way to enter year <= 0 is by specifying | ||
-- the date as three numeric parameters like ymd Date(-1, 1, 1). | -- the date as three numeric parameters like ymd Date(-1, 1, 1). | ||
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as | -- Dates of form d/m/y, m/d/y, y/m/d are rejected as ambiguous. | ||
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 475: | ||
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 484: | ||
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 493: | ||
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 519: | ||
end | end | ||
if H == 12 then | if H == 12 then | ||
date. | date.H = 0 | ||
end | end | ||
else | else | ||
Line 838: | Line 526: | ||
end | end | ||
if H <= 11 then | if H <= 11 then | ||
date. | date.H = H + 12 | ||
end | end | ||
end | end | ||
Line 844: | Line 532: | ||
end | end | ||
end | end | ||
for item in text:gsub(', | for item in text:gsub(',', ' '):gmatch('%S+') do | ||
item_count = item_count + 1 | item_count = item_count + 1 | ||
if era_text[item] then | if era_text[item] then | ||
Line 861: | Line 549: | ||
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 556: | ||
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 | ||
if not date. | if not date.y or date.y == 0 then | ||
return | return | ||
end | end | ||
local era = era_text[options.era] | local era = era_text[options.era] | ||
if era and era.isbc then | if era and era.isbc then | ||
date. | date.y = 1 - date.y | ||
end | end | ||
return date, options | return date, options | ||
end | end | ||
Line 930: | Line 587: | ||
-- Return a new date from calculating (lhs + rhs) or (lhs - rhs), | -- Return a new date from calculating (lhs + rhs) or (lhs - rhs), | ||
-- or return nothing if invalid. | -- or return nothing if invalid. | ||
-- Caller ensures that lhs is a date; its properties are copied for the new date. | -- Caller ensures that lhs is a date; its properties are copied for the new date. | ||
local function is_prefix(text, word, minlen) | local function is_prefix(text, word, minlen) | ||
local n = #text | local n = #text | ||
Line 942: | Line 593: | ||
end | end | ||
local function do_days(n) | local function do_days(n) | ||
if is_sub then | |||
if | n = -n | ||
end | end | ||
return Date(lhs, 'juliandate', lhs.jd + n) | |||
return Date(lhs, 'juliandate', jd) | |||
end | end | ||
if type(rhs) == 'number' then | if type(rhs) == 'number' then | ||
-- Add | -- Add days, including fractional days. | ||
return do_days(rhs) | return do_days(rhs) | ||
end | end | ||
if type(rhs) == 'string' then | if type(rhs) == 'string' then | ||
-- rhs is a single component like '26m' or '26 months' ( | -- rhs is a single component like '26m' or '26 months' (unsigned integer only). | ||
local num, id = rhs:match('^%s*(%d+)%s*(%a+)$') | |||
local | if num then | ||
if | local y, m | ||
num = tonumber(num) | |||
local y, m | |||
id = id:lower() | id = id:lower() | ||
if is_prefix(id, 'years') then | if is_prefix(id, 'years') then | ||
Line 980: | Line 613: | ||
m = 0 | m = 0 | ||
elseif is_prefix(id, 'months') then | elseif is_prefix(id, 'months') then | ||
y = floor(num / 12) | y = math.floor(num / 12) | ||
m = num % 12 | m = num % 12 | ||
elseif is_prefix(id, 'weeks') then | elseif is_prefix(id, 'weeks') then | ||
return do_days(num * 7) | |||
elseif is_prefix(id, 'days') then | elseif is_prefix(id, 'days') then | ||
return do_days(num) | |||
elseif is_prefix(id, 'hours') then | elseif is_prefix(id, 'hours') then | ||
return do_days(num / 24) | |||
elseif is_prefix(id, 'minutes', 3) then | elseif is_prefix(id, 'minutes', 3) then | ||
return do_days(num / (24 * 60)) | |||
elseif is_prefix(id, 'seconds') then | elseif is_prefix(id, 'seconds') then | ||
return do_days(num / (24 * 3600)) | |||
else | else | ||
return | return | ||
end | end | ||
Line 1,015: | Line 642: | ||
m = m + 12 | m = m + 12 | ||
end | end | ||
local d = math.min(lhs.day, days_in_month(y, m, lhs. | local d = math.min(lhs.day, days_in_month(y, m, lhs.calname)) | ||
return Date(lhs, y, m, d) | return Date(lhs, y, m, d) | ||
end | end | ||
end | end | ||
end | end | ||
-- Metatable for some operations on dates. | |||
datemt = { -- for forward declaration above | |||
__add = function (lhs, rhs) | |||
if not is_date(lhs) then | |||
lhs, rhs = rhs, lhs -- put date on left (it must be a date for this to have been called) | |||
end | |||
return date_add_sub(lhs, rhs) | |||
end, | |||
__sub = function (lhs, rhs) | |||
if is_date(lhs) then | |||
if is_date(rhs) then | |||
return DateDiff(lhs, rhs) | |||
if | |||
if | |||
end | end | ||
return date_add_sub(lhs, rhs, true) | |||
end | end | ||
end, | |||
__concat = function (lhs, rhs) | |||
return tostring(lhs) .. tostring(rhs) | |||
end, | |||
__tostring = function (self) | |||
return self:text() | |||
end, | |||
__eq = function (lhs, rhs) | |||
-- Return true if dates identify same date/time where, for example, | |||
-- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true. | |||
-- This is only called if lhs and rhs have the same metatable. | |||
return lhs.jdz == rhs.jdz | |||
end, | |||
__lt = function (lhs, rhs) | |||
-- Return true if lhs < rhs, for example, | |||
-- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true. | |||
-- This is only called if lhs and rhs have the same metatable. | |||
return lhs.jdz < rhs.jdz | |||
end, | |||
__index = function (self, key) | |||
local value | local value | ||
if key == 'dayabbr' then | if key == 'dayabbr' then | ||
Line 1,058: | Line 689: | ||
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 == 'dayofweek' then | elseif key == 'dayofweek' then | ||
value = self.dow | value = self.dow | ||
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 == 'dayofweekiso' then | elseif key == 'dayofweekiso' then | ||
value = self.dowiso | value = self.dowiso | ||
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 | |||
elseif key == 'dayofyear' then | elseif key == 'dayofyear' then | ||
value = self.doy | |||
value = self. | |||
elseif key == 'era' then | elseif key == 'era' then | ||
-- Era text (never a negative sign) from year and options. | -- Era text (never a negative sign) from year and options. | ||
value = get_era_for_year(self.options.era, self.year) | value = get_era_for_year(self.options.era, self.year) | ||
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 key == 'juliandate' or key == 'jd' or key == 'jdz' then | elseif key == 'juliandate' or key == 'jd' or key == 'jdz' then | ||
local jd, jdz = julian_date(self) | local jd, jdz = julian_date(self) | ||
Line 1,083: | Line 714: | ||
rawset(self, 'jdz', jdz) | rawset(self, 'jdz', jdz) | ||
return key == 'jdz' and jdz or jd | return key == 'jdz' and jdz or jd | ||
elseif key == 'isleapyear' then | elseif key == 'isleapyear' then | ||
value = is_leap_year(self.year, self. | value = is_leap_year(self.year, self.calname) | ||
elseif key == 'monthabbr' then | elseif key == 'monthabbr' then | ||
value = month_info[self.month][1] | value = month_info[self.month][1] | ||
elseif key == 'monthdays' then | elseif key == 'monthdays' then | ||
value = days_in_month(self.year, self.month, self. | value = days_in_month(self.year, self.month, self.calname) | ||
elseif key == 'monthname' then | elseif key == 'monthname' then | ||
value = month_info[self.month][2] | value = month_info[self.month][2] | ||
Line 1,101: | Line 729: | ||
end, | end, | ||
} | } | ||
--[[ Examples of syntax to construct a date: | --[[ Examples of syntax to construct a date: | ||
Line 1,168: | Line 740: | ||
Date('04:30:59 1 April 1995', 'julian') | Date('04:30:59 1 April 1995', 'julian') | ||
Date(date) copy of an existing date | Date(date) copy of an existing date | ||
Date( | LATER: Following is not yet implemented: | ||
Date('currentdate', H, M, S) current date with given time | |||
]] | ]] | ||
function Date(...) -- for forward declaration above | function Date(...) -- for forward declaration above | ||
Line 1,175: | Line 747: | ||
-- (proleptic Gregorian calendar or proleptic Julian calendar), or | -- (proleptic Gregorian calendar or proleptic Julian calendar), or | ||
-- return nothing if date is invalid. | -- return nothing if date is invalid. | ||
local is_copy | |||
local calendars = { julian = 'Julian', gregorian = 'Gregorian' } | local calendars = { julian = 'Julian', gregorian = 'Gregorian' } | ||
local | local result = { | ||
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, | ||
options = | options = make_option_table(), | ||
text = _date_text, | 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 766: | ||
-- 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] | |||
elseif is_date(v) then | elseif is_date(v) then | ||
-- Copy existing date (items can be overridden by other arguments). | -- Copy existing date (items can be overridden by other arguments). | ||
if is_copy | if is_copy then | ||
return | return | ||
end | end | ||
is_copy = true | is_copy = true | ||
result.calname = v.calname | |||
result.hastime = v.hastime | |||
result.options = v.options | |||
result.year = v.year | |||
result.month = v.month | |||
result.day = v.day | |||
result.hour = v.hour | |||
result.minute = v.minute | |||
result.second = v.second | |||
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 791: | ||
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 | ||
Line 1,279: | Line 818: | ||
end | end | ||
if argtype == 'datetext' then | if argtype == 'datetext' then | ||
if | if not (numbers.n == 0 and | ||
set_date_from_numbers(result, | |||
extract_date(datetext))) then | |||
return | return | ||
end | end | ||
elseif argtype == 'juliandate' then | elseif argtype == 'juliandate' then | ||
result.jd = numbers[1] | |||
if not (numbers.n == 1 and set_date_from_jd(result)) then | |||
if not set_date_from_jd( | |||
return | 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 | |||
elseif argtype == 'setdate' then | elseif argtype == 'setdate' then | ||
if | if not set_date_from_numbers(result, numbers) then | ||
return | return | ||
end | end | ||
elseif not | elseif not is_copy then | ||
return | return | ||
end | end | ||
return setmetatable(result, datemt) | |||
end | end | ||
function DateDiff(date1, date2) -- for forward declaration above | |||
-- Return a table with the difference between the two dates (date1 - date2). | |||
-- The difference is negative if date2 is more recent than date1. | |||
function DateDiff(date1, date2 | |||
-- Return a table with the difference between two dates (date1 - date2). | |||
-- The difference is negative if | |||
-- Return nothing if invalid. | -- Return nothing if invalid. | ||
if not (is_date(date1) and is_date(date2) and date1.calname == date2.calname) then | |||
if not (is_date(date1) and is_date(date2) and date1. | |||
return | return | ||
end | end | ||
local isnegative | |||
local isnegative | |||
if date1 < date2 then | if date1 < date2 then | ||
isnegative = true | isnegative = true | ||
date1, date2 = date2, date1 | date1, date2 = date2, date1 | ||
end | end | ||
-- It is known that date1 >= date2. | |||
local y1, m1 = date1.year, date1.month | local y1, m1 = date1.year, date1.month | ||
local y2, m2 = date2.year, date2.month | local y2, m2 = date2.year, date2.month | ||
local years = y1 - y2 | local years, months, days = y1 - y2, m1 - m2, date1.day - date2.day | ||
if days < 0 then | |||
days = days + days_in_month(y2, m2, date2.calname) | |||
days = | |||
months = months - 1 | months = months - 1 | ||
end | end | ||
if months < 0 then | if months < 0 then | ||
months = months + 12 | |||
years = years - 1 | years = years - 1 | ||
end | end | ||
return { | |||
return | |||
years = years, | years = years, | ||
months = months, | months = months, | ||
days = days, | days = days, | ||
isnegative = isnegative, | 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 | |||
end | |||
else | |||
if self.months == 0 then | |||
sign = '' | |||
end | |||
result = mtext | |||
end | |||
return sign .. result | |||
end, | |||
} | |||
end | end | ||