Jump to content

Module:Date: Difference between revisions

11,778 bytes added ,  8 years ago
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) from date's time.
-- 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 not (y and m and d) or not
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 H then
if date.partial then
-- It is not possible to set M or S without also setting H.
H = nil  -- ignore any time
date.hastime = true
M = nil
S = nil
else
else
H = date.hour or 0
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 not (0 <= H and H <= 23 and
date.want_fix = nil
0 <= M and M <= 59 and
if need_fix then
0 <= S and S <= 59) 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, for example, if id is the name of a function.
-- 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
error('date:text: invalid format', 2)
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 >'
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
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 ystr, mstr, dstr = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
-- Called when no day or month has been set.
if ystr then
local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
local m
if y then
if mstr:match('^%d%d?$') then
if date.year then
m = tonumber(mstr)
return
end
if m:match('^%d%d?$') then
m = tonumber(m)
else
else
m = month_number(mstr)
m = month_number(m)
end
end
if m then
if m then
options.format = 'ymd'
date.year = tonumber(y)
date.year = tonumber(ystr)
date.month = m
date.month = m
date.day = tonumber(dstr)
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:match('^(%d%d?)$') then
if not extract_day_or_year(item) then
return
return
end
end
date.day = tonumber(item)
elseif extract_month(item) then
elseif not extract_ymd(item) then
options.format = 'mdy'
if item:match('^(%d%d?)$') then
elseif extract_ymd(item) then
date.day = tonumber(item)
options.format = 'ymd'
elseif extract_day_or_year(item) then
if date.day then
options.format = 'dmy'
options.format = 'dmy'
elseif extract_month(item) then
options.format = 'mdy'
else
return
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' (unsigned integer only).
-- rhs is a single component like '26m' or '26 months' (with optional sign).
local num, id = rhs:match('^%s*(%d+)%s*(%a+)$')
-- Fractions like '3.25d' are accepted for the units which are handled as days.
if num then
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
return do_days(num * 7)
days = num * 7
elseif is_prefix(id, 'days') then
elseif is_prefix(id, 'days') then
return do_days(num)
days = num
elseif is_prefix(id, 'hours') then
elseif is_prefix(id, 'hours') then
return do_days(num / 24)
days = num / 24
elseif is_prefix(id, 'minutes', 3) then
elseif is_prefix(id, 'minutes', 3) then
return do_days(num / (24 * 60))
days = num / (24 * 60)
elseif is_prefix(id, 'seconds') then
elseif is_prefix(id, 'seconds') then
return do_days(num / (24 * 3600))
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 options == 'duration', an extra day is added.
-- 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 = options == 'duration' and 'duration' or 'age'
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 extra_day = options == 'duration' and 1 or 0
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 + extra_day
local offset = wantround and 0.5 or 0
if code == 'd' then
local days = diff.age_days + extra_days
return d
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
local w = floor(d / 7)
if inc_hour then
if code == 'w' then
H = H + 1
return w
if H >= 24 then
H = 0
extra_days = extra_days + 1
end
end
end
return w, d % 7
end
end
local y, m, d = diff.years, diff.months, diff.days
local y, m, d = diff.years, diff.months, diff.days
if extra_day > 0 then
if extra_days > 0 then
d = d + extra_day
d = d + extra_days
local to_date = diff.date1
if d > 28 or code == 'yd' then
if d > days_in_month(to_date.year, to_date.month, to_date.calendar) then
-- Recalculate in case have passed a month.
d = 1
diff = diff.date1 + extra_days - diff.date2
m = m + 1
y, m, d = diff.years, diff.months, diff.days
if m > 12 then
m = 1
y = y + 1
end
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 code == 'ym' then
if y > 0 then
return y, m
-- It is known that diff.date1 > diff.date2.
end
diff = diff.date1 - (diff.date2 + (y .. 'y'))
if code == 'm' then
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
if code == 'ymwd' then
-- Default: assume code == 'y'; ignore invalid codes.
return y, m, floor(d / 7), d % 7
if wantround and m >= 6 then
y = y + 1
end
end
return y -- default: assume code == 'y'; ignore invalid codes
return y
end
end


local function _diff_duration(diff, code)
local function _diff_duration(diff, code, options)
return _diff_age(diff, code, 'duration')
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,
Anonymous user
Cookies help us deliver our services. By using our services, you agree to our use of cookies.