Module:Date: Difference between revisions
update from Module:Date/sandbox; major refactor to support new features in Module:Age
(fix bug where Date('2016-06-01') - Date('2016-05-31 23:00') gave 1 day instead of 1 hour) |
(update from Module:Date/sandbox; major refactor to support new features in Module:Age) |
||
Line 69: | Line 69: | ||
local function hms(date) | local function hms(date) | ||
-- Return fraction of a day (0 <= fraction < 1) | -- Return fraction of a day from date's time, where (0 <= fraction < 1) | ||
-- if the values are valid, but could be anything if outside range. | |||
return (date.hour + (date.minute + date.second / 60) / 60) / 24 | return (date.hour + (date.minute + date.second / 60) / 60) / 24 | ||
end | end | ||
Line 149: | Line 150: | ||
date.year = 100*b + d - 4800 + floor(m/10) | date.year = 100*b + d - 4800 + floor(m/10) | ||
return true | return true | ||
end | |||
local function fix_numbers(numbers, y, m, d, H, M, S, partial, hastime, calendar) | |||
-- Put the result of normalizing the given values in table numbers. | |||
-- The result will have valid m, d values if y is valid; caller checks y. | |||
-- The logic of PHP mktime is followed where m or d can be zero to mean | |||
-- the previous unit, and -1 is the one before that, etc. | |||
-- Positive values carry forward. | |||
local date | |||
if not (1 <= m and m <= 12) then | |||
date = Date(y, 1, 1) | |||
if not date then return end | |||
date = date + ((m - 1) .. 'm') | |||
y, m = date.year, date.month | |||
end | |||
local days_hms | |||
if not partial then | |||
if hastime and H and M and S then | |||
if not (0 <= H and H <= 23 and | |||
0 <= M and M <= 59 and | |||
0 <= S and S <= 59) then | |||
days_hms = hms({ hour = H, minute = M, second = S }) | |||
end | |||
end | |||
if days_hms or not (1 <= d and d <= days_in_month(y, m, calendar)) then | |||
date = date or Date(y, m, 1) | |||
if not date then return end | |||
date = date + (d - 1 + (days_hms or 0)) | |||
y, m, d = date.year, date.month, date.day | |||
if days_hms then | |||
H, M, S = date.hour, date.minute, date.second | |||
end | |||
end | |||
end | |||
numbers.year = y | |||
numbers.month = m | |||
numbers.day = d | |||
if days_hms then | |||
-- Don't set H unless it was valid because a valid H will set hastime. | |||
numbers.hour = H | |||
numbers.minute = M | |||
numbers.second = S | |||
end | |||
end | end | ||
Line 163: | Line 207: | ||
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 | local need_fix | ||
(-9999 <= y and y <= 9999 and | if y and m and d then | ||
date.partial = nil | |||
if not (-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 | ||
if not date.want_fix then | |||
return | |||
end | |||
need_fix = true | |||
end | |||
elseif y and date.partial then | |||
if d or not (-9999 <= y and y <= 9999) then | |||
return | |||
end | |||
if m and not (1 <= m and m <= 12) then | |||
if not date.want_fix then | |||
return | |||
end | |||
need_fix = true | |||
end | |||
else | |||
return | return | ||
end | end | ||
if | if date.partial then | ||
-- | H = nil -- ignore any time | ||
M = nil | |||
S = nil | |||
else | else | ||
H = date. | if H then | ||
-- It is not possible to set M or S without also setting H. | |||
date.hastime = true | |||
else | |||
H = 0 | |||
end | |||
if not (0 <= H and H <= 23 and | |||
0 <= M and M <= 59 and | |||
0 <= S and S <= 59) then | |||
if date.want_fix then | |||
need_fix = true | |||
else | |||
return | |||
end | |||
end | |||
end | end | ||
if | date.want_fix = nil | ||
if need_fix then | |||
fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar) | |||
return | return set_date_from_numbers(date, numbers, options) | ||
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 (may be nil if partial) | ||
date.day = d -- 1 to 31 | date.day = d -- 1 to 31 (* = nil if partial) | ||
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 311: | Line 388: | ||
end | end | ||
local value = date[code.field] | local value = date[code.field] | ||
if not value then | |||
return nil -- an undefined field in a partial date | |||
end | |||
local special = code.special | local special = code.special | ||
if special then | if special then | ||
Line 363: | Line 443: | ||
return spaces .. (result and '1' or '0') | return spaces .. (result and '1' or '0') | ||
end | end | ||
-- This occurs | -- This occurs if id is an undefined field in a partial date, or is the name of a function. | ||
return nil | return nil | ||
end | end | ||
Line 384: | Line 464: | ||
return strftime(date, fmt, options) | return strftime(date, fmt, options) | ||
end | end | ||
elseif date.partial then | |||
fmt = date.month and 'my' or 'y' | |||
else | else | ||
fmt = 'dmy' | fmt = 'dmy' | ||
Line 389: | Line 471: | ||
fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt | fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt | ||
end | end | ||
end | |||
local function bad_format() | |||
-- For consistency with other format processing, return given format | |||
-- (or cleaned format if original was not a string) if invalid. | |||
return mw.text.nowiki(fmt) | |||
end | |||
if date.partial then | |||
-- Ignore days in standard formats like 'ymd'. | |||
if fmt == 'ym' or fmt == 'ymd' then | |||
fmt = date.month and '%Y-%m %{era}' or '%Y %{era}' | |||
elseif fmt == 'my' or fmt == 'dmy' or fmt == 'mdy' then | |||
fmt = date.month and '%B %-Y %{era}' or '%-Y %{era}' | |||
elseif fmt == 'y' then | |||
fmt = date.month and '%-Y %{era}' or '%-Y %{era}' | |||
else | |||
return bad_format() | |||
end | |||
return strftime(date, fmt, options) | |||
end | end | ||
local function hm_fmt() | local function hm_fmt() | ||
Line 411: | Line 511: | ||
f = '%-d %B %-Y %{era}' | f = '%-d %B %-Y %{era}' | ||
else | else | ||
return bad_format() | |||
end | end | ||
t:add(f) | t:add(f) | ||
Line 477: | Line 577: | ||
jul = 7, july = 7, | jul = 7, july = 7, | ||
aug = 8, august = 8, | aug = 8, august = 8, | ||
sep = 9, september = 9, | sep = 9, september = 9, sept = 9, | ||
oct = 10, october = 10, | oct = 10, october = 10, | ||
nov = 11, november = 11, | nov = 11, november = 11, | ||
Line 509: | Line 609: | ||
-- -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 >' | ||
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 | |||
local list = { text = _list_text } | |||
if date.partial then | |||
return list | |||
end | end | ||
local count, offset, operation | local count, offset, operation | ||
Line 607: | Line 710: | ||
end }) | end }) | ||
local function extract_date(text) | local function extract_date(newdate, 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 619: | Line 722: | ||
-- 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 = {}, {} | ||
if text:sub(-1) == 'Z' then | |||
-- Extract date/time from a Wikidata timestamp. | |||
-- The year can be 1 to 16 digits but this module handles 1 to 4 digits only. | |||
-- Examples: '+2016-06-21T14:30:00Z', '-0000000180-00-00T00:00:00Z'. | |||
local sign, y, m, d, H, M, S = text:match('^([+%-])(%d+)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)Z$') | |||
if sign then | |||
y = tonumber(y) | |||
if sign == '-' and y > 0 then | |||
y = -y | |||
end | |||
if y <= 0 then | |||
options.era = 'BCE' | |||
end | |||
date.year = y | |||
m = tonumber(m) | |||
d = tonumber(d) | |||
H = tonumber(H) | |||
M = tonumber(M) | |||
S = tonumber(S) | |||
if m == 0 then | |||
newdate.partial = true | |||
return date, options | |||
end | |||
date.month = m | |||
if d == 0 then | |||
newdate.partial = true | |||
return date, options | |||
end | |||
date.day = d | |||
if H > 0 or M > 0 or S > 0 then | |||
date.hour = H | |||
date.minute = M | |||
date.second = S | |||
end | |||
return date, options | |||
end | |||
return | |||
end | |||
local function extract_ymd(item) | local function extract_ymd(item) | ||
local | -- Called when no day or month has been set. | ||
if | local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$') | ||
if y then | |||
if | if date.year then | ||
m = tonumber( | return | ||
end | |||
if m:match('^%d%d?$') then | |||
m = tonumber(m) | |||
else | else | ||
m = month_number( | m = month_number(m) | ||
end | end | ||
if m then | if m then | ||
date.year = tonumber(y) | |||
date.year = tonumber( | |||
date.month = m | date.month = m | ||
date.day = tonumber( | date.day = tonumber(d) | ||
return true | |||
end | |||
end | |||
end | |||
local function extract_day_or_year(item) | |||
-- Called when a day would be valid, or | |||
-- when a year would be valid if no year has been set and partial is set. | |||
local number, suffix = item:match('^(%d%d?%d?%d?)(.*)$') | |||
if number then | |||
local n = tonumber(number) | |||
if #number <= 2 and n <= 31 then | |||
suffix = suffix:lower() | |||
if suffix == '' or suffix == 'st' or suffix == 'nd' or suffix == 'rd' or suffix == 'th' then | |||
date.day = n | |||
return true | |||
end | |||
elseif suffix == '' and newdate.partial and not date.year then | |||
date.year = n | |||
return true | return true | ||
end | end | ||
Line 666: | Line 827: | ||
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] -- caller checked this is not nil | ||
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 715: | Line 876: | ||
end | end | ||
elseif date.month then | elseif date.month then | ||
if not item | if not extract_day_or_year(item) then | ||
return | return | ||
end | end | ||
elseif extract_month(item) then | |||
elseif | options.format = 'mdy' | ||
elseif extract_ymd(item) then | |||
options.format = 'ymd' | |||
elseif extract_day_or_year(item) then | |||
if date.day then | |||
options.format = 'dmy' | options.format = 'dmy' | ||
end | end | ||
else | |||
return | |||
end | end | ||
end | end | ||
Line 745: | Line 906: | ||
-- 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. | ||
if lhs.partial then | |||
-- Adding to a partial is not supported. | |||
-- Can subtract a date or partial from a partial, but this is not called for that. | |||
return | |||
end | |||
local function is_prefix(text, word, minlen) | local function is_prefix(text, word, minlen) | ||
local n = #text | local n = #text | ||
Line 771: | Line 937: | ||
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' (with optional sign). | ||
local | -- Fractions like '3.25d' are accepted for the units which are handled as days. | ||
if | local sign, numstr, id = rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$') | ||
local y, m | if sign then | ||
num = tonumber(num | if sign == '-' then | ||
is_sub = not (is_sub and true or false) | |||
end | |||
local y, m, days | |||
local num = tonumber(numstr) | |||
if not num then | |||
return | |||
end | |||
id = id:lower() | id = id:lower() | ||
if is_prefix(id, 'years') then | if is_prefix(id, 'years') then | ||
Line 784: | Line 957: | ||
m = num % 12 | m = num % 12 | ||
elseif is_prefix(id, 'weeks') then | elseif is_prefix(id, 'weeks') then | ||
days = num * 7 | |||
elseif is_prefix(id, 'days') then | elseif is_prefix(id, 'days') then | ||
days = num | |||
elseif is_prefix(id, 'hours') then | elseif is_prefix(id, 'hours') then | ||
days = num / 24 | |||
elseif is_prefix(id, 'minutes', 3) then | elseif is_prefix(id, 'minutes', 3) then | ||
days = num / (24 * 60) | |||
elseif is_prefix(id, 'seconds') then | elseif is_prefix(id, 'seconds') then | ||
days = num / (24 * 3600) | |||
else | else | ||
return | |||
end | |||
if days then | |||
return do_days(days) | |||
end | |||
if numstr:find('.', 1, true) then | |||
return | return | ||
end | end | ||
Line 822: | Line 1,001: | ||
end | end | ||
end | end | ||
local full_date_only = { | |||
dayabbr = true, | |||
dayname = true, | |||
dow = true, | |||
dayofweek = true, | |||
dowiso = true, | |||
dayofweekiso = true, | |||
dayofyear = true, | |||
gsd = true, | |||
juliandate = true, | |||
jd = true, | |||
jdz = true, | |||
jdnoon = true, | |||
} | |||
-- 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) | ||
if rawget(self, 'partial') then | |||
if full_date_only[key] then return end | |||
if key == 'monthabbr' or key == 'monthdays' or key == 'monthname' then | |||
if not self.month then return end | |||
end | |||
end | |||
local value | local value | ||
if key == 'dayabbr' then | if key == 'dayabbr' then | ||
Line 905: | Line 1,105: | ||
-- 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 only called if lhs and rhs have the same metatable. | -- This is only called if lhs and rhs have the same metatable. | ||
if lhs.partial or rhs.partial then | |||
-- One date is partial; the other is a partial or a full date. | |||
-- The months may both be nil, but must be the same. | |||
return lhs.year == rhs.year and lhs.month == rhs.month and lhs.calendar == rhs.calendar | |||
end | |||
return lhs.jdz == rhs.jdz | return lhs.jdz == rhs.jdz | ||
end | end | ||
Line 912: | Line 1,117: | ||
-- 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 only called if lhs and rhs have the same metatable. | -- This is only called if lhs and rhs have the same metatable. | ||
if lhs.partial or rhs.partial then | |||
-- One date is partial; the other is a partial or a full date. | |||
if lhs.calendar ~= rhs.calendar then | |||
return lhs.calendar == 'Julian' | |||
end | |||
if lhs.partial then | |||
lhs = lhs.partial.first | |||
end | |||
if rhs.partial then | |||
rhs = rhs.partial.first | |||
end | |||
end | |||
return lhs.jdz < rhs.jdz | return lhs.jdz < rhs.jdz | ||
end | end | ||
Line 932: | Line 1,149: | ||
-- (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. | ||
-- A partial date has a valid year, however its month may be nil, and | |||
-- its day and time fields are nil. | |||
-- Field partial is set to false (if a full date) or a table (if a partial date). | |||
local calendars = { julian = 'Julian', gregorian = 'Gregorian' } | local calendars = { julian = 'Julian', gregorian = 'Gregorian' } | ||
local newdate = { | local newdate = { | ||
Line 955: | Line 1,175: | ||
elseif calendars[vlower] then | elseif calendars[vlower] then | ||
newdate.calendar = calendars[vlower] | newdate.calendar = calendars[vlower] | ||
elseif vlower == 'partial' then | |||
newdate.partial = true | |||
elseif vlower == 'fix' then | |||
newdate.want_fix = true | |||
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 962: | Line 1,186: | ||
is_copy = true | is_copy = true | ||
newdate.calendar = v.calendar | newdate.calendar = v.calendar | ||
newdate.partial = v.partial | |||
newdate.hastime = v.hastime | newdate.hastime = v.hastime | ||
newdate.options = v.options | newdate.options = v.options | ||
Line 1,025: | Line 1,250: | ||
end | end | ||
if argtype == 'datetext' then | if argtype == 'datetext' then | ||
if tnums or not set_date_from_numbers(newdate, extract_date(datetext)) then | if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then | ||
return | return | ||
end | end | ||
elseif argtype == 'juliandate' then | elseif argtype == 'juliandate' then | ||
newdate.partial = nil | |||
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,034: | Line 1,260: | ||
end | end | ||
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then | elseif argtype == 'currentdate' or argtype == 'currentdatetime' then | ||
newdate.partial = nil | |||
newdate.year = current.year | newdate.year = current.year | ||
newdate.month = current.month | newdate.month = current.month | ||
Line 1,056: | Line 1,283: | ||
return | return | ||
end | end | ||
end | |||
if newdate.partial then | |||
local year = newdate.year | |||
local month = newdate.month | |||
local first = Date(year, month or 1, 1, newdate.calendar) | |||
month = month or 12 | |||
local last = Date(year, month, days_in_month(year, month), newdate.calendar) | |||
newdate.partial = { first = first, last = last } | |||
else | |||
newdate.partial = false -- avoid index lookup | |||
end | end | ||
setmetatable(newdate, datemt) | setmetatable(newdate, datemt) | ||
Line 1,073: | Line 1,310: | ||
local function _diff_age(diff, code, options) | local function _diff_age(diff, code, options) | ||
-- Return a tuple of values from diff as specified by code. | -- Return a tuple of integer values from diff as specified by code, except that | ||
-- If | -- each integer may be a list of two integers for a diff with a partial date. | ||
-- If want round, the least significant unit is rounded to nearest whole unit. | |||
-- For a duration, an extra day is added. | |||
local wantround, wantduration, wantrange | |||
if type(options) == 'table' then | |||
wantround = options.round | |||
wantduration = options.duration | |||
wantrange = options.range | |||
else | |||
wantround = options | |||
end | |||
if not is_diff(diff) then | if not is_diff(diff) then | ||
local f = | local f = wantduration 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 | if diff.partial then | ||
-- Ignore wantround, wantduration. | |||
local function choose(v) | |||
if type(v) == 'table' then | |||
if not wantrange then | |||
-- Example: Date('partial', 2005) - Date('partial', 2001) gives | |||
-- diff.years = { 3, 4 } to show the range of possible results. | |||
-- If do not want a range, choose the second value as more expected. | |||
return v[2] | |||
end | |||
end | |||
return v | |||
end | |||
if code == 'ym' or code == 'ymd' then | |||
if not wantrange and diff.iszero then | |||
-- This avoids an unexpected result such as | |||
-- Date('partial', 2001) - Date('partial', 2001) | |||
-- giving diff = { years = 0, months = { 0, 11 } } | |||
-- which would be reported as 0 years and 11 months. | |||
return 0, 0 | |||
end | |||
return choose(diff.partial.years), choose(diff.partial.months) | |||
end | |||
-- Default: assume code == 'y'; ignore invalid codes. | |||
return choose(diff.partial.years) | |||
end | |||
local extra_days = wantduration 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 d = diff.age_days + | local offset = wantround and 0.5 or 0 | ||
if code == ' | local days = diff.age_days + extra_days | ||
return | if code == 'wd' or code == 'd' then | ||
days = floor(days + offset) | |||
if code == 'd' then | |||
return days | |||
end | |||
return floor(days/7), days % 7 | |||
end | |||
return floor(days/7 + offset) | |||
end | |||
local H, M = diff.hours, diff.minutes | |||
if code == 'dh' or code == 'dhm' then | |||
local days = floor(diff.age_days + extra_days) | |||
local inc_hour | |||
if wantround then | |||
if code == 'dh' then | |||
if M >= 30 then | |||
inc_hour = true | |||
end | |||
elseif diff.seconds >= 30 then | |||
M = M + 1 | |||
if M >= 60 then | |||
M = 0 | |||
inc_hour = true | |||
end | |||
end | |||
if inc_hour then | |||
H = H + 1 | |||
if H >= 24 then | |||
H = 0 | |||
days = days + 1 | |||
end | |||
end | |||
end | |||
if code == 'dh' then | |||
return days, H | |||
end | |||
return days, H, M | |||
end | |||
if wantround then | |||
local inc_hour | |||
if code == 'ymdh' or code == 'ymwdh' then | |||
if M >= 30 then | |||
inc_hour = true | |||
end | |||
elseif code == 'ymdhm' or code == 'ymwdhm' then | |||
if diff.seconds >= 30 then | |||
M = M + 1 | |||
if M >= 60 then | |||
M = 0 | |||
inc_hour = true | |||
end | |||
end | |||
elseif code == 'ymd' or code == 'ymwd' or code == 'yd' or code == 'md' then | |||
if H >= 12 then | |||
extra_days = extra_days + 1 | |||
end | |||
end | end | ||
if inc_hour then | |||
H = H + 1 | |||
if H >= 24 then | |||
H = 0 | |||
extra_days = extra_days + 1 | |||
end | |||
end | end | ||
end | end | ||
local y, m, d = diff.years, diff.months, diff.days | local y, m, d = diff.years, diff.months, diff.days | ||
if | if extra_days > 0 then | ||
d = d + | d = d + extra_days | ||
if d > 28 or code == 'yd' then | |||
if d > | -- Recalculate in case have passed a month. | ||
diff = diff.date1 + extra_days - diff.date2 | |||
y, m, d = diff.years, diff.months, diff.days | |||
end | end | ||
end | end | ||
if code == 'ymd' then | if code == 'ymd' then | ||
return y, m, d | return y, m, d | ||
end | elseif code == 'yd' then | ||
if y > 0 then | |||
return y, m | -- It is known that diff.date1 > diff.date2. | ||
diff = diff.date1 - (diff.date2 + (y .. 'y')) | |||
end | |||
return y, floor(diff.age_days) | |||
elseif code == 'md' then | |||
return y * 12 + m, d | |||
elseif code == 'ym' or code == 'm' then | |||
if wantround then | |||
if d >= 16 then | |||
m = m + 1 | |||
if m >= 12 then | |||
m = 0 | |||
y = y + 1 | |||
end | |||
end | |||
end | |||
if code == 'ym' then | |||
return y, m | |||
end | |||
return y * 12 + m | return y * 12 + m | ||
elseif code == 'ymw' then | |||
local weeks = floor(d/7) | |||
if wantround then | |||
local days = d % 7 | |||
if days > 3 or (days == 3 and H >= 12) then | |||
weeks = weeks + 1 | |||
end | |||
end | |||
return y, m, weeks | |||
elseif code == 'ymwd' then | |||
return y, m, floor(d/7), d % 7 | |||
elseif code == 'ymdh' then | |||
return y, m, d, H | |||
elseif code == 'ymwdh' then | |||
return y, m, floor(d/7), d % 7, H | |||
elseif code == 'ymdhm' then | |||
return y, m, d, H, M | |||
elseif code == 'ymwdhm' then | |||
return y, m, floor(d/7), d % 7, H, M | |||
end | end | ||
-- Default: assume code == 'y'; ignore invalid codes. | |||
if wantround and m >= 6 then | |||
y = y + 1 | |||
end | end | ||
return y | return y | ||
end | end | ||
local function _diff_duration(diff, code) | local function _diff_duration(diff, code, options) | ||
return _diff_age(diff, code, | if type(options) ~= 'table' then | ||
options = { round = options } | |||
end | |||
options.duration = true | |||
return _diff_age(diff, code, options) | |||
end | end | ||
Line 1,134: | Line 1,500: | ||
local value | local value | ||
if key == 'age_days' then | if key == 'age_days' then | ||
value = self.date1.jdz - self.date2.jdz | if rawget(self, 'partial') then | ||
local function jdz(date) | |||
return (date.partial and date.partial.first or date).jdz | |||
end | |||
value = jdz(self.date1) - jdz(self.date2) | |||
else | |||
value = self.date1.jdz - self.date2.jdz | |||
end | |||
end | end | ||
if value ~= nil then | if value ~= nil then | ||
Line 1,160: | Line 1,533: | ||
end | end | ||
local isnegative = false | local isnegative = false | ||
local iszero = false | |||
if date1 < date2 then | if date1 < date2 then | ||
isnegative = true | isnegative = true | ||
date1, date2 = date2, date1 | date1, date2 = date2, date1 | ||
elseif date1 == date2 then | |||
iszero = true | |||
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). | ||
if date1.partial or date2.partial then | |||
-- Two partial dates might have timelines: | |||
---------------------A=================B--- date1 is from A to B inclusive | |||
--------C=======D-------------------------- date2 is from C to D inclusive | |||
-- date1 > date2 iff A > C (date1.partial.first > date2.partial.first) | |||
-- The periods can overlap ('April 2001' - '2001'): | |||
-------------A===B------------------------- A=2001-04-01 B=2001-04-30 | |||
--------C=====================D------------ C=2001-01-01 D=2001-12-31 | |||
local function zdiff(date1, date2) | |||
local diff = date1 - date2 | |||
if diff.isnegative then | |||
return { years = 0, months = 0 } | |||
end | |||
return diff | |||
end | |||
local function getdate(date, which) | |||
return date.partial and date.partial[which] or date | |||
end | |||
local maxdiff = zdiff(getdate(date1, 'last'), getdate(date2, 'first')) | |||
local mindiff = zdiff(getdate(date1, 'first'), getdate(date2, 'last')) | |||
local years, months | |||
if maxdiff.years == mindiff.years then | |||
years = maxdiff.years | |||
if maxdiff.months == mindiff.months then | |||
months = maxdiff.months | |||
else | |||
months = { mindiff.months, maxdiff.months } | |||
end | |||
else | |||
years = { mindiff.years, maxdiff.years } | |||
end | |||
return setmetatable({ | |||
date1 = date1, | |||
date2 = date2, | |||
partial = { years = years, months = months }, | |||
isnegative = isnegative, | |||
iszero = iszero, | |||
age = _diff_age, | |||
duration = _diff_duration, | |||
}, diffmt) | |||
end | |||
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,193: | Line 1,610: | ||
date1 = date1, | date1 = date1, | ||
date2 = date2, | date2 = date2, | ||
partial = false, -- avoid index lookup | |||
years = years, | years = years, | ||
months = months, | months = months, | ||
Line 1,200: | Line 1,618: | ||
seconds = S, | seconds = S, | ||
isnegative = isnegative, | isnegative = isnegative, | ||
iszero = iszero, | |||
age = _diff_age, | age = _diff_age, | ||
duration = _diff_duration, | duration = _diff_duration, |