dateUtils.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. /**
  2. * Date utility functions to replace moment.js with native JavaScript Date
  3. */
  4. /**
  5. * Format a date to YYYY-MM-DD HH:mm format
  6. * @param {Date|string} date - Date to format
  7. * @returns {string} Formatted date string
  8. */
  9. export function formatDateTime(date) {
  10. const d = new Date(date);
  11. if (isNaN(d.getTime())) return '';
  12. const year = d.getFullYear();
  13. const month = String(d.getMonth() + 1).padStart(2, '0');
  14. const day = String(d.getDate()).padStart(2, '0');
  15. const hours = String(d.getHours()).padStart(2, '0');
  16. const minutes = String(d.getMinutes()).padStart(2, '0');
  17. return `${year}-${month}-${day} ${hours}:${minutes}`;
  18. }
  19. /**
  20. * Format a date to YYYY-MM-DD format
  21. * @param {Date|string} date - Date to format
  22. * @returns {string} Formatted date string
  23. */
  24. export function formatDate(date) {
  25. const d = new Date(date);
  26. if (isNaN(d.getTime())) return '';
  27. const year = d.getFullYear();
  28. const month = String(d.getMonth() + 1).padStart(2, '0');
  29. const day = String(d.getDate()).padStart(2, '0');
  30. return `${year}-${month}-${day}`;
  31. }
  32. /**
  33. * Format a time to HH:mm format
  34. * @param {Date|string} date - Date to format
  35. * @returns {string} Formatted time string
  36. */
  37. export function formatTime(date) {
  38. const d = new Date(date);
  39. if (isNaN(d.getTime())) return '';
  40. const hours = String(d.getHours()).padStart(2, '0');
  41. const minutes = String(d.getMinutes()).padStart(2, '0');
  42. return `${hours}:${minutes}`;
  43. }
  44. /**
  45. * Get ISO week number (ISO 8601)
  46. * @param {Date|string} date - Date to get week number for
  47. * @returns {number} ISO week number
  48. */
  49. export function getISOWeek(date) {
  50. const d = new Date(date);
  51. if (isNaN(d.getTime())) return 0;
  52. // Set to nearest Thursday: current date + 4 - current day number
  53. // Make Sunday's day number 7
  54. const target = new Date(d);
  55. const dayNr = (d.getDay() + 6) % 7;
  56. target.setDate(target.getDate() - dayNr + 3);
  57. // ISO week date weeks start on monday, so correct the day number
  58. const firstThursday = target.valueOf();
  59. target.setMonth(0, 1);
  60. if (target.getDay() !== 4) {
  61. target.setMonth(0, 1 + ((4 - target.getDay()) + 7) % 7);
  62. }
  63. return 1 + Math.ceil((firstThursday - target) / 604800000); // 604800000 = 7 * 24 * 3600 * 1000
  64. }
  65. /**
  66. * Check if a date is valid
  67. * @param {Date|string} date - Date to check
  68. * @returns {boolean} True if date is valid
  69. */
  70. export function isValidDate(date) {
  71. const d = new Date(date);
  72. return !isNaN(d.getTime());
  73. }
  74. /**
  75. * Check if a date is before another date
  76. * @param {Date|string} date1 - First date
  77. * @param {Date|string} date2 - Second date
  78. * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
  79. * @returns {boolean} True if date1 is before date2
  80. */
  81. export function isBefore(date1, date2, unit = 'millisecond') {
  82. const d1 = new Date(date1);
  83. const d2 = new Date(date2);
  84. if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
  85. switch (unit) {
  86. case 'year':
  87. return d1.getFullYear() < d2.getFullYear();
  88. case 'month':
  89. return d1.getFullYear() < d2.getFullYear() ||
  90. (d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth());
  91. case 'day':
  92. return d1.getFullYear() < d2.getFullYear() ||
  93. (d1.getFullYear() === d2.getFullYear() && d1.getMonth() < d2.getMonth()) ||
  94. (d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() < d2.getDate());
  95. case 'hour':
  96. return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60 * 60)) < Math.floor(d2.getTime() / (1000 * 60 * 60));
  97. case 'minute':
  98. return d1.getTime() < d2.getTime() && Math.floor(d1.getTime() / (1000 * 60)) < Math.floor(d2.getTime() / (1000 * 60));
  99. default:
  100. return d1.getTime() < d2.getTime();
  101. }
  102. }
  103. /**
  104. * Check if a date is after another date
  105. * @param {Date|string} date1 - First date
  106. * @param {Date|string} date2 - Second date
  107. * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
  108. * @returns {boolean} True if date1 is after date2
  109. */
  110. export function isAfter(date1, date2, unit = 'millisecond') {
  111. return isBefore(date2, date1, unit);
  112. }
  113. /**
  114. * Check if a date is the same as another date
  115. * @param {Date|string} date1 - First date
  116. * @param {Date|string} date2 - Second date
  117. * @param {string} unit - Unit of comparison ('minute', 'hour', 'day', etc.)
  118. * @returns {boolean} True if dates are the same
  119. */
  120. export function isSame(date1, date2, unit = 'millisecond') {
  121. const d1 = new Date(date1);
  122. const d2 = new Date(date2);
  123. if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return false;
  124. switch (unit) {
  125. case 'year':
  126. return d1.getFullYear() === d2.getFullYear();
  127. case 'month':
  128. return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
  129. case 'day':
  130. return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate();
  131. case 'hour':
  132. return Math.floor(d1.getTime() / (1000 * 60 * 60)) === Math.floor(d2.getTime() / (1000 * 60 * 60));
  133. case 'minute':
  134. return Math.floor(d1.getTime() / (1000 * 60)) === Math.floor(d2.getTime() / (1000 * 60));
  135. default:
  136. return d1.getTime() === d2.getTime();
  137. }
  138. }
  139. /**
  140. * Add time to a date
  141. * @param {Date|string} date - Base date
  142. * @param {number} amount - Amount to add
  143. * @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
  144. * @returns {Date} New date
  145. */
  146. export function add(date, amount, unit) {
  147. const d = new Date(date);
  148. if (isNaN(d.getTime())) return new Date();
  149. switch (unit) {
  150. case 'years':
  151. d.setFullYear(d.getFullYear() + amount);
  152. break;
  153. case 'months':
  154. d.setMonth(d.getMonth() + amount);
  155. break;
  156. case 'days':
  157. d.setDate(d.getDate() + amount);
  158. break;
  159. case 'hours':
  160. d.setHours(d.getHours() + amount);
  161. break;
  162. case 'minutes':
  163. d.setMinutes(d.getMinutes() + amount);
  164. break;
  165. case 'seconds':
  166. d.setSeconds(d.getSeconds() + amount);
  167. break;
  168. default:
  169. d.setTime(d.getTime() + amount);
  170. }
  171. return d;
  172. }
  173. /**
  174. * Subtract time from a date
  175. * @param {Date|string} date - Base date
  176. * @param {number} amount - Amount to subtract
  177. * @param {string} unit - Unit ('years', 'months', 'days', 'hours', 'minutes', 'seconds')
  178. * @returns {Date} New date
  179. */
  180. export function subtract(date, amount, unit) {
  181. return add(date, -amount, unit);
  182. }
  183. /**
  184. * Get start of a time unit
  185. * @param {Date|string} date - Base date
  186. * @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
  187. * @returns {Date} Start of unit
  188. */
  189. export function startOf(date, unit) {
  190. const d = new Date(date);
  191. if (isNaN(d.getTime())) return new Date();
  192. switch (unit) {
  193. case 'year':
  194. d.setMonth(0, 1);
  195. d.setHours(0, 0, 0, 0);
  196. break;
  197. case 'month':
  198. d.setDate(1);
  199. d.setHours(0, 0, 0, 0);
  200. break;
  201. case 'day':
  202. d.setHours(0, 0, 0, 0);
  203. break;
  204. case 'hour':
  205. d.setMinutes(0, 0, 0);
  206. break;
  207. case 'minute':
  208. d.setSeconds(0, 0);
  209. break;
  210. case 'second':
  211. d.setMilliseconds(0);
  212. break;
  213. }
  214. return d;
  215. }
  216. /**
  217. * Get end of a time unit
  218. * @param {Date|string} date - Base date
  219. * @param {string} unit - Unit ('year', 'month', 'day', 'hour', 'minute', 'second')
  220. * @returns {Date} End of unit
  221. */
  222. export function endOf(date, unit) {
  223. const d = new Date(date);
  224. if (isNaN(d.getTime())) return new Date();
  225. switch (unit) {
  226. case 'year':
  227. d.setMonth(11, 31);
  228. d.setHours(23, 59, 59, 999);
  229. break;
  230. case 'month':
  231. d.setMonth(d.getMonth() + 1, 0);
  232. d.setHours(23, 59, 59, 999);
  233. break;
  234. case 'day':
  235. d.setHours(23, 59, 59, 999);
  236. break;
  237. case 'hour':
  238. d.setMinutes(59, 59, 999);
  239. break;
  240. case 'minute':
  241. d.setSeconds(59, 999);
  242. break;
  243. case 'second':
  244. d.setMilliseconds(999);
  245. break;
  246. }
  247. return d;
  248. }
  249. /**
  250. * Format date for display with locale
  251. * @param {Date|string} date - Date to format
  252. * @param {string} format - Format string (simplified)
  253. * @returns {string} Formatted date string
  254. */
  255. export function format(date, format = 'L') {
  256. const d = new Date(date);
  257. if (isNaN(d.getTime())) return '';
  258. const year = d.getFullYear();
  259. const month = String(d.getMonth() + 1).padStart(2, '0');
  260. const day = String(d.getDate()).padStart(2, '0');
  261. const hours = String(d.getHours()).padStart(2, '0');
  262. const minutes = String(d.getMinutes()).padStart(2, '0');
  263. const seconds = String(d.getSeconds()).padStart(2, '0');
  264. switch (format) {
  265. case 'L':
  266. return `${month}/${day}/${year}`;
  267. case 'LL':
  268. return d.toLocaleDateString();
  269. case 'LLL':
  270. return d.toLocaleString();
  271. case 'llll':
  272. return d.toLocaleString();
  273. case 'YYYY-MM-DD':
  274. return `${year}-${month}-${day}`;
  275. case 'YYYY-MM-DD HH:mm':
  276. return `${year}-${month}-${day} ${hours}:${minutes}`;
  277. case 'HH:mm':
  278. return `${hours}:${minutes}`;
  279. default:
  280. return d.toLocaleString();
  281. }
  282. }
  283. /**
  284. * Parse a date string with multiple formats
  285. * @param {string} dateString - Date string to parse
  286. * @param {string[]} formats - Array of format strings to try
  287. * @param {boolean} strict - Whether to use strict parsing
  288. * @returns {Date|null} Parsed date or null if invalid
  289. */
  290. export function parseDate(dateString, formats = [], strict = true) {
  291. if (!dateString) return null;
  292. // Try native Date parsing first
  293. const nativeDate = new Date(dateString);
  294. if (!isNaN(nativeDate.getTime())) {
  295. return nativeDate;
  296. }
  297. // Try common formats
  298. const commonFormats = [
  299. 'YYYY-MM-DD HH:mm',
  300. 'YYYY-MM-DD',
  301. 'MM/DD/YYYY HH:mm',
  302. 'MM/DD/YYYY',
  303. 'DD.MM.YYYY HH:mm',
  304. 'DD.MM.YYYY',
  305. 'DD/MM/YYYY HH:mm',
  306. 'DD/MM/YYYY',
  307. 'DD-MM-YYYY HH:mm',
  308. 'DD-MM-YYYY'
  309. ];
  310. const allFormats = [...formats, ...commonFormats];
  311. for (const format of allFormats) {
  312. const parsed = parseWithFormat(dateString, format);
  313. if (parsed && isValidDate(parsed)) {
  314. return parsed;
  315. }
  316. }
  317. return null;
  318. }
  319. /**
  320. * Parse date with specific format
  321. * @param {string} dateString - Date string to parse
  322. * @param {string} format - Format string
  323. * @returns {Date|null} Parsed date or null
  324. */
  325. function parseWithFormat(dateString, format) {
  326. // Simple format parsing - can be extended as needed
  327. const formatMap = {
  328. 'YYYY': '\\d{4}',
  329. 'MM': '\\d{2}',
  330. 'DD': '\\d{2}',
  331. 'HH': '\\d{2}',
  332. 'mm': '\\d{2}',
  333. 'ss': '\\d{2}'
  334. };
  335. let regex = format;
  336. for (const [key, value] of Object.entries(formatMap)) {
  337. regex = regex.replace(new RegExp(key, 'g'), `(${value})`);
  338. }
  339. const match = dateString.match(new RegExp(regex));
  340. if (!match) return null;
  341. const groups = match.slice(1);
  342. let year, month, day, hour = 0, minute = 0, second = 0;
  343. let groupIndex = 0;
  344. for (let i = 0; i < format.length; i++) {
  345. if (format[i] === 'Y' && format[i + 1] === 'Y' && format[i + 2] === 'Y' && format[i + 3] === 'Y') {
  346. year = parseInt(groups[groupIndex++]);
  347. i += 3;
  348. } else if (format[i] === 'M' && format[i + 1] === 'M') {
  349. month = parseInt(groups[groupIndex++]) - 1;
  350. i += 1;
  351. } else if (format[i] === 'D' && format[i + 1] === 'D') {
  352. day = parseInt(groups[groupIndex++]);
  353. i += 1;
  354. } else if (format[i] === 'H' && format[i + 1] === 'H') {
  355. hour = parseInt(groups[groupIndex++]);
  356. i += 1;
  357. } else if (format[i] === 'm' && format[i + 1] === 'm') {
  358. minute = parseInt(groups[groupIndex++]);
  359. i += 1;
  360. } else if (format[i] === 's' && format[i + 1] === 's') {
  361. second = parseInt(groups[groupIndex++]);
  362. i += 1;
  363. }
  364. }
  365. if (year === undefined || month === undefined || day === undefined) {
  366. return null;
  367. }
  368. return new Date(year, month, day, hour, minute, second);
  369. }
  370. /**
  371. * Get current date and time
  372. * @returns {Date} Current date
  373. */
  374. export function now() {
  375. return new Date();
  376. }
  377. /**
  378. * Create a date from components
  379. * @param {number} year - Year
  380. * @param {number} month - Month (0-based)
  381. * @param {number} day - Day
  382. * @param {number} hour - Hour (optional)
  383. * @param {number} minute - Minute (optional)
  384. * @param {number} second - Second (optional)
  385. * @returns {Date} Created date
  386. */
  387. export function createDate(year, month, day, hour = 0, minute = 0, second = 0) {
  388. return new Date(year, month, day, hour, minute, second);
  389. }
  390. /**
  391. * Get relative time string (e.g., "2 hours ago")
  392. * @param {Date|string} date - Date to compare
  393. * @param {Date|string} now - Current date (optional)
  394. * @returns {string} Relative time string
  395. */
  396. export function fromNow(date, now = new Date()) {
  397. const d = new Date(date);
  398. const n = new Date(now);
  399. if (isNaN(d.getTime()) || isNaN(n.getTime())) return '';
  400. const diffMs = n.getTime() - d.getTime();
  401. const diffSeconds = Math.floor(diffMs / 1000);
  402. const diffMinutes = Math.floor(diffSeconds / 60);
  403. const diffHours = Math.floor(diffMinutes / 60);
  404. const diffDays = Math.floor(diffHours / 24);
  405. const diffWeeks = Math.floor(diffDays / 7);
  406. const diffMonths = Math.floor(diffDays / 30);
  407. const diffYears = Math.floor(diffDays / 365);
  408. if (diffSeconds < 60) return 'a few seconds ago';
  409. if (diffMinutes < 60) return `${diffMinutes} minute${diffMinutes !== 1 ? 's' : ''} ago`;
  410. if (diffHours < 24) return `${diffHours} hour${diffHours !== 1 ? 's' : ''} ago`;
  411. if (diffDays < 7) return `${diffDays} day${diffDays !== 1 ? 's' : ''} ago`;
  412. if (diffWeeks < 4) return `${diffWeeks} week${diffWeeks !== 1 ? 's' : ''} ago`;
  413. if (diffMonths < 12) return `${diffMonths} month${diffMonths !== 1 ? 's' : ''} ago`;
  414. return `${diffYears} year${diffYears !== 1 ? 's' : ''} ago`;
  415. }
  416. /**
  417. * Get calendar format (e.g., "Today", "Yesterday", "Tomorrow")
  418. * @param {Date|string} date - Date to format
  419. * @param {Date|string} now - Current date (optional)
  420. * @returns {string} Calendar format string
  421. */
  422. export function calendar(date, now = new Date()) {
  423. const d = new Date(date);
  424. const n = new Date(now);
  425. if (isNaN(d.getTime()) || isNaN(n.getTime())) return format(d);
  426. const diffMs = d.getTime() - n.getTime();
  427. const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  428. if (diffDays === 0) return 'Today';
  429. if (diffDays === 1) return 'Tomorrow';
  430. if (diffDays === -1) return 'Yesterday';
  431. if (diffDays > 1 && diffDays < 7) return `In ${diffDays} days`;
  432. if (diffDays < -1 && diffDays > -7) return `${Math.abs(diffDays)} days ago`;
  433. return format(d, 'L');
  434. }