legal-markdown-js
Version:
Node.js implementation of LegalMarkdown for processing legal documents with markdown and YAML - Complete feature parity with Ruby version
367 lines • 11.9 kB
JavaScript
/**
* @fileoverview Advanced Date Helpers - Node.js Extensions
*
* This module provides advanced date manipulation and formatting utilities
* that extend beyond the basic functionality of the original Ruby legal-markdown.
* These are Node.js specific enhancements for complex date operations.
*
* Features (Node.js extensions):
* - Date arithmetic (add years, months, days)
* - Advanced date formatting with multiple format options
* - Legal-specific date formats (formal, ordinal, etc.)
* - Complex date parsing and manipulation
* - Comprehensive error handling for edge cases
*
* @example
* ```typescript
* import { addYears, formatDate, DateFormats } from '../helpers/index.js';
*
* // Advanced date arithmetic
* const expiryDate = addYears('2025-01-01', 5);
*
* // Legal document formatting
* const legalDate = formatDate(new Date(), DateFormats.LEGAL);
* // Result: "16th day of July, 2025"
* ```
*/
import { getConfig } from '../../config/index.js';
function parseDate(input) {
if (input instanceof Date)
return new Date(input);
const str = String(input);
if (/^\d{4}-\d{2}-\d{2}$/.test(str)) {
return new Date(`${str}T00:00:00`);
}
return new Date(str);
}
/**
* Adds a specified number of years to a date
*
* This function handles both Date objects and ISO date strings, making it
* useful for contract calculations and legal document date processing.
*
* @param {Date | string} date - The base date as a Date object or ISO string
* @param {number} years - The number of years to add (can be negative)
* @returns {Date} A new Date object with the years added
* @throws {Error} If the input date is invalid
*
* @example
* ```typescript
* // Add 5 years to a contract start date
* const startDate = new Date('2025-01-01');
* const endDate = addYears(startDate, 5);
* // Result: Date object for 2030-01-01
*
* // Using with string input
* const futureDate = addYears('2025-07-16', 2);
* // Result: Date object for 2027-07-16
*
* // Subtract years (negative input)
* const pastDate = addYears(new Date(), -3);
* ```
*/
export function addYears(date, years) {
if (!date) {
throw new Error('Date is required for addYears');
}
const d = parseDate(date);
if (isNaN(d.getTime())) {
throw new Error(`Invalid date: ${date}`);
}
d.setFullYear(d.getFullYear() + years);
return d;
}
/**
* Adds a specified number of days to a date
*
* Useful for calculating deadlines, notice periods, and other time-sensitive
* legal obligations in documents.
*
* @param {Date | string} date - The base date as a Date object or ISO string
* @param {number} days - The number of days to add (can be negative)
* @returns {Date} A new Date object with the days added
* @throws {Error} If the input date is invalid
*
* @example
* ```typescript
* // Add 30 days for a notice period
* const noticeDate = addDays(new Date(), 30);
*
* // Calculate a deadline
* const deadline = addDays('2025-07-16', 14);
*
* // Previous dates (negative input)
* const pastDate = addDays(new Date(), -7);
* ```
*/
export function addDays(date, days) {
if (!date) {
throw new Error('Date is required for addDays');
}
const d = parseDate(date);
if (isNaN(d.getTime())) {
throw new Error(`Invalid date: ${date}`);
}
d.setDate(d.getDate() + days);
return d;
}
/**
* Adds a specified number of months to a date
*
* Handles month boundaries intelligently, useful for calculating quarterly
* reports, monthly payments, and other periodic legal obligations.
*
* @param {Date | string} date - The base date as a Date object or ISO string
* @param {number} months - The number of months to add (can be negative)
* @returns {Date} A new Date object with the months added
* @throws {Error} If the input date is invalid
*
* @example
* ```typescript
* // Add 6 months for a review period
* const reviewDate = addMonths(new Date('2025-01-31'), 6);
* // Result: Date object for 2025-07-31
*
* // Calculate quarterly dates
* const quarterlyDate = addMonths('2025-01-01', 3);
* // Result: Date object for 2025-04-01
*
* // Previous months (negative input)
* const previousQuarter = addMonths(new Date(), -3);
* ```
*/
export function addMonths(date, months) {
if (!date) {
throw new Error('Date is required for addMonths');
}
const d = parseDate(date);
if (isNaN(d.getTime())) {
throw new Error(`Invalid date: ${date}`);
}
const targetMonth = d.getMonth() + months;
d.setMonth(targetMonth);
if (d.getMonth() !== ((targetMonth % 12) + 12) % 12) {
d.setDate(0);
}
return d;
}
/**
* Formats a date using advanced format strings
*
* Provides flexible date formatting with support for multiple format tokens.
* Particularly useful for legal documents that require specific date formats.
*
* @param {Date | string} date - The date to format
* @param {string} format - Format string with tokens (default: 'YYYY-MM-DD')
* @returns {string} The formatted date string
* @throws {Error} If the input date is invalid
*
* Supported format tokens:
* - YYYY: 4-digit year (2025)
* - YY: 2-digit year (25)
* - MMMM: Full month name (July)
* - MMM: Short month name (Jul)
* - MM: 2-digit month (07)
* - M: Month without leading zero (7)
* - DD: 2-digit day (16)
* - D: Day without leading zero (16)
* - Do: Day with ordinal suffix (16th)
* - dddd: Full day name (Wednesday)
* - ddd: Short day name (Wed)
*
* @example
* ```typescript
* const date = new Date('2025-07-16');
*
* formatDate(date, 'YYYY-MM-DD'); // "2025-07-16"
* formatDate(date, 'MM/DD/YYYY'); // "07/16/2025"
* formatDate(date, 'DD/MM/YYYY'); // "16/07/2025"
* formatDate(date, 'MMMM D, YYYY'); // "July 16, 2025"
* formatDate(date, 'dddd, MMMM Do, YYYY'); // "Wednesday, July 16th, 2025"
* formatDate(date, DateFormats.LEGAL); // "16th day of July, 2025"
* formatDate(date, DateFormats.FORMAL); // "Wednesday, July 16th, 2025"
*
* // Custom formats
* formatDate(date, 'MMMM Do, YYYY'); // "July 16th, 2025"
* ```
*/
export function formatDate(date, format = 'YYYY-MM-DD', locale) {
const d = parseDate(date);
if (isNaN(d.getTime())) {
throw new Error(`Invalid date: ${date}`);
}
const year = d.getFullYear();
const month = d.getMonth();
const day = d.getDate();
const dayOfWeek = d.getDay();
const lang = locale || getConfig().processing.locale || 'en';
// Month names
const monthNames = getMonthNames(lang);
const monthNamesShort = getMonthShortNames(lang);
const monthNamesSpanish = [
'enero',
'febrero',
'marzo',
'abril',
'mayo',
'junio',
'julio',
'agosto',
'septiembre',
'octubre',
'noviembre',
'diciembre',
];
// Day names
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const dayNamesShort = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
// Format replacements
const replacements = {
YYYY: String(year),
YY: String(year).slice(-2),
MMMM: monthNames[month],
MMMM_ES: monthNamesSpanish[month],
MMM: monthNamesShort[month],
MM: String(month + 1).padStart(2, '0'),
M: String(month + 1),
DD: String(day).padStart(2, '0'),
D: String(day),
Do: addOrdinalSuffix(day),
dddd: dayNames[dayOfWeek],
ddd: dayNamesShort[dayOfWeek],
};
let result = format;
// Sort tokens by length (descending) to avoid replacement conflicts
// For example, "Do" must be processed before "D" to prevent "Do" -> "15o"
// And "MMMM_ES" must be processed before "MMMM" to prevent Spanish conflicts
const sortedTokens = Object.keys(replacements).sort((a, b) => b.length - a.length);
// Use word boundary regex to prevent partial matches within words
// This prevents "M" from matching inside "Monday" after replacement
for (const token of sortedTokens) {
const replacement = replacements[token];
// Use word boundaries (\b) for single-character tokens to prevent partial matches
const isShortToken = token.length <= 2;
const regex = isShortToken ? new RegExp(`\\b${token}\\b`, 'g') : new RegExp(token, 'g');
result = result.replace(regex, replacement);
}
return result;
}
/**
* Adds ordinal suffix to a number (1st, 2nd, 3rd, 4th, etc.)
*
* Internal helper function used by formatDate to add ordinal suffixes
* to day numbers for legal document formatting.
*
* @param {number} num - The number to add ordinal suffix to
* @returns {string} The number with ordinal suffix
*
* @example
* ```typescript
* addOrdinalSuffix(1); // "1st"
* addOrdinalSuffix(2); // "2nd"
* addOrdinalSuffix(3); // "3rd"
* addOrdinalSuffix(4); // "4th"
* addOrdinalSuffix(21); // "21st"
* addOrdinalSuffix(22); // "22nd"
* ```
*/
function addOrdinalSuffix(num) {
const suffix = ['th', 'st', 'nd', 'rd'];
const value = num % 100;
// Handle special cases: 11th, 12th, 13th
if (value >= 11 && value <= 13) {
return num + suffix[0]; // 'th'
}
// Handle regular cases: 1st, 2nd, 3rd, 4th, etc.
const lastDigit = value % 10;
return num + (suffix[lastDigit] || suffix[0]);
}
const ENGLISH_MONTH_NAMES = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const ENGLISH_MONTH_NAMES_SHORT = [
'Jan',
'Feb',
'Mar',
'Apr',
'May',
'Jun',
'Jul',
'Aug',
'Sep',
'Oct',
'Nov',
'Dec',
];
function getMonthNames(locale) {
try {
return Array.from({ length: 12 }, (_, i) => new Intl.DateTimeFormat(locale, { month: 'long' }).format(new Date(2025, i, 1)));
}
catch {
return ENGLISH_MONTH_NAMES;
}
}
function getMonthShortNames(locale) {
try {
return Array.from({ length: 12 }, (_, i) => new Intl.DateTimeFormat(locale, { month: 'short' }).format(new Date(2025, i, 1)));
}
catch {
return ENGLISH_MONTH_NAMES_SHORT;
}
}
/**
* Predefined date formats for common legal document use cases
*
* These formats provide standardized date formatting for different types
* of legal documents and jurisdictions.
*
* {Object} DateFormats
* @example
* ```typescript
* import { formatDate, DateFormats } from './advanced-date-helpers.js';
*
* const date = new Date('2025-07-16');
*
* formatDate(date, DateFormats.LEGAL); // "16th day of July, 2025"
* formatDate(date, DateFormats.FORMAL); // "Wednesday, July 16th, 2025"
* formatDate(date, DateFormats.US); // "07/16/2025"
* formatDate(date, DateFormats.EU); // "16/07/2025"
* formatDate(date, DateFormats.ISO); // "2025-07-16"
* formatDate(date, DateFormats.LONG); // "July 16, 2025"
* formatDate(date, DateFormats.SHORT); // "Jul 16, 2025"
* ```
*/
export const DateFormats = {
/** Legal format: "16th day of July, 2025" */
LEGAL: 'Do day of MMMM, YYYY',
/** Formal format: "Wednesday, July 16th, 2025" */
FORMAL: 'dddd, MMMM Do, YYYY',
/** Spanish format: "16 de julio de 2025" */
SPANISH: 'D de MMMM_ES de YYYY',
/** US format: "07/16/2025" */
US: 'MM/DD/YYYY',
/** European format: "16/07/2025" */
EU: 'DD/MM/YYYY',
/** ISO format: "2025-07-16" */
ISO: 'YYYY-MM-DD',
/** Long format: "July 16, 2025" */
LONG: 'MMMM D, YYYY',
/** Short format: "Jul 16, 2025" */
SHORT: 'MMM D, YYYY',
/** Year only: "2025" */
YEAR: 'YYYY',
/** Month and year: "July 2025" */
MONTH_YEAR: 'MMMM YYYY',
};
//# sourceMappingURL=advanced-date-helpers.js.map