notex.nvim/lua/notex/utils/date.lua

398 lines
9.4 KiB
Lua
Raw Permalink Normal View History

2025-10-05 20:16:33 -04:00
-- Date parsing and formatting utilities
local M = {}
-- Date format patterns
local DATE_PATTERNS = {
ISO_8601 = "^%d%d%d%d%-%d%d%-%d%d$",
ISO_8601_TIME = "^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%dZ?$",
ISO_8601_OFFSET = "^%d%d%d%d%-%d%d%-%d%dT%d%d:%d%d:%d%d[%+%-]%d%d:%d%d$",
RELATIVE = "^(%d+)([hdwmy])$",
NATURAL = "^(%d%d%d%d)%-(%d%d)%-(%d%d)$"
}
-- Month names
local MONTH_NAMES = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
}
local MONTH_SHORT = {
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
}
-- Parse date string to timestamp
function M.parse_date(date_string)
if not date_string or date_string == "" then
return nil
end
-- Handle relative dates
if date_string:match(DATE_PATTERNS.RELATIVE) then
return M.parse_relative_date(date_string)
end
-- Handle ISO 8601 with time
if date_string:match(DATE_PATTERNS.ISO_8601_TIME) then
return M.parse_iso8601_datetime(date_string)
end
-- Handle ISO 8601 with offset
if date_string:match(DATE_PATTERNS.ISO_8601_OFFSET) then
return M.parse_iso8601_with_offset(date_string)
end
-- Handle natural language dates
if date_string:match(DATE_PATTERNS.NATURAL) then
return M.parse_natural_date(date_string)
end
-- Handle ISO 8601 date only
if date_string:match(DATE_PATTERNS.ISO_8601) then
return M.parse_iso8601_date(date_string)
end
-- Handle common formats
return M.parse_common_formats(date_string)
end
-- Parse ISO 8601 date
function M.parse_iso8601_date(date_string)
local year, month, day = date_string:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)$")
if not year then
return nil
end
local timestamp = os.time({
year = tonumber(year),
month = tonumber(month),
day = tonumber(day),
hour = 0,
min = 0,
sec = 0
})
return timestamp
end
-- Parse ISO 8601 datetime
function M.parse_iso8601_datetime(date_string)
local year, month, day, hour, min, sec, timezone = date_string:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)(Z?)$")
if not year then
return nil
end
local timestamp = os.time({
year = tonumber(year),
month = tonumber(month),
day = tonumber(day),
hour = tonumber(hour),
min = tonumber(min),
sec = tonumber(sec)
})
return timestamp
end
-- Parse ISO 8601 with timezone offset
function M.parse_iso8601_with_offset(date_string)
local year, month, day, hour, min, sec, offset_sign, offset_hour, offset_min = date_string:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)T(%d%d):(%d%d):(%d%d)([%+%-])(%d%d):(%d%d)$")
if not year then
return nil
end
local timestamp = os.time({
year = tonumber(year),
month = tonumber(month),
day = tonumber(day),
hour = tonumber(hour),
min = tonumber(min),
sec = tonumber(sec)
})
-- Apply timezone offset (convert to UTC)
local offset_total = tonumber(offset_hour) * 3600 + tonumber(offset_min) * 60
if offset_sign == "-" then
offset_total = -offset_total
end
timestamp = timestamp - offset_total
return timestamp
end
-- Parse natural date
function M.parse_natural_date(date_string)
local year, month, day = date_string:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)$")
if not year then
return nil
end
-- Handle natural language month names
if not month then
local month_name, day_part, year_part = date_string:match("^(%a+)%s+(%d+)%s+(%d+)$")
if month_name and day_part and year_part then
month = M.get_month_number(month_name)
day = tonumber(day_part)
year = tonumber(year_part)
end
end
if year and month and day then
return os.time({
year = tonumber(year),
month = tonumber(month),
day = tonumber(day),
hour = 0,
min = 0,
sec = 0
})
end
return nil
end
-- Parse relative date
function M.parse_relative_date(date_string)
local amount, unit = date_string:match("^(%d+)([hdwmy])$")
if not amount or not unit then
return nil
end
local current_time = os.time()
amount = tonumber(amount)
local seconds = 0
if unit == "s" then
seconds = amount
elseif unit == "m" then
seconds = amount * 60
elseif unit == "h" then
seconds = amount * 3600
elseif unit == "d" then
seconds = amount * 86400
elseif unit == "w" then
seconds = amount * 604800
elseif unit == "m" then
seconds = amount * 2592000 -- 30 days
elseif unit == "y" then
seconds = amount * 31536000 -- 365 days
end
return current_time - seconds
end
-- Parse common formats
function M.parse_common_formats(date_string)
local formats = {
"^%d%d%d%d%/%d%d%/%d%d$", -- MM/DD/YYYY
"^%d%d%/%d%d%/%d%d%d%d$", -- M/D/YYYY
"^%d%d%-%d%d%-%d%d%d%d$", -- MM-DD-YYYY
}
for _, pattern in ipairs(formats) do
if date_string:match(pattern) then
-- Try to parse with Lua's built-in date parsing
local timestamp = os.time({
year = tonumber(date_string:sub(-4)),
month = tonumber(date_string:sub(1, 2)),
day = tonumber(date_string:sub(4, 5)),
hour = 0,
min = 0,
sec = 0
})
if timestamp > 0 then
return timestamp
end
end
end
return nil
end
-- Format timestamp to string
function M.format_date(timestamp, format)
format = format or "%Y-%m-%d"
if not timestamp then
return ""
end
return os.date(format, timestamp)
end
-- Get relative time string
function M.get_relative_time(timestamp)
local current_time = os.time()
local diff = current_time - timestamp
if diff < 60 then
return "just now"
elseif diff < 3600 then
local minutes = math.floor(diff / 60)
return string.format("%d minute%s ago", minutes, minutes > 1 and "s" or "")
elseif diff < 86400 then
local hours = math.floor(diff / 3600)
return string.format("%d hour%s ago", hours, hours > 1 and "s" or "")
elseif diff < 2592000 then
local days = math.floor(diff / 86400)
return string.format("%d day%s ago", days, days > 1 and "s" or "")
elseif diff < 31536000 then
local months = math.floor(diff / 2592000)
return string.format("%d month%s ago", months, months > 1 and "s" or "")
else
local years = math.floor(diff / 31536000)
return string.format("%d year%s ago", years, years > 1 and "s" or "")
end
end
-- Get month number from name
function M.get_month_number(month_name)
local lower_name = month_name:lower()
for i, name in ipairs(MONTH_NAMES) do
if name:lower() == lower_name then
return i
end
end
for i, name in ipairs(MONTH_SHORT) do
if name:lower() == lower_name then
return i
end
end
return nil
end
-- Get month name from number
function M.get_month_name(month_number, short)
if month_number < 1 or month_number > 12 then
return nil
end
if short then
return MONTH_SHORT[month_number]
else
return MONTH_NAMES[month_number]
end
end
-- Validate date string
function M.is_valid_date(date_string)
return M.parse_date(date_string) ~= nil
end
-- Add time to timestamp
function M.add_time(timestamp, amount, unit)
unit = unit or "days"
local seconds = 0
if unit == "seconds" then
seconds = amount
elseif unit == "minutes" then
seconds = amount * 60
elseif unit == "hours" then
seconds = amount * 3600
elseif unit == "days" then
seconds = amount * 86400
elseif unit == "weeks" then
seconds = amount * 604800
elseif unit == "months" then
seconds = amount * 2592000
elseif unit == "years" then
seconds = amount * 31536000
end
return timestamp + seconds
end
-- Get date range
function M.get_date_range(start_date, end_date)
local start_timestamp = M.parse_date(start_date)
local end_timestamp = M.parse_date(end_date)
if not start_timestamp or not end_timestamp then
return nil
end
return {
start_timestamp = start_timestamp,
end_timestamp = end_timestamp,
start_formatted = M.format_date(start_timestamp),
end_formatted = M.format_date(end_timestamp),
duration_days = math.floor((end_timestamp - start_timestamp) / 86400)
}
end
-- Get week start/end
function M.get_week_bounds(timestamp)
timestamp = timestamp or os.time()
local date_table = os.date("*t", timestamp)
local day_of_week = date_table.wday -- Sunday = 1, Monday = 2, etc.
-- Adjust to Monday = 0
day_of_week = (day_of_week + 5) % 7
local week_start = timestamp - (day_of_week * 86400)
local week_end = week_start + (6 * 86400)
return {
start_timestamp = week_start,
end_timestamp = week_end,
start_formatted = M.format_date(week_start),
end_formatted = M.format_date(week_end)
}
end
-- Get month start/end
function M.get_month_bounds(timestamp)
timestamp = timestamp or os.time()
local date_table = os.date("*t", timestamp)
local month_start = os.time({
year = date_table.year,
month = date_table.month,
day = 1,
hour = 0,
min = 0,
sec = 0
})
local next_month = os.time({
year = date_table.year,
month = date_table.month + 1,
day = 1,
hour = 0,
min = 0,
sec = 0
})
local month_end = next_month - 1
return {
start_timestamp = month_start,
end_timestamp = month_end,
start_formatted = M.format_date(month_start),
end_formatted = M.format_date(month_end)
}
end
-- Get time zones
function M.get_timezones()
return {
"UTC",
"America/New_York",
"America/Chicago",
"America/Denver",
"America/Los_Angeles",
"Europe/London",
"Europe/Paris",
"Asia/Tokyo",
"Australia/Sydney"
}
end
return M