Jump to content

Module:Date: Difference between revisions

2,645 bytes added ,  8 years ago
date:list() now accepts a count; DateDiff methods age and duration; cleaner member names; remember if time entered with an am/pm option for default output
(rework date differences for more consistent years/months/days; differences include hours/minutes/seconds; can add 'date + diff'; tweaks)
(date:list() now accepts a count; DateDiff methods age and duration; cleaner member names; remember if time entered with an am/pm option for default output)
Line 85: Line 85:
local a = floor((14 - date.month)/12)
local a = floor((14 - date.month)/12)
local y = date.year + 4800 - a
local y = date.year + 4800 - a
if date.calname == 'Julian' then
if date.calendar == 'Julian' then
offset = floor(y/4) - 32083
offset = floor(y/4) - 32083
else
else
Line 105: Line 105:
-- This handles the proleptic Julian and Gregorian calendars.
-- This handles the proleptic Julian and Gregorian calendars.
-- Negative Julian dates are not defined but they work.
-- Negative Julian dates are not defined but they work.
local calname = date.calname
local calname = date.calendar
local low, high  -- min/max limits for date ranges −9999-01-01 to 9999-12-31
local low, high  -- min/max limits for date ranges −9999-01-01 to 9999-12-31
if calname == 'Gregorian' then
if calname == 'Gregorian' then
Line 166: Line 166:
(-9999 <= y and y <= 9999 and
(-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.calname)) then
1 <= d and d <= days_in_month(y, m, date.calendar)) then
return
return
end
end
Line 199: Line 199:
-- If options1 is a string, return a table with its settings, or
-- If options1 is a string, return a table with its settings, or
-- if it is a table, use its settings.
-- if it is a table, use its settings.
-- Missing options are set from options2 or defaults.
-- Missing options are set from table options2 or defaults.
-- If a default is used, a flag is set so caller knows the value was not intentionally set.
-- Valid option settings are:
-- Valid option settings are:
-- am: 'am', 'a.m.', 'AM', 'A.M.'
-- am: 'am', 'a.m.', 'AM', 'A.M.'
Line 209: Line 210:
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
-- BCMINUS displays a MINUS if year < 0 and the display format does not include %{era}.
-- BCNEGATIVE is similar but displays a hyphen.
-- BCNEGATIVE is similar but displays a hyphen.
local result = {}
local result = { bydefault = {} }
if type(options1) == 'table' then
if type(options1) == 'table' then
result = options1
result.am = options1.am
result.era = options1.era
elseif type(options1) == 'string' then
elseif type(options1) == 'string' then
-- Example: 'am:AM era:BC' or 'am=AM era=BC'.
-- Example: 'am:AM era:BC' or 'am=AM era=BC'.
Line 224: Line 226:
local defaults = { am = 'am', era = 'BC' }
local defaults = { am = 'am', era = 'BC' }
for k, v in pairs(defaults) do
for k, v in pairs(defaults) do
result[k] = result[k] or options2[k] or v
if not result[k] then
if options2[k] then
result[k] = options2[k]
else
result[k] = v
result.bydefault[k] = true
end
end
end
end
return result
return result
Line 287: Line 296:
M = { fmt = '%02d', fmt2 = '%d', field = 'minute' },
M = { fmt = '%02d', fmt2 = '%d', field = 'minute' },
S = { fmt = '%02d', fmt2 = '%d', field = 'second' },
S = { fmt = '%02d', fmt2 = '%d', field = 'second' },
j = { fmt = '%03d', fmt2 = '%d', field = 'doy' },
j = { fmt = '%03d', fmt2 = '%d', field = 'dayofyear' },
I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' },
I = { fmt = '%02d', fmt2 = '%d', field = 'hour', special = 'hour12' },
p = { field = 'hour', special = 'am' },
p = { field = 'hour', special = 'am' },
Line 372: Line 381:
end
end
if type(fmt) ~= 'string' then
if type(fmt) ~= 'string' then
fmt = '%-d %B %-Y %{era}'
fmt = 'dmy'
if date.hastime then
if date.hastime then
if date.second > 0 then
fmt = (date.second > 0 and 'hms ' or 'hm ') .. fmt
fmt = '%H:%M:%S ' .. fmt
else
fmt = '%H:%M ' .. fmt
end
end
end
elseif fmt:find('%', 1, true) then
return strftime(date, fmt, options)
return strftime(date, fmt, options)
end
end
if fmt:find('%', 1, true) then
local function hm_fmt()
return strftime(date, fmt, options)
local plain = make_option_table(options, date.options).bydefault.am
return plain and '%H:%M' or '%-I:%M %p'
end
end
local need_time = date.hastime
local t = collection()
local t = collection()
for item in fmt:gmatch('%S+') do
for item in fmt:gmatch('%S+') do
local f
local f
if item == 'hm' then
if item == 'hm' then
f = '%H:%M'
f = hm_fmt()
need_time = false
elseif item == 'hms' then
elseif item == 'hms' then
f = '%H:%M:%S'
f = '%H:%M:%S'
need_time = false
elseif item == 'ymd' then
elseif item == 'ymd' then
f = '%Y-%m-%d %{era}'
f = '%Y-%m-%d %{era}'
Line 403: Line 413:
t:add(f)
t:add(f)
end
end
return strftime(date, t:join(' '), options)
fmt = t:join(' ')
if need_time then
fmt = hm_fmt() .. ' ' .. fmt
end
return strftime(date, fmt, options)
end
end


Line 480: Line 494:
end
end


local function _make_list(date, spec)
local function _date_list(date, spec)
-- Return a possibly empty numbered table of dates meeting the specification.
-- Return a possibly empty numbered table of dates meeting the specification.
-- The spec should be a string like "Tue >=" meaning that the list will
-- Dates in the list are in ascending order (oldest date first).
-- hold dates for all Tuesdays on or after date, and in date's month.
-- The spec should be a string of form "<count> <day> <op>"
-- where each item is optional and
--  count = number of items wanted in list
--   day = abbreviation or name such as Mon or Monday
--  op = >, >=, <, <= (default is > meaning after date)
-- If no count is given, the list is for the specified days in date's month.
-- The default day is date's day.
-- The spec can also be a positive or negative number:
--  -5 is equivalent to '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
return 'Need a date (use "date:list()" with a colon).'
return 'Need a date (use "date:list()" with a colon).'
end
end
local want_dow, op
local count, offset, operation
local ops = {
local ops = {
['>='] = { before = false, include = true  },
['>='] = { before = false, include = true  },
Line 495: Line 519:
}
}
if spec then
if spec then
if type(spec) ~= 'string' then
if type(spec) == 'number' then
return {}
count = floor(spec + 0.5)
end
if count < 0 then
for item in spec:gmatch('%S+') do
count = -count
if ops[item] then
operation = ops['<']
if op then
end
return {}
elseif type(spec) == 'string' then
end
local num, day, op = spec:match('^%s*(%d*)%s*(%a*)%s*([<>=]*)%s*$')
op = ops[item]
if not num then
else
return list
local dow = day_number(item)
end
if dow then
if num ~= '' then
if want_dow then
count = tonumber(num)
-- LATER Could handle more than one day, but probably not needed.
end
return {}
if day ~= '' then
end
local dow = day_number(day:gsub('[sS]$', '')) -- accept plural days
want_dow = dow
if not dow then
else
return list
return {}
end
end
offset = dow - date.dow
end
end
operation = ops[op]
else
return list
end
end
end
end
local offset = want_dow and want_dow - date.dow or 0
offset = offset or 0
op = op or ops['>=']
operation = operation or ops['>']
local first, last
local datefrom, dayfirst, daylast
if op.before then
if operation.before then
if offset >= 0 and not (op.include and offset == 0) then
if offset > 0 or (offset == 0 and not operation.include) then
offset = offset - 7
offset = offset - 7
end
end
last = date.day + offset
if count then
first = last % 7
if count > 1 then
if first == 0 then
offset = offset - 7*(count - 1)
first = 7
end
datefrom = date + offset
else
daylast = date.day + offset
dayfirst = daylast % 7
if dayfirst == 0 then
dayfirst = 7
end
end
end
else
else
if offset < 0 or (not op.include and offset == 0) then
if offset < 0 or (offset == 0 and not operation.include) then
offset = offset + 7
offset = offset + 7
end
end
first = date.day + offset
if count then
last = date.monthdays
datefrom = date + offset
else
dayfirst = date.day + offset
daylast = date.monthdays
end
end
if not count then
if daylast < dayfirst then
return list
end
count = floor((daylast - dayfirst)/7) + 1
datefrom = Date(date, {day = dayfirst})
end
end
local list = { text = _list_text }
local count = floor((last - first)/7) + 1
for i = 1, count do
for i = 1, count do
list[i] = Date(date, {day = first})
if not datefrom then break end  -- exceeds date limits
first = first + 7
list[i] = datefrom
datefrom = datefrom + 7
end
end
return list
return list
Line 760: Line 804:
m = m + 12
m = m + 12
end
end
local d = math.min(lhs.day, days_in_month(y, m, lhs.calname))
local d = math.min(lhs.day, days_in_month(y, m, lhs.calendar))
return Date(lhs, y, m, d)
return Date(lhs, y, m, d)
end
end
end
end
if is_diff(rhs) then
if is_diff(rhs) then
local days = rhs.daystotal
local days = rhs.age_days
if (is_sub or false) ~= (rhs.isnegative or false) then
if (is_sub or false) ~= (rhs.isnegative or false) then
days = -days
days = -days
Line 789: Line 833:
elseif key == 'dayofweekiso' then
elseif key == 'dayofweekiso' then
value = self.dowiso
value = self.dowiso
elseif key == 'doy' then
elseif key == 'dayofyear' then
local first = Date(self.year, 1, 1, self.calname).jdnoon
local first = Date(self.year, 1, 1, self.calendar).jdnoon
value = self.jdnoon - first + 1  -- day-of-year 1 to 366
value = self.jdnoon - first + 1  -- day-of-year 1 to 366
elseif key == 'dayofyear' then
value = self.doy
elseif key == 'era' then
elseif key == 'era' then
-- Era text (never a negative sign) from year and options.
-- Era text (never a negative sign) from year and options.
Line 811: Line 853:
value = floor(self.jd + 0.5)
value = floor(self.jd + 0.5)
elseif key == 'isleapyear' then
elseif key == 'isleapyear' then
value = is_leap_year(self.year, self.calname)
value = is_leap_year(self.year, self.calendar)
elseif key == 'monthabbr' then
elseif key == 'monthabbr' then
value = month_info[self.month][1]
value = month_info[self.month][1]
elseif key == 'monthdays' then
elseif key == 'monthdays' then
value = days_in_month(self.year, self.month, self.calname)
value = days_in_month(self.year, self.month, self.calendar)
elseif key == 'monthname' then
elseif key == 'monthname' then
value = month_info[self.month][2]
value = month_info[self.month][2]
Line 885: Line 927:
local newdate = {
local newdate = {
_id = uniq,
_id = uniq,
calname = 'Gregorian',  -- default is Gregorian calendar
calendar = 'Gregorian',  -- default is Gregorian calendar
hastime = false,  -- true if input sets a time
hastime = false,  -- true if input sets a time
hour = 0,  -- always set hour/minute/second so don't have to handle nil
hour = 0,  -- always set hour/minute/second so don't have to handle nil
minute = 0,
minute = 0,
second = 0,
second = 0,
options = make_option_table(),
options = {},
list = _make_list,
list = _date_list,
text = _date_text,
text = _date_text,
}
}
Line 904: Line 946:
-- Ignore empty arguments after stripping so modules can directly pass template parameters.
-- Ignore empty arguments after stripping so modules can directly pass template parameters.
elseif calendars[vlower] then
elseif calendars[vlower] then
newdate.calname = 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 911: Line 953:
end
end
is_copy = true
is_copy = true
newdate.calname = v.calname
newdate.calendar = v.calendar
newdate.hastime = v.hastime
newdate.hastime = v.hastime
newdate.options = v.options
newdate.options = v.options
Line 993: Line 1,035:
newdate.hastime = true
newdate.hastime = true
end
end
newdate.calname = 'Gregorian'  -- ignore any given calendar name
newdate.calendar = 'Gregorian'  -- ignore any given calendar name
elseif argtype == 'setdate' then
elseif argtype == 'setdate' then
if tnums or not set_date_from_numbers(newdate, numbers) then
if tnums or not set_date_from_numbers(newdate, numbers) then
Line 1,020: Line 1,062:
}
}
return setmetatable(readonly, mt)
return setmetatable(readonly, mt)
end
local function _diff_age(diff, code, options)
-- Return a tuple of values from diff as specified by code.
-- If options == 'duration', an extra day is added.
if not is_diff(diff) then
local f = options == 'duration' and 'duration' or 'age'
return 'Need a date difference (use "diff:' .. f .. '()" with a colon).'
end
local extra_day = options == 'duration' and 1 or 0
if code == 'wd' or code == 'w' or code == 'd' then
local d = diff.age_days + extra_day
if code == 'd' then
return d
end
local w = floor(d / 7)
if code == 'w' then
return w
end
return w, d % 7
end
local y, m, d = diff.years, diff.months, diff.days
if extra_day > 0 then
d = d + extra_day
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
if code == 'ymd' then
return y, m, d
end
if code == 'ym' then
return y, m
end
if code == 'm' then
return y * 12 + m
end
if code == 'ymwd' then
return y, m, floor(d / 7), d % 7
end
return y  -- default: assume code == 'y'; ignore invalid codes
end
local function _diff_duration(diff, code)
return _diff_age(diff, code, 'duration')
end
end


Line 1,028: Line 1,121:
end,
end,
__tostring = function (self)
__tostring = function (self)
return tostring(self.daystotal)
return tostring(self.age_days)
end,
end,
__index = function (self, key)
__index = function (self, key)
local value
local value
if key == 'daystotal' then
if key == 'age_days' then
value = self.date1.jdz - self.date2.jdz
value = self.date1.jdz - self.date2.jdz
end
end
Line 1,055: Line 1,148:
--    d = Date(2015,3,3) - Date(2015,1,31)
--    d = Date(2015,3,3) - Date(2015,1,31)
-- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
-- gives d.years, d.months, d.days = 0, 1, 3 (excluding date1).
if not (is_date(date1) and is_date(date2) and date1.calname == date2.calname) then
if not (is_date(date1) and is_date(date2) and date1.calendar == date2.calendar) then
return
return
end
end
Line 1,076: Line 1,169:
months = months - 1
months = months - 1
-- Get days in previous month (before the "to" date) given December has 31 days.
-- Get days in previous month (before the "to" date) given December has 31 days.
local dpm = m1 > 1 and days_in_month(y1, m1 - 1, date1.calname) 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
Line 1,099: Line 1,192:
seconds = S,
seconds = S,
isnegative = isnegative,
isnegative = isnegative,
age = _diff_age,
duration = _diff_duration,
}, diffmt)
}, diffmt)
end
end
Anonymous user
Cookies help us deliver our services. By using our services, you agree to our use of cookies.