[Date Prev][Date Next][Thread Prev][Thread Next]
[Date Index]
[Thread Index]
- Subject: Re: date arithmetic?
- From: Philippe Lhoste <PhiLho@...>
- Date: Wed, 24 Aug 2005 11:32:35 +0200
PA wrote:
How does one do date arithmetic in Lua?
Some time ago, I save Philipp Janda's date class, and slightly improved
it. I didn't used it much since then, but I keep it around just in case :-)
Here is my version, the link to Philipp's home page is inside.
I send it to the list, because I believe it can be of interest for other
people. The file is quite small.
HTH.
--
Philippe Lhoste
-- (near) Paris -- France
-- http://Phi.Lho.free.fr
-- -- -- -- -- -- -- -- -- -- -- -- -- --
-- Simple class for date/time calculations in Lua 5.0
-- author: Philipp Janda <philipp.janda@web.de>
-- http://www.ratnet.stw.uni-erlangen.de/~siphjand/lua50/date.lua
--
-- The algorithms for date calculations were from here:
-- http://home.capecod.net/~pbaum/date/date0.htm
-- (PL) which is now located there:
-- http://vsg.cape.com/~pbaum/date/date0.htm
--
-- Some minor improvements (?) by Philippe Lhoste:
-- - Accepts '/' as date separator and use it in the default __tostring metamethod.
-- - Weekday member becomes pure number, it is up to the user to convert it
-- to string using its own, localized, table.
-- Usage:
-- Create a date object using e.g.:
-- local d1 = date:parse("31-12-2000")
-- local d2 = date:parse("29_2_2000/10:52:44")
-- local d3 = date:parse("01.10.1582 11:11:11")
-- local d4 = date:now()
-- -- ...
-- Date delimiters can be `/', `.', `_' or `-'. The only allowed time
-- delimiter is `:'. Between date and time strings there must be
-- exactly one (arbitrary) character.
--
-- You can output the date using:
-- print(d1)
--
-- This is mainly for debugging. If you need finer control, you
-- will probably use the member variables of the date object to
-- do your own formatting:
-- print(d1.weekday, d1.day, d1.month, d1.year)
-- print(d1.hour, d1.minute, d1.second)
--
-- You can do date calculations assigning to the members of the
-- date objects. E.g.:
-- d1.day = d1.day + 10 -- add 10 days to date d1
-- d1.year = d1.year - 20 -- 20 years ago...
-- -- ...
-- Note that you cannot assign to the weekday member of date objects!
-- You can also calculate the number of seconds between two dates:
-- local nsecs = d1 - d2
-- Unresolved issues:
-- There are some unintuitive behaviours when subtracting months
-- (or maybe even leap years), e.g.:
-- local d = date:parse("31.3.2003")
-- d.month = d.month - 1
-- print(d)
-- --> 03.03.2003/00:00:00
-- 31.03.2003 minus one month is the 31.02.2003, but this date
-- doesn't exist, so we get 3 days after the 28th of february, which
-- is 03.03.2003!
-- This isn't beautiful, but kind of logic. I don't known if I should
-- change this behaviour since it is kind of implicit in the
-- calculation formulas.
local Public, Private, Meta = {}, {}, {}
date = Public
----------------------------------------------------------------------
-- Public calculation functions
function Public.gregorian2daynumber(d, m, y)
if m < 3 then
m = m + 12
y = y - 1
end
local a = math.floor((153*m - 457) / 5)
local b = math.floor(y / 4)
local c = math.floor(y / 100)
local e = math.floor(y / 400)
return d + a + 365*y + b - c + e + 1721118.5
end
function Public.daynumber2gregorian(jdn)
local temp = jdn - 1721118.5
local z = math.floor(temp)
local r = temp - z
local g = z - 0.25
local a = math.floor(g / 36524.25)
local b = a - math.floor(a / 4)
local year = math.floor((b + g) / 365.25)
local c = b + z - math.floor( 365.25 * year)
local month = math.trunc((5*c + 456) / 153)
local day = c - math.trunc((153*month - 457) / 5) + r
if month > 12 then
year = year + 1
month = month - 12
end
return day, month, year
end
function Public.julian2daynumber(d, m, y)
if m < 3 then
m = m + 12
y = y - 1
end
local a = math.floor((153*m - 457) / 5)
local b = math.floor(y / 4)
return d + a + 365*y + b + 1721116.5
end
function Public.daynumber2julian(jdn)
local temp = jdn - 1721116.5
local z = math.floor(temp)
local r = temp - z
local year = math.floor((z - 0.25) / 365.25)
local c = z - math.floor(365.25 * year)
local month = math.trunc((5*c + 456) / 153)
local day = c - math.trunc((153*month - 457) / 5) + r
if month > 12 then
year = year + 1
month = month - 12
end
return day, month, year
end
function Public.date2daynumber(day, month, year)
if year > 1582 or
(year == 1582 and month > 10) or
(year == 1582 and month == 10 and day > 14) then
return Public.gregorian2daynumber(day, month, year)
else
return Public.julian2daynumber(day, month, year)
end
end
function Public.daynumber2date(jdn)
local day, month, year
if jdn > 2299160 then
day, month, year = Public.daynumber2gregorian(jdn)
else
day, month, year = Public.daynumber2julian(jdn)
end
return day, month, year
end
----------------------------------------------------------------------
-- Private calculation functions
local seconds_per_minute = 60
local seconds_per_hour = 60 * seconds_per_minute
local seconds_per_day = 24 * seconds_per_hour
--Private.weekday = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" }
function Private.update(data)
local ticks = data.ticks
local jdn = ticks / seconds_per_day
jdn_int = math.floor(jdn)
local jdn_midnight
if jdn - jdn_int >= 0.5 then
jdn_midnight = jdn_int + 0.5
else
jdn_midnight = jdn_int - 0.5
end
local second = ticks - jdn_midnight*seconds_per_day
-- compute the time since midnight
local hour = math.floor(second / seconds_per_hour)
second = second - hour*seconds_per_hour
local minute = math.floor(second / seconds_per_minute)
second = second - minute*seconds_per_minute
-- compute the date
local day, month, year = Public.daynumber2date(jdn_midnight)
-- set the values for the DateTime object
data.second = second
data.minute = minute
data.hour = hour
data.day = day
data.month = month
data.year = year
-- data.weekday = Private.weekday[math.mod(jdn_midnight+1, 7)+0.5]
data.weekday = math.floor(math.mod(jdn_midnight+1, 7)+0.5)
end
----------------------------------------------------------------------
-- the constructors
-- normal constructor
function Public:new(year, month, day, hour, minute, second)
-- parameters
year = tonumber(year) or 1970
month = tonumber(month) or 1
day = tonumber(day) or 1
hour = tonumber(hour) or 0
minute = tonumber(minute) or 0
second = tonumber(second) or 0
-- calculate seconds
local sex = Public.date2daynumber(day, month, year) * seconds_per_day
sex = sex + hour*seconds_per_hour + minute*seconds_per_minute + second
local obj = {}
local data = {
ticks = sex,
}
-- compute the missing fields in data table
Private.update(data)
local meta = {
-- metamethods
__index = data, -- just return the data (or methods...)
__newindex = Meta.__newindex,
__tostring = Meta.__tostring,
__sub = Meta.__sub,
__le = Meta.__le,
__lt = Meta.__lt,
__eq = Meta.__eq
}
setmetatable(obj, meta)
return obj
end
-- parses a given date string and creates a DateTime object
function Public:parse(datestr)
local year, month, day, hour, minute, second, i, j, _
local datepattern = "^(%d+)[%.%-%/_](%d+)[%.%-%/_](%d+)"
local timepattern = "^(%d+):(%d+):(%d+)"
-- parse date
i, j, day, month, year = string.find(datestr, datepattern)
if not i then -- no date found, assume today
_, _, day, month, year = string.find(
os.date("%d.%m.%Y"),
datepattern
)
j = 1
else
j = j + 2 -- skip delimiter
end
-- parse time
_, _, hour, minute, second = string.find(datestr, timepattern, j)
if not hour then
second = 0
_, _, hour, minute = string.find(datestr, "^(%d+):(%d+)$", j)
end
return Public.new(self, year, month, day, hour, minute, second)
end
-- makes a DateTime object from the current date and time
function Public:now()
local year, month, day, hour, minute, second, _
local patt = "^(%d+)%.(%d+)%.(%d+)/(%d+):(%d+):(%d+)$"
_, _, day, month, year, hour, minute, second = string.find(
os.date("%d.%m.%Y/%H:%M:%S"),
patt
)
return Public.new(self, year, month, day, hour, minute, second)
end
----------------------------------------------------------------------
-- some member functions
-- return a string representation
function Meta.__tostring(self)
local data = getmetatable(self).__index
return string.format("%02d/%02d/%04d %02d:%02d:%02d",
data.day, data.month, data.year,
data.hour, data.minute, data.second)
end
-- compute the time difference in seconds...
function Meta.__sub(a, b)
if type(a) == "table" and type(a.ticks) == "number" and
type(b) == "table" and type(b.ticks) == "number" then
return a.ticks - b.ticks
else
error("can only subtract Date objects")
end
end
-- compare two Date/Time objects
function Meta.__le(a, b)
if type(a) == "table" and type(a.ticks) == "number" and
type(b) == "table" and type(b.ticks) == "number" then
return a.ticks <= b.ticks
else
error("can only compare two Date objects")
end
end
-- compare two Date/Time objects
function Meta.__lt(a, b)
if type(a) == "table" and type(a.ticks) == "number" and
type(b) == "table" and type(b.ticks) == "number" then
return a.ticks < b.ticks
else
error("can only compare two Date objects")
end
end
-- compare two Date/Time objects
function Meta.__eq(a, b)
if type(a) == "table" and type(a.ticks) == "number" and
type(b) == "table" and type(b.ticks) == "number" then
return a.ticks == b.ticks
else
error("can only compare two Date objects")
end
end
-- set a field, but update the ticks counter and all fields
function Meta.__newindex(self, key, value)
local data = getmetatable(self).__index
if key == "ticks" then
data.ticks = value
elseif key == "second" then
data.ticks = data.ticks + (value - data.second)
elseif key == "minute" then
data.ticks = data.ticks + (value - data.minute)*seconds_per_minute
elseif key == "hour" then
data.ticks = data.ticks + (value - data.hour)*seconds_per_hour
elseif key == "day" then
data.ticks = data.ticks + (value - data.day)*seconds_per_day
elseif key == "month" then
local hour, minute, second = data.hour, data.minute, data.second
local day, month, year = data.day, value, data.year
local addyears = math.floor(month / 12)
month = month - addyears*12
year = year + addyears
if month == 0 then
year = year - 1
month = 12
end
local sex = Public.date2daynumber(day, month, year) * seconds_per_day
sex = sex + hour*seconds_per_hour + minute*seconds_per_minute + second
data.ticks = sex
elseif key == "year" then
local day, month, year = data.day, data.month, value
local hour, minute, second = data.hour, data.minute, data.second
local sex = Public.date2daynumber(day, month, year) * seconds_per_day
sex = sex + hour*seconds_per_hour + minute*seconds_per_minute + second
data.ticks = sex
elseif key == "weekday" then
error("cannot set weekday for Date object")
else
rawset(self, key, value)
return
end
Private.update(data)
end
----------------------------------------------------------------------
-- a helper function for the math library
function math.trunc(number)
if number < 0 then
return -math.floor(-number)
else
return math.floor(number)
end
end