list = _date_list,
subtract = function (self, rhs, options)
return DateDiff(self, rhs, options)
end,
text = _date_text,
}
local argtype, datetext, is_copy, jd_number, tnums
local numindex = 0
local numfields = { 'year', 'month', 'day', 'hour', 'minute', 'second' }
local numbers = {}
for _, v in ipairs({...}) do
v = strip_to_nil(v)
local vlower = type(v) == 'string' and v:lower or nil
if v == nil then
-- Ignore empty arguments after stripping so modules can directly pass template parameters.
elseif calendars[vlower] then
newdate.calendar = calendars[vlower]
elseif vlower == 'partial' then
newdate.partial = true
elseif vlower == 'fix' then
newdate.want_fix = true
elseif is_date(v) then
-- Copy existing date (items can be overridden by other arguments).
if is_copy or tnums then
return
end
is_copy = true
newdate.calendar = v.calendar
newdate.partial = v.partial
newdate.hastime = v.hastime
newdate.options = v.options
newdate.year = v.year
newdate.month = v.month
newdate.day = v.day
newdate.hour = v.hour
newdate.minute = v.minute
newdate.second = v.second
elseif type(v) == 'table' then
if tnums then
return
end
tnums = {}
local tfields = { year=1, month=1, day=1, hour=2, minute=2, second=2 }
for tk, tv in pairs(v) do
if tfields[tk] then
tnums[tk] = tonumber(tv)
end
if tfields[tk] == 2 then
newdate.hastime = true
end
end
else
local num = tonumber(v)
if not num and argtype == 'setdate' and numindex == 1 then
num = month_number(v)
end
if num then
if not argtype then
argtype = 'setdate'
end
if argtype == 'setdate' and numindex < 6 then
numindex = numindex + 1
numbers[numfields[numindex]] = num
elseif argtype == 'juliandate' and not jd_number then
jd_number = num
if type(v) == 'string' then
if v:find('.', 1, true) then
newdate.hastime = true
end
elseif num ~= floor(num) then
-- The given value was a number. The time will be used
-- if the fractional part is nonzero.
newdate.hastime = true
end
else
return
end
elseif argtype then
return
elseif type(v) == 'string' then
if v == 'currentdate' or v == 'currentdatetime' or v == 'juliandate' then
argtype = v
else
argtype = 'datetext'
datetext = v
end
else
return
end
end
end
if argtype == 'datetext' then
if tnums or not set_date_from_numbers(newdate, extract_date(newdate, datetext)) then
return
end
elseif argtype == 'juliandate' then
newdate.partial = nil
newdate.jd = jd_number
if not set_date_from_jd(newdate) then
return
end
elseif argtype == 'currentdate' or argtype == 'currentdatetime' then
newdate.partial = nil
newdate.year = current.year
newdate.month = current.month
newdate.day = current.day
if argtype == 'currentdatetime' then
newdate.hour = current.hour
newdate.minute = current.minute
newdate.second = current.second
newdate.hastime = true
end
newdate.calendar = 'Gregorian' -- ignore any given calendar name
elseif argtype == 'setdate' then
if tnums or not set_date_from_numbers(newdate, numbers) then
return
end
elseif not (is_copy or tnums) then
return
end
if tnums then
newdate.jd = nil -- force recalculation in case jd was set before changes from tnums
if not set_date_from_numbers(newdate, tnums) then
return
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
setmetatable(newdate, datemt)
local readonly = {}
local mt = {
__index = newdate,
__newindex = function(t, k, v) error('date.' .. tostring(k) .. ' is read-only', 2) end,
__add = mt_date_add,
__sub = mt_date_sub,
__concat = mt_date_concat,
__tostring = mt_date_tostring,
__eq = mt_date_eq,
__lt = mt_date_lt,
}
return setmetatable(readonly, mt)
end  
local function _diff_age(diff, code, options)
-- Return a tuple of integer values from diff as specified by code, except that
-- each integer may be a list of two integers for a diff with a partial date, or
-- 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
local f = wantduration and 'duration' or 'age'
error(f .. ': need a date difference (use "diff:' .. f .. '" with a colon)', 2)
end
if diff.partial then
-- 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
local offset = wantround and 0.5 or 0
local days = diff.age_days + extra_days
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, 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' 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
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
if code == 'dh' or code == 'dhm' or code == 'dhms' then
if code == 'dh' then
return days, H
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
end
return hours, M, S
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 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
if code == 'ymd' then
return y, m, d
elseif code == 'yd' then
if y > 0 then
-- 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
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