Skip to content

Commit

Permalink
Fix date arithmetic issues with month (#748)
Browse files Browse the repository at this point in the history
* test(date): expose bug with test

* fix(date): handle end of month correctly

- handle 30d month after 31d month correctly
- handle February correctly including leap years

---------

Co-authored-by: Sebastian Flügge <[email protected]>
  • Loading branch information
seflue and seflue authored Jun 5, 2024
1 parent c34ae3c commit 1fbc263
Show file tree
Hide file tree
Showing 2 changed files with 81 additions and 1 deletion.
26 changes: 25 additions & 1 deletion lua/orgmode/objects/date.lua
Original file line number Diff line number Diff line change
Expand Up @@ -481,7 +481,8 @@ function Date:end_of(span)
end

if span == 'month' then
return self:add({ month = 1 }):start_of('month'):adjust('-1d'):end_of('day')
local date = os.date('*t', self.timestamp)
return self:set({ day = Date._days_of_month(date) }):end_of('day')
end

return self
Expand Down Expand Up @@ -517,6 +518,7 @@ end
---@return OrgDate
function Date:add(opts)
opts = opts or {}
---@type table
local date = os.date('*t', self.timestamp)
for opt, val in pairs(opts) do
if opt == 'week' then
Expand All @@ -525,9 +527,31 @@ function Date:add(opts)
end
date[opt] = date[opt] + val
end
if opts['month'] then
date['day'] = math.min(date['day'], Date._days_of_month(date))
end
return self:from_time_table(date)
end

---@param date table
---@return number
function Date._days_of_month(date)
local days_of = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
if date.month == 2 then
return Date._days_of_february(date.year)
else
return days_of[date.month]
end
end

function Date._days_of_february(year)
return Date._is_leap_year(year) and 29 or 28
end

function Date._is_leap_year(year)
return year % 400 == 0 or (year % 100 ~= 0 and year % 4 == 0)
end

---@param opts table
---@return OrgDate
function Date:subtract(opts)
Expand Down
56 changes: 56 additions & 0 deletions tests/plenary/object/date_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,14 @@ describe('Date object', function()
assert.are.same('2021-05-31 Mon 23:59', date:to_string())
end)

it('should properly handle end of month', function()
local date = Date.from_string('2021-05-12')
date = date:end_of('month')
assert.are.same('2021-05-31 Mon', date:to_string())
date = date:end_of('month')
assert.are.same('2021-05-31 Mon', date:to_string())
end)

it('should add/subtract/set date', function()
local date = Date.from_string('2021-05-12 14:00')
date = date:add({ week = 2 })
Expand Down Expand Up @@ -810,4 +818,52 @@ describe('Date object', function()
local end_of_2021 = Date.from_string('2021-12-31')
assert.are.same('52', end_of_2021:get_week_number())
end)

it('should add month correctly | long month + short month', function()
local date = Date.from_string('2021-05-31')
assert.are.same('2021-05-31 Mon', date:to_string())
assert.are.same('2021-06-30 Wed', date:add({ month = 1 }):to_string())
end)

it('should add month correctly | short month + long month', function()
local date = Date.from_string('2021-04-30')
assert.are.same('2021-04-30 Fri', date:to_string())
assert.are.same('2021-05-30 Sun', date:add({ month = 1 }):to_string())
end)

it('should add month correctly | long month + february', function()
local date = Date.from_string('2021-01-31')
assert.are.same('2021-01-31 Sun', date:to_string())
assert.are.same('2021-02-28 Sun', date:add({ month = 1 }):to_string())
end)

it('should add month correctly | long month + february in leap year', function()
local date = Date.from_string('2024-01-31')
assert.are.same('2024-01-31 Wed', date:to_string())
assert.are.same('2024-02-29 Thu', date:add({ month = 1 }):to_string())
end)

it('should calculate end of month correctly | long month', function()
local date = Date.from_string('2021-05-31')
assert.are.same('2021-05-31 Mon', date:to_string())
assert.are.same('2021-05-31 Mon', date:end_of('month'):to_string())
end)

it('should calculate end of month correctly | short month', function()
local date = Date.from_string('2021-04-30')
assert.are.same('2021-04-30 Fri', date:to_string())
assert.are.same('2021-04-30 Fri', date:end_of('month'):to_string())
end)

it('should calculate end of month correctly | february', function()
local date = Date.from_string('2021-02-28')
assert.are.same('2021-02-28 Sun', date:to_string())
assert.are.same('2021-02-28 Sun', date:end_of('month'):to_string())
end)

it('should calculate end of month correctly | february leap-year', function()
local date = Date.from_string('2024-02-29')
assert.are.same('2024-02-29 Thu', date:to_string())
assert.are.same('2024-02-29 Thu', date:end_of('month'):to_string())
end)
end)

0 comments on commit 1fbc263

Please sign in to comment.