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 69: | Line 69: | ||
local function hms(date) | local function hms(date) | ||
-- Return fraction of a day | -- Return fraction of a day (0 <= fraction < 1) from date's time. | ||
return (date.hour + (date.minute + date.second / 60) / 60) / 24 | return (date.hour + (date.minute + date.second / 60) / 60) / 24 | ||
end | end | ||
Line 150: | Line 149: | ||
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 163: | ||
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.calendar)) then | 1 <= d and d <= days_in_month(y, m, date.calendar)) then | ||
return | return | ||
end | end | ||
if | if H then | ||
-- It is not possible to set M or S without also setting H. | |||
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', 'format' }) do | for _, k in ipairs({ 'am', 'era', 'format' }) do | ||
Line 388: | Line 311: | ||
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 443: | Line 363: | ||
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 464: | Line 384: | ||
return strftime(date, fmt, options) | return strftime(date, fmt, options) | ||
end | end | ||
else | else | ||
fmt = 'dmy' | fmt = 'dmy' | ||
Line 471: | Line 389: | ||
fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt | fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt | ||
end | end | ||
end | end | ||
local function hm_fmt() | local function hm_fmt() | ||
Line 511: | Line 411: | ||
f = '%-d %B %-Y %{era}' | f = '%-d %B %-Y %{era}' | ||
else | else | ||
error('date:text: invalid format', 2) | |||
end | end | ||
t:add(f) | t:add(f) | ||
Line 577: | Line 477: | ||
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 609: | Line 509: | ||
-- -5 is equivalent to '5 <' | -- -5 is equivalent to '5 <' | ||
-- 5 is equivalent to '5' which is '5 >' | -- 5 is equivalent to '5' which is '5 >' | ||
local list = { text = _list_text } | |||
if not is_date(date) then | if not is_date(date) then | ||
error('date:list: need a date (use "date:list()" with a colon)', 2) | error('date:list: need a date (use "date:list()" with a colon)', 2) | ||
end | end | ||
local count, offset, operation | local count, offset, operation | ||
Line 696: | Line 593: | ||
-- 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 607: | ||
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 | ||
Line 720: | Line 619: | ||
-- Dates of form d/m/y, m/d/y, y/m/d are rejected as potentially ambiguous. | -- Dates of form d/m/y, m/d/y, y/m/d are rejected as potentially 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( | options.format = 'ymd' | ||
date.year = tonumber(ystr) | |||
date.month = m | date.month = m | ||
date.day = tonumber( | date.day = tonumber(dstr) | ||
return true | return true | ||
end | end | ||
Line 825: | Line 666: | ||
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 874: | Line 715: | ||
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) | |||
options.format = 'dmy' | options.format = 'dmy' | ||
elseif extract_month(item) then | |||
options.format = 'mdy' | |||
else | |||
return | |||
end | end | ||
end | end | ||
end | end | ||
Line 897: | Line 738: | ||
end | end | ||
return date, options | return date, options | ||
end | end | ||
Line 932: | Line 745: | ||
-- 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 963: | Line 771: | ||
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 983: | Line 784: | ||
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,027: | Line 822: | ||
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,130: | Line 904: | ||
-- 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 911: | ||
-- 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 932: | ||
-- (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 = { | ||
Line 1,188: | Line 942: | ||
options = {}, | options = {}, | ||
list = _date_list, | list = _date_list, | ||
text = _date_text, | text = _date_text, | ||
} | } | ||
Line 1,204: | Line 955: | ||
elseif calendars[vlower] then | elseif calendars[vlower] then | ||
newdate.calendar = calendars[vlower] | newdate.calendar = 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,215: | Line 962: | ||
is_copy = true | is_copy = true | ||
newdate.calendar = v.calendar | newdate.calendar = v.calendar | ||
newdate.hastime = v.hastime | newdate.hastime = v.hastime | ||
newdate.options = v.options | newdate.options = v.options | ||
Line 1,279: | Line 1,025: | ||
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 1,034: | ||
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,312: | Line 1,056: | ||
return | return | ||
end | end | ||
end | end | ||
setmetatable(newdate, datemt) | setmetatable(newdate, datemt) | ||
Line 1,339: | Line 1,073: | ||
local function _diff_age(diff, code, options) | local function _diff_age(diff, code, options) | ||
-- Return a tuple of | -- Return a tuple of values from diff as specified by code. | ||
-- If options == 'duration', an extra day is added. | |||
-- If | |||
if not is_diff(diff) then | if not is_diff(diff) then | ||
local f = | local f = options == 'duration' and 'duration' or 'age' | ||
error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2) | error(f .. ': need a date difference (use "diff:' .. f .. '()" with a colon)', 2) | ||
end | end | ||
local extra_day = options == 'duration' and 1 or 0 | |||
if code == 'wd' or code == 'w' or code == 'd' then | if code == 'wd' or code == 'w' or code == 'd' then | ||
local | local d = diff.age_days + extra_day | ||
if code == 'd' then | |||
if | return d | ||
end | end | ||
local w = floor(d / 7) | |||
if code == 'w' then | |||
return w | |||
local | |||
if code == ' | |||
return | |||
end | end | ||
return | return w, d % 7 | ||
end | end | ||
if | local y, m, d = diff.years, diff.months, diff.days | ||
local | if extra_day > 0 then | ||
if | d = d + extra_day | ||
if | local to_date = diff.date1 | ||
if d > days_in_month(to_date.year, to_date.month, to_date.calendar) then | |||
d = 1 | |||
m = m + 1 | |||
if m > 12 then | |||
m = 1 | |||
y = y + 1 | |||
end | end | ||
end | end | ||
end | end | ||
if code == 'ymd' then | if code == 'ymd' then | ||
return y, m, d | return y, m, d | ||
end | |||
if code == 'ym' then | |||
return y, m | |||
end | |||
if code == 'm' then | |||
return y | |||
return y * 12 + m | return y * 12 + m | ||
end | end | ||
if code == ' | if code == 'ymwd' then | ||
return y, m, floor(d / 7), d % 7 | |||
return y | |||
end | end | ||
return | return y -- default: assume code == 'y'; ignore invalid codes | ||
end | end | ||
local function _diff_duration(diff, code | local function _diff_duration(diff, code) | ||
return _diff_age(diff, code, 'duration') | |||
return _diff_age(diff, code, | |||
end | end | ||
Line 1,563: | Line 1,134: | ||
local value | local value | ||
if key == 'age_days' then | if key == 'age_days' then | ||
value = self.date1.jdz - self.date2.jdz | |||
end | end | ||
if value ~= nil then | if value ~= nil then | ||
Line 1,579: | Line 1,143: | ||
} | } | ||
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 two dates (date1 - date2). | ||
-- The difference is negative if date1 is older than date2. | -- The difference is negative if date1 is older than date2. | ||
Line 1,594: | Line 1,158: | ||
if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then | if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then | ||
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 (period is from date2 to date1). | -- It is known that date1 >= date2 (period is from date2 to date1). | ||
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 | ||
Line 1,672: | Line 1,179: | ||
local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calendar) or 31 | local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calendar) or 31 | ||
if d2 >= dpm then | if d2 >= dpm then | ||
days = d1 | days = d1 | ||
else | else | ||
days = dpm - d2 + d1 | days = dpm - d2 + d1 | ||
Line 1,686: | Line 1,193: | ||
date1 = date1, | date1 = date1, | ||
date2 = date2, | date2 = date2, | ||
years = years, | years = years, | ||
months = months, | months = months, | ||
Line 1,694: | Line 1,200: | ||
seconds = S, | seconds = S, | ||
isnegative = isnegative, | isnegative = isnegative, | ||
age = _diff_age, | age = _diff_age, | ||
duration = _diff_duration, | duration = _diff_duration, |