lua-users home
lua-l archive

[Date Prev][Date Next][Thread Prev][Thread Next] [Date Index] [Thread Index]


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