--- Date and Date Format classes.
-- See @{05-dates.md|the Guide}.
--
-- Dependencies: `pl.class`, `pl.stringx`
-- @module pl.Date
-- @pragma nostrip
local class = require 'pl.class'
local os_time, os_date = os.time, os.date
local stringx = require 'pl.stringx'
local utils = require 'pl.utils'
local assert_arg,assert_string,raise = utils.assert_arg,utils.assert_string,utils.raise
local Date = class()
Date.Format = class()
--- Date constructor.
-- @param t this can be either
--
-- * `nil` or empty - use current date and time
-- * number - seconds since epoch (as returned by @{os.time})
-- * `Date` - copy constructor
-- * table - table containing year, month, etc as for `os.time`. You may leave out year, month or day,
-- in which case current values will be used.
-- *three to six numbers: year, month, day, hour, min, sec
--
-- @function Date
function Date:_init(t,...)
local time
if select('#',...) > 2 then
local extra = {...}
local year = t
t = {
year = year,
month = extra[1],
day = extra[2],
hour = extra[3],
min = extra[4],
sec = extra[5]
}
end
if t == nil then
time = os_time()
elseif type(t) == 'number' then
time = t
local next = ...
self.interval = next == true or next == 'interval'
elseif type(t) == 'table' then
if getmetatable(t) == Date then -- copy ctor
time = t.time
else
if not (t.year and t.month and t.year) then
local lt = os.date('*t')
if not t.year and not t.month and not t.day then
t.year = lt.year
t.month = lt.month
t.day = lt.day
else
t.year = t.year or lt.year
t.month = t.month or (t.day and lt.month or 1)
t.day = t.day or 1
end
end
time = os_time(t)
end
end
self:set(time)
end
local tzone_
--- get the time zone offset from UTC.
-- @return seconds ahead of UTC
function Date.tzone ()
if not tzone_ then
local now = os.time()
local utc = os.date('!*t',now)
local lcl = os.date('*t',now)
local unow = os.time(utc)
tzone_ = os.difftime(now,unow)
if lcl.isdst then
if tzone_ > 0 then
tzone_ = tzone_ - 3600
else
tzone_ = tzone_ + 3600
end
end
end
return tzone_
end
--- convert this date to UTC.
function Date:toUTC ()
self:add { sec = -Date.tzone() }
end
--- convert this UTC date to local.
function Date:toLocal ()
self:add { sec = Date.tzone() }
end
--- set the current time of this Date object.
-- @param t seconds since epoch
function Date:set(t)
self.time = t
if self.interval then
self.tab = os_date('!*t',self.time)
else
self.tab = os_date('*t',self.time)
end
end
--- set the year.
-- @param y Four-digit year
-- @class function
-- @name Date:year
--- set the month.
-- @param m month
-- @class function
-- @name Date:month
--- set the day.
-- @param d day
-- @class function
-- @name Date:day
--- set the hour.
-- @param h hour
-- @class function
-- @name Date:hour
--- set the minutes.
-- @param min minutes
-- @class function
-- @name Date:min
--- set the seconds.
-- @param sec seconds
-- @class function
-- @name Date:sec
--- set the day of year.
-- @class function
-- @param yday day of year
-- @name Date:yday
--- get the year.
-- @param y Four-digit year
-- @class function
-- @name Date:year
--- get the month.
-- @class function
-- @name Date:month
--- get the day.
-- @class function
-- @name Date:day
--- get the hour.
-- @class function
-- @name Date:hour
--- get the minutes.
-- @class function
-- @name Date:min
--- get the seconds.
-- @class function
-- @name Date:sec
--- get the day of year.
-- @class function
-- @name Date:yday
for _,c in ipairs{'year','month','day','hour','min','sec','yday'} do
Date[c] = function(self,val)
if val then
assert_arg(1,val,"number")
self.tab[c] = val
self:set(os_time(self.tab))
return self
else
return self.tab[c]
end
end
end
--- name of day of week.
-- @param full abbreviated if true, full otherwise.
-- @return string name
function Date:weekday_name(full)
return os_date(full and '%A' or '%a',self.time)
end
--- name of month.
-- @param full abbreviated if true, full otherwise.
-- @return string name
function Date:month_name(full)
return os_date(full and '%B' or '%b',self.time)
end
--- is this day on a weekend?.
function Date:is_weekend()
return self.tab.wday == 0 or self.tab.wday == 6
end
--- add to a date object.
-- @param t a table containing one of the following keys and a value:
-- year,month,day,hour,min,sec
-- @return this date
function Date:add(t)
local key,val = next(t)
self.tab[key] = self.tab[key] + val
self:set(os_time(self.tab))
return self
end
--- last day of the month.
-- @return int day
function Date:last_day()
local d = 28
local m = self.tab.month
while self.tab.month == m do
d = d + 1
self:add{day=1}
end
self:add{day=-1}
return self
end
--- difference between two Date objects.
-- Note: currently the result is a regular @{Date} object,
-- but also has `interval` field set, which means a more
-- appropriate string rep is used.
-- @param other Date object
-- @return a Date object
function Date:diff(other)
local dt = self.time - other.time
if dt < 0 then error("date difference is negative!",2) end
return Date(dt,true)
end
--- long numerical ISO data format version of this date.
-- If it's an interval then the format is '2 hours 29 sec' etc.
function Date:__tostring()
if not self.interval then
return os_date('%Y-%m-%d %H:%M:%S',self.time)
else
local t, res = self.tab, ''
local y,m,d = t.year - 1970, t.month - 1, t.day - 1
if y > 0 then res = res .. y .. ' years ' end
if m > 0 then res = res .. m .. ' months ' end
if d > 0 then res = res .. d .. ' days ' end
if y == 0 and m == 0 then
local h = t.hour
if h > 0 then res = res .. h .. ' hours ' end
if t.min > 0 then res = res .. t.min .. ' min ' end
if t.sec > 0 then res = res .. t.sec .. ' sec ' end
end
return res
end
end
--- equality between Date objects.
function Date:__eq(other)
return self.time == other.time
end
--- equality between Date objects.
function Date:__lt(other)
return self.time < other.time
end
------------ Date.Format class: parsing and renderinig dates ------------
-- short field names, explicit os.date names, and a mask for allowed field repeats
local formats = {
d = {'day',{true,true}},
y = {'year',{false,true,false,true}},
m = {'month',{true,true}},
H = {'hour',{true,true}},
M = {'min',{true,true}},
S = {'sec',{true,true}},
}
--
--- Date.Format constructor.
-- @param fmt. A string where the following fields are significant: