export class SMDate { date: Date | null = null; dayString: string[] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; fullDayString: string[] = [ "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", ]; monthString: string[] = [ "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", ]; fullMonthString: string[] = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December", ]; constructor( dateOrString: string | Date = "", options: { format?: string; utc?: boolean } = {} ) { this.date = null; if (typeof dateOrString === "string") { if (dateOrString.length > 0) { this.parse(dateOrString, options); } } else if ( dateOrString instanceof Date && !Number.isNaN(dateOrString.getTime()) ) { this.date = dateOrString; } } /** * Parse a string date into a Date object * * @param {string} dateString The date string. * @param {object} options (optional) Options object. * @param {string} options.format (optional) The format of the date string. * @param {boolean} options.utc (optional) The date string is UTC. * @returns {SMDate} SMDate object. */ public parse( dateString: string, options: { format?: string; utc?: boolean } = {} ): SMDate { const now = new Date(); if (dateString.toLowerCase() == "now") { this.date = now; return this; } // Parse the date format to determine the order of the date components const order = (options.format || "dmy").toLowerCase().split(""); options.utc = options.utc || false; let components = []; let time = ""; // Test if the dateString is in ISO 8601 if ( /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{1,10})?Z$/i.test( dateString ) ) { options.format = "YMd"; [dateString, time] = dateString.split("T"); time = time.slice(0, -8); } // Split the date string into an array of components based on the length of each date component components = dateString.split(/[ /-]/); for (let i = 0; i < components.length; i++) { if (components[i].includes(":")) { time = components[i]; components.splice(i, 1); if ( i < components.length && /^(am?|a\.m\.|pm?|p\.m\.)$/i.test(components[i]) ) { time += " " + components[i].toUpperCase(); components.splice(i, 1); } break; } } if (components.every((v) => !isNaN(parseInt(v))) == false) { return this; } if (components.length > 3) { return this; } // Map the date components to the expected order based on the format const [day, month, year] = order[0] === "d" ? [components[0], components[1], components[2]] : order[0] === "m" ? [components[1], components[0], components[2]] : [components[2], components[1], components[0]]; let parsedDay: number = 0, parsedMonth: number = 0, parsedYear: number = 0; if (year.length == 3 || year.length >= 5) { return this; } if (day && day.length != 0 && month && month.length != 0) { // Parse the day, month, and year components parsedDay = parseInt(day.padStart(2, "0"), 10); parsedMonth = this.getMonthAsNumber(month); parsedYear = year ? parseInt(year.padStart(4, "20"), 10) : now.getFullYear(); } else { parsedDay = now.getDate(); parsedMonth = now.getMonth() + 1; parsedYear = now.getFullYear(); } let parsedHours: number = 0, parsedMinutes: number = 0, parsedSeconds: number = 0; if (time) { const regEx = new RegExp( /^(\d+)(?::(\d+))?(?::(\d+))? ?(am?|a\.m\.|pm?|p\.m\.)?$/, "i" ); if (regEx.test(time)) { const match = time.match(regEx); if (match) { parsedHours = parseInt(match[1]); parsedMinutes = match[2] ? parseInt(match[2]) : 0; parsedSeconds = match[3] ? parseInt(match[3]) : 0; if (match[4] && /pm/i.test(match[4])) { parsedHours += 12; } if ( match[4] && /am/i.test(match[4]) && parsedHours === 12 ) { parsedHours = 0; } } else { return this; } } else { return this; } } // Create a date object with the parsed components let date: Date | null = null; if (options.utc) { date = new Date( Date.UTC( parsedYear, parsedMonth - 1, parsedDay, parsedHours, parsedMinutes, parsedSeconds ) ); } else { date = new Date( parsedYear, parsedMonth - 1, parsedDay, parsedHours, parsedMinutes, parsedSeconds ); } // Test created date object let checkYear: number, checkMonth: number, checkDay: number, checkHours: number, checkMinutes: number, checkSeconds: number; if (options.utc) { const isoDate = date.toISOString(); checkYear = parseInt(isoDate.substring(0, 4), 10); checkMonth = parseInt(isoDate.substring(5, 7), 10); checkDay = new Date(isoDate).getUTCDate(); checkHours = parseInt(isoDate.substring(11, 13), 10); checkMinutes = parseInt(isoDate.substring(14, 16), 10); checkSeconds = parseInt(isoDate.substring(17, 19), 10); } else { checkYear = date.getFullYear(); checkMonth = date.getMonth() + 1; checkDay = date.getDate(); checkHours = date.getHours(); checkMinutes = date.getMinutes(); checkSeconds = date.getSeconds(); } if ( Number.isNaN(date.getTime()) == false && checkYear == parsedYear && checkMonth == parsedMonth && checkDay == parsedDay && checkHours == parsedHours && checkMinutes == parsedMinutes && checkSeconds == parsedSeconds ) { this.date = date; } else { this.date = null; } return this; } /** * Format the date to a string. * * @param {string} format The format to return. * @param {object} options (optional) Function options. * @param {boolean} options.utc (optional) Format the date to be as UTC instead of local. * @returns {string} The formatted date. */ public format(format: string, options: { utc?: boolean } = {}): string { if (this.date == null) { return ""; } let result = format; let year: string, month: string, date: string, day: number, hour: string, min: string, sec: string; if (options.utc) { const isoDate = this.date.toISOString(); year = isoDate.substring(0, 4); month = isoDate.substring(5, 7); date = isoDate.substring(8, 10); day = new Date(isoDate).getUTCDay(); hour = isoDate.substring(11, 13); min = isoDate.substring(14, 16); sec = isoDate.substring(17, 18); } else { year = this.date.getFullYear().toString(); month = (this.date.getMonth() + 1).toString(); date = this.date.getDate().toString(); day = this.date.getDay(); hour = this.date.getHours().toString(); min = this.date.getMinutes().toString(); sec = this.date.getSeconds().toString(); } const apm = parseInt(hour, 10) >= 12 ? "pm" : "am"; /* eslint-disable indent */ const apmhours = ( parseInt(hour, 10) > 12 ? parseInt(hour, 10) - 12 : parseInt(hour, 10) == 0 ? 12 : parseInt(hour, 10) ).toString(); /* eslint-enable indent */ // year result = result.replace(/\byy\b/g, year.slice(-2)); result = result.replace(/\byyyy\b/g, year); // month result = result.replace(/\bM\b/g, month); result = result.replace(/\bMM\b/g, (0 + month).slice(-2)); result = result.replace( /\bMMM\b/g, this.monthString[parseInt(month) - 1] ); result = result.replace( /\bMMMM\b/g, this.fullMonthString[parseInt(month) - 1] ); // day result = result.replace(/\bd\b/g, date); result = result.replace(/\bdd\b/g, (0 + date).slice(-2)); result = result.replace(/\bEEE\b/g, this.dayString[day]); result = result.replace(/\bEEEE\b/g, this.fullDayString[day]); // hour result = result.replace(/\bH\b/g, hour); result = result.replace(/\bHH\b/g, (0 + hour).slice(-2)); result = result.replace(/\bh\b/g, apmhours); result = result.replace(/\bhh\b/g, (0 + apmhours).slice(-2)); // min result = result.replace(/\bm\b/g, min); result = result.replace(/\bmm\b/g, (0 + min).slice(-2)); // sec result = result.replace(/\bs\b/g, sec); result = result.replace(/\bss\b/g, (0 + sec).slice(-2)); // am/pm result = result.replace(/\baa\b/g, apm); return result; } /** * Return a relative date string from now. * * @returns {string} A relative date string. */ public relative(): string { let prefix = ""; let postfix = " ago"; if (this.date === null) { return ""; } const now = new Date(); let dif = Math.round((now.getTime() - this.date.getTime()) / 1000); if (dif < 0) { dif = Math.abs(dif); prefix = "In "; postfix = ""; } if (dif < 60) { return "Just now"; } else if (dif < 3600) { const v = Math.round(dif / 60); return prefix + v + " min" + (v != 1 ? "s" : "") + postfix; } else if (dif < 86400) { const v = Math.round(dif / 3600); return prefix + v + " hour" + (v != 1 ? "s" : "") + postfix; } else if (dif < 604800) { const v = Math.round(dif / 86400); return prefix + v + " day" + (v != 1 ? "s" : "") + postfix; } else if (dif < 2419200) { const v = Math.round(dif / 604800); return prefix + v + " week" + (v != 1 ? "s" : "") + postfix; } return ( this.monthString[this.date.getMonth()] + " " + this.date.getDate() + ", " + this.date.getFullYear() ); } /** * If the date is before the passed date. * * @param {Date|SMDate} d (optional) The date to check. If none, use now * @returns {boolean} If the date is before the passed date. */ public isBefore(d: Date | SMDate = new SMDate("now")): boolean { const otherDate = d instanceof SMDate ? d.date : d; if (otherDate == null) { return false; } if (this.date == null) { return true; } return otherDate > this.date; } /** * If the date is after the passed date. * * @param {Date|SMDate} d (optional) The date to check. If none, use now * @returns {boolean} If the date is after the passed date. */ public isAfter(d: Date | SMDate = new SMDate("now")): boolean { const otherDate = d instanceof SMDate ? d.date : d; if (otherDate == null) { return false; } if (this.date == null) { return true; } return otherDate < this.date; } /** * Return a month number from a string or a month number or month name * * @param {string} monthString The month string as number or name * @returns {number} The month number */ private getMonthAsNumber(monthString: string): number { const months = this.fullMonthString.map((month) => month.toLowerCase()); const shortMonths = months.map((month) => month.slice(0, 3)); const monthIndex = months.indexOf(monthString.toLowerCase()); if (monthIndex !== -1) { return monthIndex + 1; } const shortMonthIndex = shortMonths.indexOf(monthString.toLowerCase()); if (shortMonthIndex !== -1) { return shortMonthIndex + 1; } const monthNumber = parseInt(monthString, 10); if (!isNaN(monthNumber) && monthNumber >= 1 && monthNumber <= 12) { return monthNumber; } return 0; } /** * Test if the current date is valid. * * @returns {boolean} If the current date is valid. */ public isValid(): boolean { return this.date !== null; } /** * Return a string with only the first occurrence of characters * * @param {string} str The string to modify. * @param {string} characters The characters to use to test. * @returns {string} A string that only contains the first occurrence of the characters. */ private onlyFirstOccurrence( str: string, characters: string = "dMy" ): string { let findCharacters = characters.split(""); const replaceRegex = new RegExp("[^" + characters + "]", "g"); let result = ""; str = str.replace(replaceRegex, ""); if (str.length > 0) { str.split("").forEach((strChar) => { if ( findCharacters.length > 0 && findCharacters.includes(strChar) ) { result += strChar; const index = findCharacters.findIndex( (findChar) => findChar === strChar ); if (index !== -1) { findCharacters = findCharacters .slice(0, index) .concat(findCharacters.slice(index + 1)); } } }); } return result; } }