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, diffmt -- forward declarations | local Date, DateDiff, diffmt -- forward declarations | ||
Line 35: | Line 34: | ||
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 65: | ||
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 76: | ||
-- 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 88: | ||
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 100: | ||
-- 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 limits -- min/max limits for date ranges −9999-01-01 to 9999-12-31 | ||
if calname == 'Julian' then | |||
elseif calname == ' | limits = { -1931076.5, 5373557.49999 } | ||
elseif calname == 'Gregorian' then | |||
limits = { -1930999.5, 5373484.49999 } | |||
else | else | ||
return | return | ||
end | end | ||
local jd = date.jd | local jd = date.jd | ||
if not (type(jd) == 'number' and | if not (type(jd) == 'number' and limits[1] <= jd and jd <= limits[2]) then | ||
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 151: | ||
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 207: | Line 165: | ||
local M = numbers.minute or date.minute or 0 | local M = numbers.minute or date.minute or 0 | ||
local S = numbers.second or date.second or 0 | local S = numbers.second or date.second or 0 | ||
if not (y and m and d) or not | |||
if y and m and d | (-9999 <= y and y <= 9999 and | ||
1 <= m and m <= 12 and | 1 <= m and m <= 12 and | ||
1 <= d and d <= days_in_month(y, m, date. | 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 = date.hour or 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 200: | ||
-- 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+)[:=](.+)$') | ||
Line 303: | Line 223: | ||
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 245: | ||
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 273: | ||
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 288: | ||
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 298: | ||
['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 340: | ||
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 458: | Line 355: | ||
-- Return a formatted string representing the given date. | -- Return a formatted string representing the 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 385: | ||
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 577: | Line 447: | ||
jul = 7, july = 7, | jul = 7, july = 7, | ||
aug = 8, august = 8, | aug = 8, august = 8, | ||
sep = 9, september | sep = 9, september = 9, | ||
oct = 10, october = 10, | oct = 10, october = 10, | ||
nov = 11, november = 11, | nov = 11, november = 11, | ||
Line 587: | Line 457: | ||
-- Return a list of formatted strings from a list of dates. | -- Return a list of formatted strings from a list of dates. | ||
if not type(list) == 'table' then | if not type(list) == 'table' then | ||
return 'Need "list:text()" with a colon.' | |||
end | end | ||
local result = { join = _list_join } | local result = { join = _list_join } | ||
Line 596: | Line 466: | ||
end | end | ||
local function | local function _make_list(date, spec) | ||
-- Return a possibly empty numbered table of dates meeting the specification. | -- Return a possibly empty numbered table of dates meeting the specification. | ||
-- The spec should be a string like "Tue >=" meaning that the list will | |||
-- The spec should be a string | -- hold dates for all Tuesdays on or after date, and in date's month. | ||
-- | |||
if not is_date(date) then | if not is_date(date) then | ||
return 'Need a date (use "date:list()" with a colon).' | |||
end | end | ||
local | local want_dow, op | ||
local ops = { | local ops = { | ||
['>='] = { before = false, include = true }, | ['>='] = { before = false, include = true }, | ||
Line 624: | Line 481: | ||
} | } | ||
if spec then | if spec then | ||
if type(spec) | if type(spec) ~= 'string' then | ||
return {} | |||
end | |||
for item in spec:gmatch('%S+') do | |||
if ops[item] then | |||
if op then | |||
return {} | |||
end | |||
if | op = ops[item] | ||
return | else | ||
local dow = day_number(item) | |||
if dow then | |||
if want_dow then | |||
-- LATER Could handle more than one day, but probably not needed. | |||
return {} | |||
local dow = day_number( | end | ||
if not dow | want_dow = dow | ||
return | else | ||
return {} | |||
end | end | ||
end | end | ||
end | end | ||
end | end | ||
offset = | local offset = want_dow and want_dow - date.dow or 0 | ||
op = op or ops['>='] | |||
local | local first, last | ||
if | if op.before then | ||
if offset > 0 | if offset >= 0 and not (op.include and offset == 0) then | ||
offset = offset - 7 | offset = offset - 7 | ||
end | end | ||
last = date.day + offset | |||
first = last % 7 | |||
if first == 0 then | |||
first = 7 | |||
end | end | ||
else | else | ||
if offset < 0 or (offset == 0 | if offset < 0 or (not op.include and offset == 0) then | ||
offset = offset + 7 | offset = offset + 7 | ||
end | end | ||
first = date.day + offset | |||
last = date.monthdays | |||
end | end | ||
local list = { text = _list_text } | |||
local count = math.floor((last - first)/7) + 1 | |||
for i = 1, count do | for i = 1, count do | ||
list[i] = Date(date, {day = first}) | |||
list[i] = | first = first + 7 | ||
end | end | ||
return list | return list | ||
Line 696: | Line 533: | ||
-- 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({}, { | -- A local test can set the global variable to produce fixed results. | ||
local current = setmetatable( | |||
set_current_for_test or {}, { | |||
__index = function (self, key) | __index = function (self, key) | ||
local d = os.date('!*t') | local d = os.date('!*t') | ||
Line 708: | Line 547: | ||
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 557: | ||
-- ('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.year = tonumber( | date.year = tonumber(ystr) | ||
date.month = m | date.month = m | ||
date.day = tonumber( | date.day = tonumber(dstr) | ||
return true | return true | ||
end | end | ||
Line 797: | Line 577: | ||
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 | ||
Line 820: | Line 600: | ||
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 | ||
Line 825: | Line 615: | ||
local H = date.hour | local H = date.hour | ||
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 844: | Line 634: | ||
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 874: | Line 664: | ||
end | end | ||
elseif date.month then | elseif date.month then | ||
if not | if not item:match('^(%d%d?)$') then | ||
return | return | ||
end | end | ||
date.day = tonumber(item) | |||
elseif not extract_ymd(item) then | |||
elseif extract_ymd(item) then | if item:match('^(%d%d?)$') then | ||
date.day = tonumber(item) | |||
elseif not extract_month(item) then | |||
return | |||
end | end | ||
end | end | ||
end | end | ||
Line 897: | Line 684: | ||
end | end | ||
return date, options | return date, options | ||
end | end | ||
Line 932: | Line 691: | ||
-- The result is nil if the calculated date exceeds allowable limits. | -- The result is nil if the calculated date exceeds allowable limits. | ||
-- 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 943: | Line 697: | ||
local function do_days(n) | local function do_days(n) | ||
local forcetime, jd | local forcetime, jd | ||
if floor(n) == n then | if math.floor(n) == n then | ||
jd = lhs.jd | jd = lhs.jd | ||
else | else | ||
Line 963: | Line 717: | ||
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 727: | ||
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 756: | ||
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 a date's calculated fields. | -- Metatable for a date's calculated fields. | ||
local datemt = { | local datemt = { | ||
__index = function (self, key) | __index = function (self, key) | ||
local value | local value | ||
if key == 'dayabbr' then | if key == 'dayabbr' then | ||
Line 1,065: | Line 778: | ||
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).jdnoon | |||
value = self.jdnoon - 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 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,085: | Line 798: | ||
elseif key == 'jdnoon' then | elseif key == 'jdnoon' then | ||
-- Julian date at noon (an integer) on the calendar day when jd occurs. | -- Julian date at noon (an integer) on the calendar day when jd occurs. | ||
value = floor(self.jd + 0.5) | value = math.floor(self.jd + 0.5) | ||
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,130: | Line 843: | ||
-- Return true if dates identify same date/time where, for example, | -- Return true if dates identify same date/time where, for example, | ||
-- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true. | -- Date(-4712, 1, 1, 'Julian') == Date(-4713, 11, 24, 'Gregorian') is true. | ||
-- This is called | -- This is only called if lhs and rhs have the same metatable. | ||
return lhs.jdz == rhs.jdz | return lhs.jdz == rhs.jdz | ||
end | end | ||
Line 1,142: | Line 850: | ||
-- Return true if lhs < rhs, for example, | -- Return true if lhs < rhs, for example, | ||
-- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true. | -- Date('1 Jan 2016') < Date('06:00 1 Jan 2016') is true. | ||
-- This is called | -- This is only called if lhs and rhs have the same metatable. | ||
return lhs.jdz < rhs.jdz | return lhs.jdz < rhs.jdz | ||
end | end | ||
Line 1,175: | Line 871: | ||
-- (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 calendars = { julian = 'Julian', gregorian = 'Gregorian' } | local calendars = { julian = 'Julian', gregorian = 'Gregorian' } | ||
local newdate = { | local newdate = { | ||
_id = uniq, | _id = uniq, | ||
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(), | ||
list = | list = _make_list, | ||
text = _date_text, | text = _date_text, | ||
} | } | ||
Line 1,203: | Line 893: | ||
-- 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 | ||
newdate. | newdate.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). | ||
Line 1,214: | Line 900: | ||
end | end | ||
is_copy = true | is_copy = true | ||
newdate. | newdate.calname = v.calname | ||
newdate.hastime = v.hastime | newdate.hastime = v.hastime | ||
newdate.options = v.options | newdate.options = v.options | ||
Line 1,256: | Line 941: | ||
newdate.hastime = true | newdate.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. | ||
Line 1,279: | Line 964: | ||
end | end | ||
if argtype == 'datetext' then | if argtype == 'datetext' then | ||
if tnums or not set_date_from_numbers(newdate, extract_date( | if tnums or not set_date_from_numbers(newdate, extract_date(datetext)) then | ||
return | return | ||
end | end | ||
elseif argtype == 'juliandate' then | elseif argtype == 'juliandate' then | ||
newdate.jd = jd_number | newdate.jd = jd_number | ||
if not set_date_from_jd(newdate) then | if not set_date_from_jd(newdate) then | ||
Line 1,289: | Line 973: | ||
end | end | ||
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then | elseif argtype == 'currentdate' or argtype == 'currentdatetime' then | ||
newdate.year = current.year | newdate.year = current.year | ||
newdate.month = current.month | newdate.month = current.month | ||
Line 1,299: | Line 982: | ||
newdate.hastime = true | newdate.hastime = true | ||
end | end | ||
newdate. | newdate.calname = 'Gregorian' -- ignore any given calendar name | ||
elseif argtype == 'setdate' then | elseif argtype == 'setdate' then | ||
if tnums or not set_date_from_numbers(newdate, numbers) then | if tnums or not set_date_from_numbers(newdate, numbers) then | ||
Line 1,312: | Line 995: | ||
return | return | ||
end | end | ||
end | end | ||
setmetatable(newdate, datemt) | setmetatable(newdate, datemt) | ||
Line 1,327: | Line 1,000: | ||
local mt = { | local mt = { | ||
__index = newdate, | __index = newdate, | ||
__newindex = function(t, k, v) error(' | __newindex = function(t, k, v) error('Date.' .. tostring(k) .. ' is read-only', 2) end, | ||
__add = mt_date_add, | __add = mt_date_add, | ||
__sub = mt_date_sub, | __sub = mt_date_sub, | ||
Line 1,338: | Line 1,011: | ||
end | end | ||
local function | local function _age_ym(diff) | ||
-- Return | -- Return text specifying date difference in years, months. | ||
local sign = diff.isnegative and MINUS or '' | |||
local mtext = number_name(diff.months, 'month') | |||
local result | |||
if diff.years > 0 then | |||
local ytext = number_name(diff.years, 'year') | |||
if diff.months == 0 then | |||
result = ytext | |||
else | |||
result = ytext .. ', ' .. mtext | |||
end | |||
else | else | ||
if diff.months == 0 then | |||
sign = '' | |||
end | end | ||
result = mtext | |||
end | end | ||
return sign .. result | |||
end | end | ||
Line 1,558: | Line 1,038: | ||
end, | end, | ||
__tostring = function (self) | __tostring = function (self) | ||
return tostring(self. | return tostring(self.daystotal) | ||
end, | end, | ||
__index = function (self, key) | __index = function (self, key) | ||
local value | local value | ||
if key == ' | if key == 'age_ym' then | ||
value = _age_ym(self) | |||
elseif key == 'daystotal' then | |||
value = self.date1.jdz - self.date2.jdz | |||
end | end | ||
if value ~= nil then | if value ~= nil then | ||
Line 1,579: | Line 1,054: | ||
} | } | ||
function DateDiff(date1, date2 | function DateDiff(date1, date2) -- for forward declaration above | ||
-- Return a table with the difference between two dates (date1 - date2). | -- Return a table with the difference between the two dates (date1 - date2). | ||
-- The difference is negative if date1 is older than date2. | -- The difference is negative if date1 is older than date2. | ||
-- 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 = false | local isnegative = false | ||
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 setmetatable({ | return setmetatable({ | ||
date1 = date1, | date1 = date1, | ||
date2 = date2, | date2 = date2, | ||
years = years, | years = years, | ||
months = months, | months = months, | ||
days = days, | days = days, | ||
isnegative = isnegative, | isnegative = isnegative, | ||
}, diffmt) | }, diffmt) | ||
end | end |