Editing Module:Date

Warning: You are not logged in. Your IP address will be publicly visible if you make any edits. Read the Privacy Policy to learn what information we collect about you and how we use it.

If you log in or create an account, your edits will be attributed to your username, along with other benefits.

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 from date's time, where (0 <= fraction < 1)
-- Return fraction of a day (0 <= fraction < 1) from date's time.
-- 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 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
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 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
local need_fix
if not (y and m and d) or not
if y and m and d then
(-9999 <= y and y <= 9999 and
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 date.partial then
if H then
H = nil  -- ignore any time
-- It is not possible to set M or S without also setting H.
M = nil
date.hastime = true
S = nil
else
else
if H then
H = date.hour or 0
-- 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
date.want_fix = nil
if not (0 <= H and H <= 23 and
if need_fix then
0 <= M and M <= 59 and
fix_numbers(numbers, y, m, d, H, M, S, date.partial, date.hastime, date.calendar)
0 <= S and S <= 59) then
return set_date_from_numbers(date, numbers, options)
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 (may be nil if partial)
date.month = m  -- 1 to 12
date.day = d    -- 1 to 31 (* = nil if partial)
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]
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 443: Line 363:
return  spaces .. (result and '1' or '0')
return  spaces .. (result and '1' or '0')
end
end
-- This occurs if id is an undefined field in a partial date, or is the name of a function.
-- 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
elseif date.partial then
fmt = date.month and 'my' or 'y'
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
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 511: Line 411:
f = '%-d %B %-Y %{era}'
f = '%-d %B %-Y %{era}'
else
else
return bad_format()
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 = 9, sept = 9,
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
local list = { text = _list_text }
if date.partial then
return list
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(newdate, text)
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 = {}, {}
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)
-- Called when no day or month has been set.
local ystr, mstr, dstr = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
local y, m, d = item:match('^(%d%d%d%d)%-(%w+)%-(%d%d?)$')
if ystr then
if y then
local m
if date.year then
if mstr:match('^%d%d?$') then
return
m = tonumber(mstr)
end
if m:match('^%d%d?$') then
m = tonumber(m)
else
else
m = month_number(m)
m = month_number(mstr)
end
end
if m then
if m then
date.year = tonumber(y)
options.format = 'ymd'
date.year = tonumber(ystr)
date.month = m
date.month = m
date.day = tonumber(d)
date.day = tonumber(dstr)
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 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] -- caller checked this is not nil
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 extract_day_or_year(item) then
if not item:match('^(%d%d?)$') then
return
return
end
end
elseif extract_month(item) then
date.day = tonumber(item)
options.format = 'mdy'
elseif not extract_ymd(item) then
elseif extract_ymd(item) then
if item:match('^(%d%d?)$') then
options.format = 'ymd'
date.day = tonumber(item)
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 897: Line 738:
end
end
return date, options
return date, options
end
local function autofill(date1, date2)
-- Fill any missing month or day in each date using the
-- corresponding component from the other date, if present,
-- or with 1 if both dates are missing the month or day.
-- This gives a good result for calculating the difference
-- between two partial dates when no range is wanted.
-- Return filled date1, date2 (two full dates).
local function filled(a, b)
-- Return date a filled, if necessary, with month and/or day from date b.
-- The filled day is truncated to fit the number of days in the month.
local fillmonth, fillday
if not a.month then
fillmonth = b.month or 1
end
if not a.day then
fillday = b.day or 1
end
if fillmonth or fillday then  -- need to create a new date
a = Date(a, {
month = fillmonth,
day = math.min(fillday or a.day, days_in_month(a.year, fillmonth or a.month, a.calendar))
})
end
return a
end
return filled(date1, date2), filled(date2, date1)
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.
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 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' (with optional sign).
-- rhs is a single component like '26m' or '26 months' (unsigned integer only).
-- Fractions like '3.25d' are accepted for the units which are handled as days.
local num, id = rhs:match('^%s*(%d+)%s*(%a+)$')
local sign, numstr, id = rhs:match('^%s*([+-]?)([%d%.]+)%s*(%a+)$')
if num then
if sign then
local y, m
if sign == '-' then
num = tonumber(num)
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 983: Line 784:
m = num % 12
m = num % 12
elseif is_prefix(id, 'weeks') then
elseif is_prefix(id, 'weeks') then
days = num * 7
return do_days(num * 7)
elseif is_prefix(id, 'days') then
elseif is_prefix(id, 'days') then
days = num
return do_days(num)
elseif is_prefix(id, 'hours') then
elseif is_prefix(id, 'hours') then
days = num / 24
return do_days(num / 24)
elseif is_prefix(id, 'minutes', 3) then
elseif is_prefix(id, 'minutes', 3) then
days = num / (24 * 60)
return do_days(num / (24 * 60))
elseif is_prefix(id, 'seconds') then
elseif is_prefix(id, 'seconds') then
days = num / (24 * 3600)
return do_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 1,027: Line 822:
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 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 only if lhs and rhs have the same type and the same metamethod.
-- 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 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 only if lhs and rhs have the same type and the same metamethod.
-- 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 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.
-- 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 1,188: Line 942:
options = {},
options = {},
list = _date_list,
list = _date_list,
subtract = function (self, rhs, options)
return DateDiff(self, rhs, options)
end,
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 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 1,215: Line 962:
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,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(newdate, datetext)) then
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.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,289: Line 1,034:
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,312: Line 1,056:
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,339: Line 1,073:


local function _diff_age(diff, code, options)
local function _diff_age(diff, code, options)
-- Return a tuple of integer values from diff as specified by code, except that
-- Return a tuple of values from diff as specified by code.
-- each integer may be a list of two integers for a diff with a partial date, or
-- If options == 'duration', an extra day is added.
-- return nil if the code is not supported.
-- 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 = wantduration and 'duration' or 'age'
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
if diff.partial then
local extra_day = options == 'duration' and 1 or 0
-- Ignore wantround, wantduration.
local function choose(v)
if type(v) == 'table' then
if not wantrange or v[1] == v[2] 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
if code == 'y' then
return choose(diff.partial.years)
end
if code == 'm' or code == 'w' or code == 'd' then
return choose({ diff.partial.mindiff:age(code), diff.partial.maxdiff:age(code) })
end
return nil
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 offset = wantround and 0.5 or 0
local d = diff.age_days + extra_day
local days = diff.age_days + extra_days
if code == 'd' then
if code == 'wd' or code == 'd' then
return d
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, S = diff.hours, diff.minutes, diff.seconds
if code == 'dh' or code == 'dhm' or code == 'dhms' or code == 'h' or code == 'hm' or code == 'hms' or code == 'M' or code == 's' then
local days = floor(diff.age_days + extra_days)
local inc_hour
if wantround then
if code == 'dh' or code == 'h' then
if M >= 30 then
inc_hour = true
end
elseif code == 'dhm' or code == 'hm' then
if S >= 30 then
M = M + 1
if M >= 60 then
M = 0
inc_hour = true
end
end
elseif code == 'M' then
if S >= 30 then
M = M + 1
end
else
-- Nothing needed because S is an integer.
end
if inc_hour then
H = H + 1
if H >= 24 then
H = 0
days = days + 1
end
end
end
end
if code == 'dh' or code == 'dhm' or code == 'dhms' then
local w = floor(d / 7)
if code == 'dh' then
if code == 'w' then
return days, H
return w
elseif code == 'dhm' then
return days, H, M
else
return days, H, M, S
end
end
local hours = days * 24 + H
if code == 'h' then
return hours
elseif code == 'hm' then
return hours, M
elseif code == 'M' or code == 's' then
M = hours * 60 + M
if code == 'M' then
return M
end
return M * 60 + S
end
end
return hours, M, S
return w, d % 7
end
end
if wantround then
local y, m, d = diff.years, diff.months, diff.days
local inc_hour
if extra_day > 0 then
if code == 'ymdh' or code == 'ymwdh' then
d = d + extra_day
if M >= 30 then
local to_date = diff.date1
inc_hour = true
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
elseif code == 'ymdhm' or code == 'ymwdhm' then
if S >= 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
if inc_hour then
H = H + 1
if H >= 24 then
H = 0
extra_days = extra_days + 1
end
end
end
local y, m, d = diff.years, diff.months, diff.days
if extra_days > 0 then
d = d + extra_days
if d > 28 or code == 'yd' then
-- 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
elseif code == 'yd' then
end
if y > 0 then
if code == 'ym' then
-- It is known that diff.date1 > diff.date2.
return y, m
diff = diff.date1 - (diff.date2 + (y .. 'y'))
end
end
if code == 'm' then
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 == 'y' then
if code == 'ymwd' then
if wantround and m >= 6 then
return y, m, floor(d / 7), d % 7
y = y + 1
end
return y
end
end
return nil
return y  -- default: assume code == 'y'; ignore invalid codes
end
end


local function _diff_duration(diff, code, options)
local function _diff_duration(diff, code)
if type(options) ~= 'table' then
return _diff_age(diff, code, 'duration')
options = { round = options }
end
options.duration = true
return _diff_age(diff, code, options)
end
end


Line 1,563: Line 1,134:
local value
local value
if key == 'age_days' then
if key == 'age_days' then
if rawget(self, 'partial') then
value = self.date1.jdz - self.date2.jdz
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,579: Line 1,143:
}
}


function DateDiff(date1, date2, options)  -- for forward declaration above
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
local wantfill
if type(options) == 'table' then
wantfill = options.fill
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
if wantfill then
date1, date2 = autofill(date1, date2)
else
local function zdiff(date1, date2)
local diff = date1 - date2
if diff.isnegative then
return date1 - date1  -- a valid diff in case we call its methods
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,
maxdiff = maxdiff,
mindiff = mindiff,
},
isnegative = isnegative,
iszero = iszero,
age = _diff_age,
duration = _diff_duration,
}, diffmt)
end
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,686: Line 1,193:
date1 = date1,
date1 = date1,
date2 = date2,
date2 = date2,
partial = false,  -- avoid index lookup
years = years,
years = years,
months = months,
months = months,
Line 1,694: Line 1,200:
seconds = S,
seconds = S,
isnegative = isnegative,
isnegative = isnegative,
iszero = iszero,
age = _diff_age,
age = _diff_age,
duration = _diff_duration,
duration = _diff_duration,
Please note that all contributions to Nonbinary Wiki are considered to be released under the Creative Commons Attribution-ShareAlike (see Nonbinary Wiki:Copyrights for details). If you do not want your writing to be edited mercilessly and redistributed at will, then do not submit it here.
You are also promising us that you wrote this yourself, or copied it from a public domain or similar free resource. Do not submit copyrighted work without permission!
Cancel Editing help (opens in new window)

Template used on this page: