feat: time-aware due dates, persistent undo, @return audit
Problem: Due dates had no time component, the undo stack was lost on restart and stored in a separate file, and many public functions lacked required @return annotations. Solution: Add YYYY-MM-DDThh:mm support across parse, views, recur, complete, and init with time-aware overdue checks. Merge the undo stack into the task store JSON so a single file holds all state. Add @return nil annotations to all 27 void public functions across every module.
This commit is contained in:
parent
c69afacc87
commit
ee2d125846
11 changed files with 369 additions and 118 deletions
|
|
@ -24,6 +24,28 @@ local function is_valid_date(s)
|
|||
return check.year == yn and check.month == mn and check.day == dn
|
||||
end
|
||||
|
||||
---@param s string
|
||||
---@return boolean
|
||||
local function is_valid_time(s)
|
||||
local h, m = s:match('^(%d%d):(%d%d)$')
|
||||
if not h then
|
||||
return false
|
||||
end
|
||||
local hn = tonumber(h) --[[@as integer]]
|
||||
local mn = tonumber(m) --[[@as integer]]
|
||||
return hn >= 0 and hn <= 23 and mn >= 0 and mn <= 59
|
||||
end
|
||||
|
||||
---@param s string
|
||||
---@return boolean
|
||||
local function is_valid_datetime(s)
|
||||
local date_part, time_part = s:match('^(.+)T(.+)$')
|
||||
if not date_part then
|
||||
return is_valid_date(s)
|
||||
end
|
||||
return is_valid_date(date_part) and is_valid_time(time_part)
|
||||
end
|
||||
|
||||
---@return string
|
||||
local function date_key()
|
||||
return config.get().date_syntax or 'due'
|
||||
|
|
@ -65,146 +87,217 @@ local function today_str(today)
|
|||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day })) --[[@as string]]
|
||||
end
|
||||
|
||||
---@param date_part string
|
||||
---@param time_suffix? string
|
||||
---@return string
|
||||
local function append_time(date_part, time_suffix)
|
||||
if time_suffix then
|
||||
return date_part .. 'T' .. time_suffix
|
||||
end
|
||||
return date_part
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@return string|nil
|
||||
function M.resolve_date(text)
|
||||
local lower = text:lower()
|
||||
local date_input, time_suffix = text:match('^(.+)@(%d%d:%d%d)$')
|
||||
if time_suffix then
|
||||
if not is_valid_time(time_suffix) then
|
||||
return nil
|
||||
end
|
||||
else
|
||||
date_input = text
|
||||
end
|
||||
|
||||
local dt = date_input:match('^(%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d)$')
|
||||
if dt then
|
||||
local dp, tp = dt:match('^(.+)T(.+)$')
|
||||
if is_valid_date(dp) and is_valid_time(tp) then
|
||||
return dt
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if is_valid_date(date_input) then
|
||||
return append_time(date_input, time_suffix)
|
||||
end
|
||||
|
||||
local lower = date_input:lower()
|
||||
local today = os.date('*t') --[[@as osdate]]
|
||||
|
||||
if lower == 'today' or lower == 'eod' then
|
||||
return today_str(today)
|
||||
return append_time(today_str(today), time_suffix)
|
||||
end
|
||||
|
||||
if lower == 'yesterday' then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day - 1 })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day - 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'tomorrow' then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + 1 })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = today.day + 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'sow' then
|
||||
local delta = -((today.wday - 2) % 7)
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eow' then
|
||||
local delta = (1 - today.wday) % 7
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'som' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eom' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = today.month + 1, day = 0 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'soq' then
|
||||
local q = math.ceil(today.month / 3)
|
||||
local first_month = (q - 1) * 3 + 1
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = first_month, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = first_month, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eoq' then
|
||||
local q = math.ceil(today.month / 3)
|
||||
local last_month = q * 3
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = last_month + 1, day = 0 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'soy' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = 1, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = 1, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'eoy' then
|
||||
return os.date('%Y-%m-%d', os.time({ year = today.year, month = 12, day = 31 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = today.year, month = 12, day = 31 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
if lower == 'later' or lower == 'someday' then
|
||||
return config.get().someday_date
|
||||
return append_time(config.get().someday_date, time_suffix)
|
||||
end
|
||||
|
||||
local n = lower:match('^%+(%d+)d$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%+(%d+)w$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%+(%d+)m$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
day = today.day,
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month + (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
day = today.day,
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%-(%d+)d$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
),
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
n = lower:match('^%-(%d+)w$')
|
||||
if n then
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({
|
||||
year = today.year,
|
||||
month = today.month,
|
||||
day = today.day - (
|
||||
tonumber(n) --[[@as integer]]
|
||||
) * 7,
|
||||
})
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
local ord = lower:match('^(%d+)[snrt][tdh]$')
|
||||
|
|
@ -222,7 +315,7 @@ function M.resolve_date(text)
|
|||
local t = os.time({ year = y, month = m, day = day_num })
|
||||
local check = os.date('*t', t) --[[@as osdate]]
|
||||
if check.day == day_num then
|
||||
return os.date('%Y-%m-%d', t) --[[@as string]]
|
||||
return append_time(os.date('%Y-%m-%d', t) --[[@as string]], time_suffix)
|
||||
end
|
||||
m = m + 1
|
||||
if m > 12 then
|
||||
|
|
@ -232,7 +325,7 @@ function M.resolve_date(text)
|
|||
t = os.time({ year = y, month = m, day = day_num })
|
||||
check = os.date('*t', t) --[[@as osdate]]
|
||||
if check.day == day_num then
|
||||
return os.date('%Y-%m-%d', t) --[[@as string]]
|
||||
return append_time(os.date('%Y-%m-%d', t) --[[@as string]], time_suffix)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
|
@ -244,17 +337,23 @@ function M.resolve_date(text)
|
|||
if today.month >= target_month then
|
||||
y = y + 1
|
||||
end
|
||||
return os.date('%Y-%m-%d', os.time({ year = y, month = target_month, day = 1 })) --[[@as string]]
|
||||
return append_time(
|
||||
os.date('%Y-%m-%d', os.time({ year = y, month = target_month, day = 1 })) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
local target_wday = weekday_map[lower]
|
||||
if target_wday then
|
||||
local current_wday = today.wday
|
||||
local delta = (target_wday - current_wday) % 7
|
||||
return os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]]
|
||||
return append_time(
|
||||
os.date(
|
||||
'%Y-%m-%d',
|
||||
os.time({ year = today.year, month = today.month, day = today.day + delta })
|
||||
) --[[@as string]],
|
||||
time_suffix
|
||||
)
|
||||
end
|
||||
|
||||
return nil
|
||||
|
|
@ -273,7 +372,7 @@ function M.body(text)
|
|||
local i = #tokens
|
||||
local dk = date_key()
|
||||
local rk = recur_key()
|
||||
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d)$'
|
||||
local date_pattern_strict = '^' .. vim.pesc(dk) .. ':(%d%d%d%d%-%d%d%-%d%d[T%d:]*)$'
|
||||
local date_pattern_any = '^' .. vim.pesc(dk) .. ':(.+)$'
|
||||
local rec_pattern = '^' .. vim.pesc(rk) .. ':(%S+)$'
|
||||
|
||||
|
|
@ -284,7 +383,7 @@ function M.body(text)
|
|||
if metadata.due then
|
||||
break
|
||||
end
|
||||
if not is_valid_date(due_val) then
|
||||
if not is_valid_datetime(due_val) then
|
||||
break
|
||||
end
|
||||
metadata.due = due_val
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue