@jcleigh/clickup-mcp-server
Version:
ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol
289 lines (288 loc) • 10.7 kB
JavaScript
/**
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
* SPDX-License-Identifier: MIT
*
* Date Utility Functions
*
* This module provides utilities for handling dates, timestamps, and due date parsing.
*/
import { Logger } from '../logger.js';
// Create a logger instance for date utilities
const logger = new Logger('DateUtils');
/**
* Get a timestamp for a relative time
*
* @param minutes Minutes from now
* @param hours Hours from now
* @param days Days from now
* @param weeks Weeks from now
* @param months Months from now
* @returns Timestamp in milliseconds
*/
export function getRelativeTimestamp(minutes = 0, hours = 0, days = 0, weeks = 0, months = 0) {
const now = new Date();
if (minutes)
now.setMinutes(now.getMinutes() + minutes);
if (hours)
now.setHours(now.getHours() + hours);
if (days)
now.setDate(now.getDate() + days);
if (weeks)
now.setDate(now.getDate() + (weeks * 7));
if (months)
now.setMonth(now.getMonth() + months);
return now.getTime();
}
/**
* Get the start of today (midnight) in Unix milliseconds
* @returns Timestamp in milliseconds for start of current day
*/
export function getStartOfDay() {
const now = new Date();
now.setHours(0, 0, 0, 0);
return now.getTime();
}
/**
* Get the end of today (23:59:59.999) in Unix milliseconds
* @returns Timestamp in milliseconds for end of current day
*/
export function getEndOfDay() {
const now = new Date();
now.setHours(23, 59, 59, 999);
return now.getTime();
}
/**
* Get the current time in Unix milliseconds
* @returns Current timestamp in milliseconds
*/
export function getCurrentTimestamp() {
return new Date().getTime();
}
/**
* Parse a due date string into a timestamp
* Supports ISO 8601 format or natural language like "tomorrow"
*
* @param dateString Date string to parse
* @returns Timestamp in milliseconds or undefined if parsing fails
*/
export function parseDueDate(dateString) {
if (!dateString)
return undefined;
try {
// Handle natural language dates
const lowerDate = dateString.toLowerCase().trim();
// Handle "now" specifically
if (lowerDate === 'now') {
return getCurrentTimestamp();
}
// Handle "today" with different options
if (lowerDate === 'today') {
return getEndOfDay();
}
if (lowerDate === 'today start' || lowerDate === 'start of today') {
return getStartOfDay();
}
if (lowerDate === 'today end' || lowerDate === 'end of today') {
return getEndOfDay();
}
// Handle "yesterday" and "tomorrow"
if (lowerDate === 'yesterday') {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
yesterday.setHours(23, 59, 59, 999);
return yesterday.getTime();
}
if (lowerDate === 'tomorrow') {
const tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
tomorrow.setHours(23, 59, 59, 999);
return tomorrow.getTime();
}
// Handle relative dates with specific times
const relativeTimeRegex = /(?:(\d+)\s*(minutes?|hours?|days?|weeks?|months?)\s*from\s*now|tomorrow|next\s+(?:week|month|year))\s*(?:at\s+(\d+)(?::(\d+))?\s*(am|pm)?)?/i;
const match = lowerDate.match(relativeTimeRegex);
if (match) {
const date = new Date();
const [_, amount, unit, hours, minutes, meridian] = match;
// Calculate the future date
if (amount && unit) {
const value = parseInt(amount);
if (unit.startsWith('minute')) {
date.setMinutes(date.getMinutes() + value);
}
else if (unit.startsWith('hour')) {
date.setHours(date.getHours() + value);
}
else if (unit.startsWith('day')) {
date.setDate(date.getDate() + value);
}
else if (unit.startsWith('week')) {
date.setDate(date.getDate() + (value * 7));
}
else if (unit.startsWith('month')) {
date.setMonth(date.getMonth() + value);
}
}
else if (lowerDate.startsWith('tomorrow')) {
date.setDate(date.getDate() + 1);
}
else if (lowerDate.includes('next week')) {
date.setDate(date.getDate() + 7);
}
else if (lowerDate.includes('next month')) {
date.setMonth(date.getMonth() + 1);
}
else if (lowerDate.includes('next year')) {
date.setFullYear(date.getFullYear() + 1);
}
// Set the time if specified
if (hours) {
let parsedHours = parseInt(hours);
const parsedMinutes = minutes ? parseInt(minutes) : 0;
// Convert to 24-hour format if meridian is specified
if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
parsedHours += 12;
if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
parsedHours = 0;
date.setHours(parsedHours, parsedMinutes, 0, 0);
}
else {
// Default to end of day if no time specified
date.setHours(23, 59, 59, 999);
}
return date.getTime();
}
// Handle various relative formats
const relativeFormats = [
{ regex: /(\d+)\s*minutes?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(m) },
{ regex: /(\d+)\s*hours?\s*from\s*now/i, handler: (h) => getRelativeTimestamp(0, h) },
{ regex: /(\d+)\s*days?\s*from\s*now/i, handler: (d) => getRelativeTimestamp(0, 0, d) },
{ regex: /(\d+)\s*weeks?\s*from\s*now/i, handler: (w) => getRelativeTimestamp(0, 0, 0, w) },
{ regex: /(\d+)\s*months?\s*from\s*now/i, handler: (m) => getRelativeTimestamp(0, 0, 0, 0, m) }
];
for (const format of relativeFormats) {
if (format.regex.test(lowerDate)) {
const value = parseInt(lowerDate.match(format.regex)[1]);
return format.handler(value);
}
}
// Handle specific date formats
// Format: MM/DD/YYYY
const usDateRegex = /^(\d{1,2})\/(\d{1,2})\/(\d{4})(?:\s+(\d{1,2})(?::(\d{1,2}))?(?:\s+(am|pm))?)?$/i;
const usDateMatch = lowerDate.match(usDateRegex);
if (usDateMatch) {
const [_, month, day, year, hours, minutes, meridian] = usDateMatch;
const date = new Date(parseInt(year), parseInt(month) - 1, // JS months are 0-indexed
parseInt(day));
// Add time if specified
if (hours) {
let parsedHours = parseInt(hours);
const parsedMinutes = minutes ? parseInt(minutes) : 0;
// Convert to 24-hour format if meridian is specified
if (meridian?.toLowerCase() === 'pm' && parsedHours < 12)
parsedHours += 12;
if (meridian?.toLowerCase() === 'am' && parsedHours === 12)
parsedHours = 0;
date.setHours(parsedHours, parsedMinutes, 0, 0);
}
else {
// Default to end of day if no time specified
date.setHours(23, 59, 59, 999);
}
return date.getTime();
}
// Try to parse as a date string
const date = new Date(dateString);
if (!isNaN(date.getTime())) {
return date.getTime();
}
// If all parsing fails, return undefined
return undefined;
}
catch (error) {
logger.warn(`Failed to parse due date: ${dateString}`, error);
throw new Error(`Invalid date format: ${dateString}`);
}
}
/**
* Format a due date timestamp into a human-readable string
*
* @param timestamp Unix timestamp in milliseconds
* @returns Formatted date string or undefined if timestamp is invalid
*/
export function formatDueDate(timestamp) {
if (!timestamp)
return undefined;
try {
const date = new Date(timestamp);
if (isNaN(date.getTime()))
return undefined;
// Format: "March 10, 2025 at 10:56 PM"
return date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
}).replace(' at', ',');
}
catch (error) {
logger.warn(`Failed to format due date: ${timestamp}`, error);
throw new Error(`Invalid timestamp: ${timestamp}`);
}
}
/**
* Checks if a timestamp is for today
*
* @param timestamp Unix timestamp in milliseconds
* @returns Boolean indicating if the timestamp is for today
*/
export function isToday(timestamp) {
const date = new Date(timestamp);
const today = new Date();
return date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear();
}
/**
* Get timestamp range for today (start to end)
*
* @returns Object with start and end timestamps for today
*/
export function getTodayRange() {
return {
start: getStartOfDay(),
end: getEndOfDay()
};
}
/**
* Format a date for display in errors and messages
* @param timestamp The timestamp to format
* @returns A human-readable relative time (e.g., "2 hours ago")
*/
export function formatRelativeTime(timestamp) {
if (!timestamp)
return 'Unknown';
const timestampNum = typeof timestamp === 'string' ? parseInt(timestamp, 10) : timestamp;
const now = Date.now();
const diffMs = now - timestampNum;
// Convert to appropriate time unit
const diffSec = Math.floor(diffMs / 1000);
if (diffSec < 60)
return `${diffSec} seconds ago`;
const diffMin = Math.floor(diffSec / 60);
if (diffMin < 60)
return `${diffMin} minutes ago`;
const diffHour = Math.floor(diffMin / 60);
if (diffHour < 24)
return `${diffHour} hours ago`;
const diffDays = Math.floor(diffHour / 24);
if (diffDays < 30)
return `${diffDays} days ago`;
const diffMonths = Math.floor(diffDays / 30);
if (diffMonths < 12)
return `${diffMonths} months ago`;
const diffYears = Math.floor(diffMonths / 12);
return `${diffYears} years ago`;
}