diff --git a/.changeset/cuddly-eggs-design.md b/.changeset/cuddly-eggs-design.md new file mode 100644 index 00000000..ffb6b9b9 --- /dev/null +++ b/.changeset/cuddly-eggs-design.md @@ -0,0 +1,5 @@ +--- +"@razorpay/i18nify-js": patch +--- + +feat[ATLAS-104]: Introducing Date & Time Module diff --git a/packages/i18nify-js/README.md b/packages/i18nify-js/README.md index 8ad5a4f3..2b7b7210 100644 --- a/packages/i18nify-js/README.md +++ b/packages/i18nify-js/README.md @@ -488,3 +488,443 @@ const parsedInfo = parsePhoneNumber('', countryCode); console.log('Country Code:', parsedInfo.countryCode); // 'JP' console.log('Format Template:', parsedInfo.formatTemplate); // 'xxx-xxxx-xxxx' ``` + +### Module 03: Date & Time Module + +This module provides functions for formatting and manipulating dates and times in a locale-sensitive manner using the JavaScript Intl API & Date object. + +#### add(date, options:) + +๐Ÿ•’๐Ÿš€ This nifty time traveler lets you leap through the calendar with ease! Whether you're planning future events or reminiscing the past, it swiftly adds days, months, or years to any given date. No more manual date calculations; this function uses JavaScript's Date object to fast-forward or rewind your dates seamlessly. ๐Ÿ—“๏ธโญ๏ธ + +##### Examples + +```javascript +// Adding 10 days to today +console.log(add(new Date(), {value: 10, unit: 'days'})); // Outputs a date 10 days from now + +// Fast-forwarding 5 months from a specific date +console.log(add('2024-01-23', {value: 5, unit: 'months'})); // Outputs a date 5 months after January 23, 2024 + +// Jumping 3 years into the future from a date object +console.log(add(new Date(2024, 0, 23), {value: 3, unit: 'years'})); // Outputs a date 3 years after January 23, 2024 +``` + +๐Ÿ’ก No matter the format of your starting dateโ€”a string or a Date objectโ€”this function handles it. Just make sure your date string matches one of the recognized formats, or else you'll be time-traveling to the era of error messages! ๐Ÿ›‘๐Ÿ“… + +#### formatDate(date, options:) + +๐ŸŒ๐Ÿ“† This global time stylist effortlessly turns your dates into beautifully formatted strings, tailored to different locales. Whether you're dealing with international clients or just love the beauty of diverse date formats, `formatDate` is your go-to function. It leverages the power of the Intl.DateTimeFormat API, ensuring that your dates always dress to impress, no matter where they're displayed. ๐ŸŽฉ๐ŸŒŸ + +##### Examples + +```javascript +// Basic date formatting +console.log(formatDate(new Date(), {locale: 'en-US'})); // Outputs today's date in 'MM/DD/YYYY' format + +// Formatting with different locale +console.log(formatDate('2024-05-20', {locale: 'de-DE'})); // Outputs '20.05.2024' + +// Using Intl.DateTimeFormat options +console.log( + formatDate('2024-05-20', {locale: 'en-GB', intlOptions: { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }}), +); // Outputs 'Monday, 20 May 2024' +``` + +๐Ÿ’ก Remember, if the date string doesn't match any supported formats, the function raises the curtain on an error message! ๐Ÿ›‘๐ŸŽญ + +#### formatDateTime(date, options:) + +๐Ÿ•ฐ๏ธ๐ŸŒ This savvy time tailor is your go-to for dressing up dates and times in locale-specific styles. Whether you're marking milestones, scheduling global meetings, or just need that perfect date-time format, `formatDateTime` uses the Internationalization API (Intl) to translate your dates and times into the local lingo. It's like having a linguistic time machine at your fingertips! ๐ŸŒŸ๐Ÿ—“๏ธ + +##### Examples + +```javascript +// Standard date-time formatting +console.log(formatDateTime(new Date(), {locale: 'en-US'})); // Outputs something like '1/23/2024, 10:00 AM' + +// Custom date-time formatting in French +console.log( + formatDateTime('2024-05-20 15:00', {locale: 'fr-FR', intlOptions: { + weekday: 'long', + hour: '2-digit', + minute: '2-digit', + }}), +); // Outputs 'lundi, 15:00' + +// Locale-specific date-time formatting with extended options +console.log( + formatDateTime('2024-12-31 23:59', {locale: 'ja-JP', intlOptions: { + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }}), +); // Outputs '2024ๅนด12ๆœˆ31ๆ—ฅ 23:59:00' +``` + +๐Ÿ’ก Remember, it's not just about translating the date and time; it's about presenting them in a way that feels natural and familiar to the user, no matter where they are in the world. ๐ŸŒโŒš + +#### formatTime(date, options:) + +โฐ๐ŸŒ This timely charmer is your key to unlocking the secrets of time presentation across different cultures. Using the wizardry of the Internationalization API (Intl), `formatTime` translates your time into a format that resonates with local customs and practices. Whether it's for scheduling international calls or just making sure you're in sync with the world's timezones, this function is your trusty sidekick in the realm of time formatting! ๐ŸŒŸโŒš + +##### Examples + +```javascript +// Simple time formatting +console.log(formatTime(new Date(), {locale: 'en-US'})); // Outputs something like '10:00 AM' + +// Time formatting with extended options in French +console.log( + formatTime('2024-05-20 15:00', {locale: 'fr-FR', intlOptions: { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }}), +); // Outputs '15:00:00' + +// Custom time formatting in Japanese +console.log( + formatTime('2024-05-20 23:59', {locale: 'ja-JP', intlOptions: { + hour: '2-digit', + minute: '2-digit', + hour12: true, + }}), +); // Outputs '11:59 ๅˆๅพŒ' +``` + +๐Ÿ’ก Pro Tip: `formatTime` isn't just about showing the time; it's about presenting it in a way that's intuitive and familiar to your audience, wherever they may be. ๐ŸŒ๐Ÿ•’ + +#### getQuarter(date) + +๐Ÿ—“๏ธ๐ŸŒท๐Ÿ‚ This calendar connoisseur takes any date and magically determines its quarter, effortlessly dividing the year into four distinct parts. Whether you're tracking financial quarters, academic periods, or just curious about the season, `getQuarter` is your key to easily navigating through the year's chapters. A handy tool for anyone dealing with dates, from accountants to students! ๐ŸŒŸ๐Ÿ“š + +##### Examples + +```javascript +// Determining the quarter for a date in April +console.log(getQuarter('2024-04-15')); // Outputs 2 (Q2) + +// Finding out the quarter for a date in November +console.log(getQuarter(new Date(2024, 10, 25))); // Outputs 4 (Q4) + +// Identifying the quarter for a date in January +console.log(getQuarter('2024-01-01')); // Outputs 1 (Q1) +``` + +๐Ÿ’ก Fun Fact: Did you know that quarters are not only useful in business and academia, but also in various forms of planning and analysis? With `getQuarter`, you'll always know where you stand in the rhythm of the year! ๐Ÿ“ˆ๐Ÿ + +#### getRelativeTime(date, baseDate, options:) + +โณ๐ŸŒ This time-traveling virtuoso effortlessly bridges the gap between dates, offering a glimpse into the past or a peek into the future. With the help of the Internationalization API (Intl), `getRelativeTime` transforms absolute dates into relatable, human-friendly phrases like '3 hours ago' or 'in 2 days'. Whether you're reminiscing the past or anticipating the future, this function keeps you connected to time in the most intuitive way! ๐Ÿš€๐Ÿ•ฐ๏ธ + +##### Examples + +```javascript +// How long ago was a past date? +console.log(getRelativeTime('2024-01-20', new Date())); // Outputs something like '3 days ago' + +// How much time until a future date? +console.log(getRelativeTime('2024-01-26', new Date())); // Outputs 'in 3 days' + +// Customizing output for different locales +console.log(getRelativeTime('2024-01-26', '2024-01-23', {locale: 'fr-FR'})); // Outputs 'dans 3 jours' (in 3 days in French) +``` + +๐Ÿ’ก Pro Tip: `getRelativeTime` is not just a way to express time differences; it's a bridge that connects your users to the temporal context in a way that's both meaningful and culturally aware. Time is more than seconds and minutes; it's a story, and this function helps you tell it! ๐Ÿ“–โŒš + +#### getWeek(date) + +๐Ÿ“…๐Ÿ”ข This clever calendar companion swiftly calculates the week number for any given date, placing you precisely within the tapestry of the year. It's like having a bird's-eye view of the calendar, helping you navigate through the weeks with ease. Whether you're planning projects, tracking milestones, or simply curious about where you stand in the year, `getWeek` is your reliable guide through the annual journey! ๐ŸŒŸ๐Ÿ—“๏ธ + +##### Examples + +```javascript +// Finding the week number for a date in January +console.log(getWeek('2024-01-15')); // Outputs the week number in January 2024 + +// Determining the week number for a date in mid-year +console.log(getWeek(new Date(2024, 5, 20))); // Outputs the week number in June 2024 + +// Calculating the week number for a date towards the end of the year +console.log(getWeek('2024-12-31')); // Outputs the week number at the end of December 2024 +``` + +๐Ÿ’ก Did You Know? The concept of week numbers is especially popular in business and academia for organizing schedules and events. With `getWeek`, staying on top of your plans becomes a breeze, giving you a clear view of your year at a glance! ๐ŸŒ๐Ÿ“Š + +#### getWeekdays(options:) + +๐Ÿ“…๐ŸŒ This global day-namer is your trusty guide through the week, no matter where you are in the world. Using the power of the Internationalization API (Intl), `getWeekdays` serves up the names of all seven days tailored to your chosen locale. From planning international meetings to creating a multilingual planner, this function provides the perfect blend of cultural awareness and practical utility, keeping you in sync with the local rhythm of life, one day at a time! ๐ŸŒŸ๐Ÿ—“๏ธ + +##### Examples + +```javascript +// Getting weekdays in English +console.log(getWeekdays({locale: 'en-US'})); // Outputs ['Sunday', 'Monday', ..., 'Saturday'] + +// Discovering weekdays in French +console.log(getWeekdays({locale: 'fr-FR'})); // Outputs ['dimanche', 'lundi', ..., 'samedi'] + +// Exploring weekdays in Japanese +console.log(getWeekdays({locale: 'ja-JP'})); // Outputs ['ๆ—ฅๆ›œๆ—ฅ', 'ๆœˆๆ›œๆ—ฅ', ..., 'ๅœŸๆ›œๆ—ฅ'] +``` + +๐Ÿ’ก Did You Know? The order and names of weekdays vary across cultures and languages. With `getWeekdays`, you can easily cater to a global audience, ensuring that your application speaks their language, quite literally! ๐ŸŒ๐Ÿ—ฃ๏ธ + +#### isAfter(date1, date2) + +โฑ๏ธ๐Ÿ” This temporal detective is your go-to for solving date mysteries! `isAfter` takes two dates and cleverly reveals whether the first is indeed later than the second. It's like having a time-traveling magnifying glass, making it super easy to compare dates in your applications. Whether you're scheduling deadlines, organizing events, or just curious about the order of things, `isAfter` is your trusty sidekick in the world of time! ๐ŸŒŸ๐Ÿ“… + +##### Examples + +```javascript +// Checking if one date is after another +console.log(isAfter('2024-01-25', '2024-01-20')); // Outputs true (Jan 25, 2024 is after Jan 20, 2024) + +// Comparing today with a future date +console.log(isAfter(new Date(), '2024-12-31')); // Outputs false if today is before Dec 31, 2024 + +// Comparing dates in different years +console.log(isAfter('2025-01-01', '2024-12-31')); // Outputs true (Jan 1, 2025 is after Dec 31, 2024) +``` + +๐Ÿ’ก Pro Tip: `isAfter` isn't just a function; it's a time machine in your coding toolbox! Use it to prevent past dates in booking systems, validate deadlines, or even in time-sensitive games and activities. Time is in your hands now, code it wisely! ๐ŸŽฉโณ + +#### isBefore(date1, date2) + +โณ๐Ÿ”Ž This is your chronological compass, guiding you through the timelines with ease! `isBefore` is the function that answers one of time's classic questions: Is this date before that one? It's an essential tool for applications dealing with deadlines, scheduling, and historical data. With `isBefore`, you can effortlessly determine the sequence of events, plan ahead, and ensure that you're not mixing up your yesterdays and tomorrows. ๐ŸŒŸ๐Ÿ“† + +##### Examples + +```javascript +// Checking if a date is before another +console.log(isBefore('2024-01-10', '2024-01-15')); // Outputs true if Jan 10, 2024 is before Jan 15, 2024 + +// Verifying if today is before a specific date +console.log(isBefore(new Date(), '2024-12-31')); // Outputs true if today is before Dec 31, 2024 + +// Comparing two dates in different years +console.log(isBefore('2023-12-31', '2024-01-01')); // Outputs true since Dec 31, 2023 is before Jan 1, 2024 +``` + +๐Ÿ’ก Pro Tip: `isBefore` is not just about past and future. It's about making informed decisions, managing timelines efficiently, and ensuring that everything happens at the right moment. Use it to navigate through the complexities of time with confidence and precision! ๐ŸŽฉโŒ› + +#### isLeapYear(year) + +๐ŸŒŒ๐Ÿ“… Leap into the fascinating world of calendars with `isLeapYear`! This function is your trusty sidekick in unraveling the mysteries of the Gregorian calendar. It answers the question: Is this year a leap year? Leap years, with their extra day in February, keep our calendars aligned with Earth's orbit around the Sun. Whether you're scheduling events, programming a calendar application, or just satisfying your curiosity, `isLeapYear` is an essential tool. ๐Ÿš€๐Ÿ—“๏ธ + +##### Examples + +```javascript +// Check if 2020 is a leap year +console.log(isLeapYear(2020)); // Outputs true, as 2020 is a leap year + +// Verify if 2023 is a leap year +console.log(isLeapYear(2023)); // Outputs false, as 2023 is not a leap year + +// Determine if 1900 is a leap year (it's not, despite being divisible by 4!) +console.log(isLeapYear(1900)); // Outputs false, as 1900 is not a leap year by Gregorian rules +``` + +๐Ÿ’ก Pro Tip: `isLeapYear` not only simplifies date calculations but also serves as a fun fact generator! Impress your friends and colleagues with your knowledge about leap years and why they exist. Remember, every four years, we get that extra day, thanks to the quirks of our solar system and the way we track time! ๐ŸŒโฐ๐ŸŽ‰ + +#### isSameDay(date1, date2) + +๐ŸŒž๐Ÿ“… The `isSameDay` function is a calendar wizard's dream! Itโ€™s like having an eagle-eye view of your calendar, helping you pinpoint if two dates fall on the same glorious day. Whether you're organizing events, tracking special occasions, or coding up the next great scheduling app, `isSameDay` is your go-to for aligning dates with cosmic precision. ๐ŸŒŒ๐Ÿ” + +##### Examples + +```javascript +// Compare two dates for the same day +const firstDate = new Date(2022, 3, 15); // April 15, 2022 +const secondDate = new Date(2022, 3, 15); // April 15, 2022 +console.log(isSameDay(firstDate, secondDate)); // Outputs true, both dates are April 15, 2022 + +// Checking different days +const anotherDate = new Date(2022, 3, 16); // April 16, 2022 +console.log(isSameDay(firstDate, anotherDate)); // Outputs false, different days! + +// Works with string inputs too! +console.log(isSameDay('2022-04-15', '2022-04-15')); // Outputs true, both represent April 15, 2022 +``` + +๐Ÿ’ก Handy Tip: Use `isSameDay` to avoid double-booking, remember anniversaries, or even to trigger daily reminders. It's your silent guardian in the realm of dates, ensuring you're always on top of your day-to-day adventures. ๐ŸŽฏ๐Ÿ“†๐Ÿš€ + +#### isValidDate(dateString, options:) + +๐Ÿ•ต๏ธโ€โ™‚๏ธ๐Ÿ—“๏ธ The `isValidDate` function now comes with an international flair! It's a robust date validator that not only checks if a date is valid but also ensures it aligns with the date format of a specific locale. Perfect for applications catering to a global audience, it scrutinizes dates against various international formats, making it a versatile tool in your date validation arsenal. ๐ŸŒโณ + +##### Examples + +```javascript +// Validating a date string against a specific locale +console.log(isValidDate('15/04/2022', {countryCode: 'GB'})); // Outputs true for DD/MM/YYYY format (UK) + +// Checking a date string in American format +console.log(isValidDate('04-15-2022', {countryCode: 'US'})); // Outputs true for MM-DD-YYYY format (USA) + +// Testing an invalid date string for a given locale +console.log(isValidDate('2022-15-04', {countryCode: 'US'})); // Outputs false, incorrect format for USA + +// Attempting to validate a date with an unsupported country code +console.log(isValidDate('15.04.2022', {countryCode: 'ZZ'})); // Outputs false, 'ZZ' is not a recognized country code +``` + +๐Ÿ’ก Pro Tip: Employ `isValidDate` for validating user inputs in internationalized applications, ensuring compatibility with locale-specific date formats. Itโ€™s your trusty guardian, assuring that dates align with regional norms. ๐Ÿšฆ๐Ÿ”๐ŸŒ + +#### parseDateTime(dateInput, options:) + +๐Ÿ”๐Ÿ—“๏ธ The `parseDateTime` function is like a time-traveler's best friend, expertly navigating the complex world of dates and times. Whether it's a string or a Date object you're dealing with, this function seamlessly transforms it into a comprehensive, easy-to-digest package of date information, tailored to any locale you desire. ๐ŸŒโฒ๏ธ + +##### Examples + +```javascript +// Parsing a date string with default locale and options +const parsed1 = parseDateTime('18/01/2024'); +console.log(parsed1); // Outputs object with detailed date components +/* + { + "day": "18", + "month": "01", + "year": "2024", + "rawParts": [ + { + "type": "day", + "value": "18" + }, + { + "type": "literal", + "value": "/" + }, + { + "type": "month", + "value": "01" + }, + { + "type": "literal", + "value": "/" + }, + { + "type": "year", + "value": "2024" + } + ], + "formattedDate": "18/01/2024", + "dateObj": "2024-01-17T18:30:00.000Z" + } +*/ + +// Parsing with specific locale and formatting options +const parsed2 = parseDateTime( + '2024-01-23', + { + intlOptions: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }, + locale: 'fr-FR', + } +); +console.log(parsed2); // Outputs object with formatted date in French +/* + { + "weekday": "mardi", + "day": "23", + "month": "janvier", + "year": "2024", + "rawParts": [ + { + "type": "weekday", + "value": "mardi" + }, + { + "type": "literal", + "value": " " + }, + { + "type": "day", + "value": "23" + }, + { + "type": "literal", + "value": " " + }, + { + "type": "month", + "value": "janvier" + }, + { + "type": "literal", + "value": " " + }, + { + "type": "year", + "value": "2024" + } + ], + "formattedDate": "mardi 23 janvier 2024", + "dateObj": "2024-01-22T18:30:00.000Z" + } +*/ + +// Parsing a Date object +const parsed3 = parseDateTime(new Date(2024, 0, 23)); +console.log(parsed3); // Outputs object with date components for January 23, 2024 +/* + { + "day": "23", + "month": "01", + "year": "2024", + "rawParts": [ + { + "type": "day", + "value": "23" + }, + { + "type": "literal", + "value": "/" + }, + { + "type": "month", + "value": "01" + }, + { + "type": "literal", + "value": "/" + }, + { + "type": "year", + "value": "2024" + } + ], + "formattedDate": "23/01/2024", + "dateObj": "2024-01-22T18:30:00.000Z" + } +*/ +``` + +๐Ÿ’ก Pro Tip: Leverage `parseDateTime` in applications where detailed date analysis and manipulation are key, such as in calendar apps, scheduling tools, or date-sensitive data processing. It's like having a Swiss Army knife for all things related to dates and times! ๐Ÿ“…๐Ÿ› ๏ธ + +#### subtract(date, options:) + +๐Ÿ•’๐Ÿ”™ The `subtract` function is like your personal time machine, allowing you to step back in time with ease. It's perfect for those moments when you need to calculate past dates, like figuring out what day it was 'x' days, months, or years ago. Simply tell it the time unit and how far back you want to go, and voilร ! You're traveling back in time! ๐Ÿš€๐Ÿ—“๏ธ + +##### Examples + +```javascript +// Subtracting days +console.log(subtract(new Date(2024, 0, 23), {value: 10, unit: 'days'})); // Go back 10 days from Jan 23, 2024 + +// Subtracting months +console.log(subtract('2024-01-23', {value: 2, unit: 'months'})); // Go back 2 months from Jan 23, 2024 + +// Subtracting years +console.log(subtract(new Date(2024, 0, 23), {value: 5, unit: 'years'})); // Go back 5 years from Jan 23, 2024 +``` + +๐Ÿ’ก Pro Tip: Use the `subtract` function in applications like reminder services, historical data analysis, or anywhere you need to calculate past dates. It's a handy tool to have in your developer toolkit for managing date-based logic! ๐Ÿ“…โฎ๏ธ diff --git a/packages/i18nify-js/package.json b/packages/i18nify-js/package.json index fef2b6bf..d317bfe3 100644 --- a/packages/i18nify-js/package.json +++ b/packages/i18nify-js/package.json @@ -41,6 +41,9 @@ ], "phoneNumber": [ "./lib/esm/phoneNumber/index.d.ts" + ], + "dateTime": [ + "./lib/esm/dateTime/index.d.ts" ] } }, diff --git a/packages/i18nify-js/src/index.ts b/packages/i18nify-js/src/index.ts index c0b5c56a..e550133e 100644 --- a/packages/i18nify-js/src/index.ts +++ b/packages/i18nify-js/src/index.ts @@ -1,3 +1,4 @@ export * from './modules/core'; export * from './modules/currency'; export * from './modules/phoneNumber'; +export * from './modules/dateTime'; diff --git a/packages/i18nify-js/src/modules/.internal/constants.ts b/packages/i18nify-js/src/modules/.internal/constants.ts new file mode 100644 index 00000000..b915afeb --- /dev/null +++ b/packages/i18nify-js/src/modules/.internal/constants.ts @@ -0,0 +1,97 @@ +export const COUNTRY_CODES_TO_LOCALE: { [key: string]: string } = { + AE: 'ar-AE', + AL: 'sq-AL', + AM: 'hy-AM', + AR: 'es-AR', + AU: 'en-AU', + AW: 'nl-AW', + BB: 'en-BB', + BD: 'bn-BD', + BM: 'en-BM', + BN: 'ms-BN', + BO: 'es-BO', + BS: 'en-BS', + BW: 'en-BW', + BZ: 'en-BZ', + CA: 'en-CA', + CH: 'de-CH', + CN: 'zh-CN', + CO: 'es-CO', + CR: 'es-CR', + CU: 'es-CU', + CZ: 'cs-CZ', + DK: 'da-DK', + DO: 'es-DO', + DZ: 'ar-DZ', + EG: 'ar-EG', + ET: 'am-ET', + EU: 'en-EU', + FJ: 'en-FJ', + GB: 'en-GB', + GH: 'en-GH', + GI: 'en-GI', + GM: 'en-GM', + GT: 'es-GT', + GY: 'en-GY', + HK: 'en-HK', + HN: 'es-HN', + HR: 'hr-HR', + HT: 'ht-HT', + HU: 'hu-HU', + ID: 'id-ID', + IL: 'he-IL', + IN: 'en-IN', + JM: 'en-JM', + KE: 'en-KE', + KG: 'ky-KG', + KH: 'km-KH', + KY: 'en-KY', + KZ: 'kk-KZ', + LA: 'lo-LA', + LK: 'si-LK', + LR: 'en-LR', + LS: 'en-LS', + MA: 'ar-MA', + MD: 'ro-MD', + MK: 'mk-MK', + MM: 'my-MM', + MN: 'mn-MN', + MO: 'zh-MO', + MU: 'en-MU', + MV: 'dv-MV', + MW: 'en-MW', + MX: 'es-MX', + MY: 'ms-MY', + NA: 'en-NA', + NG: 'en-NG', + NI: 'es-NI', + NO: 'no-NO', + NP: 'ne-NP', + NZ: 'en-NZ', + PE: 'es-PE', + PG: 'en-PG', + PH: 'en-PH', + PK: 'en-PK', + QA: 'ar-QA', + RU: 'ru-RU', + SA: 'ar-SA', + SC: 'en-SC', + SE: 'sv-SE', + SG: 'en-SG', + SL: 'en-SL', + SO: 'so-SO', + SS: 'en-SS', + SV: 'es-SV', + SZ: 'en-SZ', + TH: 'th-TH', + TT: 'en-TT', + TZ: 'sw-TZ', + US: 'en-US', + UY: 'es-UY', + UZ: 'uz-UZ', + YE: 'ar-YE', + ZA: 'en-ZA', + KW: 'ar-KW', + BH: 'ar-BH', + OM: 'ar-OM' + } \ No newline at end of file diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/add.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/add.test.ts new file mode 100644 index 00000000..7e59cf93 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/add.test.ts @@ -0,0 +1,63 @@ +import add from '../add'; + +describe('dateTime - add', () => { + // Basic Functionality Tests + test('adds days to a date', () => { + const startDate = new Date(2024, 0, 1); // Jan 1, 2024 + expect(add(startDate, {value: 10, unit: 'days'})).toEqual(new Date(2024, 0, 11)); + }); + + test('adds months to a date', () => { + const startDate = new Date(2024, 0, 1); // Jan 1, 2024 + expect(add(startDate,{value: 2, unit: 'months'})).toEqual(new Date(2024, 2, 1)); + }); + + test('adds years to a date', () => { + const startDate = new Date(2024, 0, 1); // Jan 1, 2024 + expect(add(startDate, {value: 1, unit: 'years'})).toEqual(new Date(2025, 0, 1)); + }); + + test('handles negative values', () => { + const startDate = new Date(2024, 0, 10); + expect(add(startDate, {value: -5, unit: 'days'})).toEqual(new Date(2024, 0, 5)); + }); + + test('handles adding zero', () => { + const startDate = new Date(2024, 1, 13); + expect(add(startDate, {value: 0, unit: 'months'})).toEqual(startDate); + }); + + test('handles leap years', () => { + const startDate = new Date(2024, 1, 29); // Feb 29, 2024 + expect(add(startDate, {value: 1, unit: 'years'})).toEqual(new Date(2025, 1, 28)); // Feb 28, 2025 + }); + + test('handles month-end dates', () => { + const startDate = new Date(2024, 0, 31); // Jan 31, 2024 + expect(add(startDate, {value: 1, unit: 'months'})).toEqual(new Date(2024, 1, 29)); // Feb 29, 2024 + }); + + // Invalid Inputs + test('throws error for invalid date string', () => { + expect(() => add('invalid-date', {value: 1, unit: 'days'})).toThrow( + 'Error: Date format not recognized', + ); + }); + + test('throws error for invalid value', () => { + const startDate = new Date(2024, 0, 1); + expect(() => add(startDate, {value: NaN, unit: 'days'})).toThrow( + 'Error: Invalid value passed!', + ); + expect(() => add(startDate, {value: Infinity, unit: 'days'})).toThrow( + 'Error: Invalid value passed!', + ); + }); + + // Type Checking + test('handles Date object and date string inputs', () => { + const startDate = new Date(2024, 0, 1); + const startDateString = '2024-01-01'; + expect(add(startDate, {value: 1, unit: 'days'})).toEqual(add(startDateString, {value: 1, unit: 'days'})); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/formatDate.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/formatDate.test.ts new file mode 100644 index 00000000..69a66629 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/formatDate.test.ts @@ -0,0 +1,58 @@ +import formatDate from '../formatDate'; +import { DateFormatOptions } from '../types'; + +describe('dateTime - formatDate', () => { + // Basic Functionality Tests + test.each([ + ['2024-01-01', 'en-US', undefined, '1/1/2024'], // US format + ['2024-01-01', 'en-GB', undefined, '01/01/2024'], // UK format + [ + '2024-02-29', + 'en-US', + { + day: '2-digit', + month: '2-digit', + year: 'numeric', + } as DateFormatOptions, + '02/29/2024', + ], // Leap year with specific format + ])( + 'formats date "%s" with locale "%s" and options %o to "%s"', + (date, locale, options, expected) => { + expect(formatDate(date, {locale: locale, intlOptions: options})).toBe(expected); + }, + ); + + test('formats end of year date', () => { + expect(formatDate('2024-12-31', {locale: 'en-US'})).toBe('12/31/2024'); + }); + + test('handles invalid date strings', () => { + expect(() => formatDate('invalid-date', {locale: 'en-US'})).toThrow(); + }); + + // Locale and Option Variations + test('formats date with different locales', () => { + const date = '2024-03-01'; + expect(formatDate(date, {locale: 'fr-FR'})).not.toBe(formatDate(date, {locale: 'de-DE'})); + }); + + test('formats date with different options', () => { + const date = '2024-03-01'; + const options1 = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + } as DateFormatOptions; + const options2 = { + year: '2-digit', + month: 'numeric', + day: 'numeric', + } as DateFormatOptions; + + expect(formatDate(date, {locale: 'en-US', intlOptions: options1})).not.toBe( + formatDate(date, {locale: 'en-US', intlOptions: options2}), + ); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/formatDateTime.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/formatDateTime.test.ts new file mode 100644 index 00000000..a565410e --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/formatDateTime.test.ts @@ -0,0 +1,53 @@ +import formatDateTime from '../formatDateTime'; +import { DateTimeFormatOptions } from '../types'; + +describe('dateTime - formatDateTime', () => { + // Basic Functionality Tests + test.each([ + ['2024-01-01T12:00:00', 'en-US', undefined, '1/1/2024, 12:00:00 PM'], // US format with time + ['2024-01-01T00:00:00', 'en-GB', { hour12: false }, '01/01/2024, 00:00:00'], // UK format with midnight time + ['2024-02-29T15:30:00', 'en-US', { hour12: false }, '2/29/2024, 15:30:00'], // Leap year with 24-hour format + ])( + 'formats date "%s" with locale "%s" and options %o to "%s"', + (date, locale, options, expected) => { + expect(formatDateTime(date, {locale: locale, intlOptions :options})).toBe(expected); + }, + ); + + test('formats end of year date with time', () => { + expect(formatDateTime('2024-12-31T23:59:59', {locale: 'en-US'})).toBe( + '12/31/2024, 11:59:59 PM', + ); + }); + + test('handles invalid date strings', () => { + expect(() => formatDateTime('invalid-date', {locale: 'en-US'})).toThrow(); + }); + + // Locale and Option Variations + test('formats date and time with different locales', () => { + const date = '2024-03-01T20:00:00'; + expect(formatDateTime(date, {locale: 'fr-FR'})).not.toBe( + formatDateTime(date, {locale: 'de-DE'}), + ); + }); + + test('formats date and time with different options', () => { + const date = '2024-03-01T20:00:00'; + const options1 = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true, + } as DateTimeFormatOptions; + const options2 = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + } as DateTimeFormatOptions; + expect(formatDateTime(date, {locale: 'en-US', intlOptions :options1})).not.toBe( + formatDateTime(date, {locale: 'en-US', intlOptions: options2}), + ); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/formatTime.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/formatTime.test.ts new file mode 100644 index 00000000..bbc4ebae --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/formatTime.test.ts @@ -0,0 +1,43 @@ +import formatTime from '../formatTime'; +import { DateTimeFormatOptions } from '../types'; + +describe('formatTime function', () => { + // Basic Functionality Tests + test.each([ + ['2024-01-01T12:00:00', 'en-US', undefined, '12:00:00 PM'], // US format 12-hour clock + ['2024-01-01T00:00:00', 'en-GB', { hour12: false }, '00:00:00'], // UK format 24-hour clock + ['2024-01-01T15:30:00', 'en-US', { hour12: false }, '15:30:00'], // US format 24-hour clock + ])( + 'formats time "%s" with locale "%s" and options %o to "%s"', + (date, locale, options, expected) => { + expect(formatTime(date, {locale, intlOptions :options})).toBe(expected); + }, + ); + + test('formats midnight time', () => { + expect(formatTime('2024-01-01T00:00:00', {locale: 'en-US'})).toBe('12:00:00 AM'); + }); + + test('formats end of day time', () => { + expect(formatTime('2024-01-01T23:59:59', {locale: 'en-US'})).toBe('11:59:59 PM'); + }); + + test('formats time with different options', () => { + const date = '2024-03-01T20:00:00'; + const options1 = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true, + } as Omit; + const options2 = { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + } as Omit; + expect(formatTime(date, {locale: 'en-US', intlOptions :options1})).not.toBe( + formatTime(date, {locale: 'en-US', intlOptions: options2}), + ); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/getQuarter.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/getQuarter.test.ts new file mode 100644 index 00000000..99b473a2 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/getQuarter.test.ts @@ -0,0 +1,38 @@ +import getQuarter from '../getQuarter'; + +describe('dateTime - getQuarter', () => { + test('returns 1 for dates in the first quarter', () => { + expect(getQuarter('2024-01-01')).toBe(1); // Beginning of Q1 + expect(getQuarter('2024-02-15')).toBe(1); // Middle of Q1 + expect(getQuarter('2024-03-31')).toBe(1); // End of Q1 + }); + + test('returns 2 for dates in the second quarter', () => { + expect(getQuarter('2024-04-01')).toBe(2); // Beginning of Q2 + expect(getQuarter('2024-05-15')).toBe(2); // Middle of Q2 + expect(getQuarter('2024-06-30')).toBe(2); // End of Q2 + }); + + test('returns 3 for dates in the third quarter', () => { + expect(getQuarter('2024-07-01')).toBe(3); // Beginning of Q3 + expect(getQuarter('2024-08-15')).toBe(3); // Middle of Q3 + expect(getQuarter('2024-09-30')).toBe(3); // End of Q3 + }); + + test('returns 4 for dates in the fourth quarter', () => { + expect(getQuarter('2024-10-01')).toBe(4); // Beginning of Q4 + expect(getQuarter('2024-11-15')).toBe(4); // Middle of Q4 + expect(getQuarter('2024-12-31')).toBe(4); // End of Q4 + }); + + test('handles string and Date inputs', () => { + expect(getQuarter('2024-04-15')).toBe(2); // String input + expect(getQuarter(new Date('2024-04-15'))).toBe(2); // Date object input + }); + + test('throws an error for invalid date inputs', () => { + expect(() => getQuarter('invalid-date')).toThrow( + 'Date format not recognized', + ); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/getRelativeTime.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/getRelativeTime.test.ts new file mode 100644 index 00000000..11963a8f --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/getRelativeTime.test.ts @@ -0,0 +1,45 @@ +import getRelativeTime from '../getRelativeTime'; + +describe('dateTime - getRelativeTime', () => { + const now = new Date(); + + test('returns correct relative time for seconds', () => { + const thirtySecondsAgo = new Date(now.getTime() - 30 * 1000); + expect(getRelativeTime(thirtySecondsAgo, now)).toBe('30 seconds ago'); + const inThirtySeconds = new Date(now.getTime() + 30 * 1000); + expect(getRelativeTime(inThirtySeconds, now)).toBe('in 30 seconds'); + }); + + test('returns correct relative time for minutes', () => { + const fiveMinutesAgo = new Date(now.getTime() - 5 * 60 * 1000); + expect(getRelativeTime(fiveMinutesAgo, now)).toBe('5 minutes ago'); + const inFiveMinutes = new Date(now.getTime() + 5 * 60 * 1000); + expect(getRelativeTime(inFiveMinutes, now)).toBe('in 5 minutes'); + }); + + test('returns correct relative time for hours', () => { + const twoHoursAgo = new Date(now.getTime() - 2 * 60 * 60 * 1000); + expect(getRelativeTime(twoHoursAgo, now)).toBe('2 hours ago'); + const inTwoHours = new Date(now.getTime() + 2 * 60 * 60 * 1000); + expect(getRelativeTime(inTwoHours, now)).toBe('in 2 hours'); + }); + + test('returns correct relative time for days', () => { + const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000); + expect(getRelativeTime(threeDaysAgo, now)).toBe('3 days ago'); + const inThreeDays = new Date(now.getTime() + 3 * 24 * 60 * 60 * 1000); + expect(getRelativeTime(inThreeDays, now)).toBe('in 3 days'); + }); + + test('handles different locales', () => { + const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + expect(getRelativeTime(oneDayAgo, now, {locale: 'en-US'})).toBe('1 day ago'); + expect(getRelativeTime(oneDayAgo, now, {locale: 'fr-FR'})).toBe('il y a 1 jour'); + }); + + test('throws an error for invalid date inputs', () => { + expect(() => getRelativeTime('invalid-date', now)).toThrow( + 'Date format not recognized', + ); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/getWeek.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/getWeek.test.ts new file mode 100644 index 00000000..930e116d --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/getWeek.test.ts @@ -0,0 +1,29 @@ +import getWeek from '../getWeek'; + +describe('dateTime - getWeek', () => { + test('returns correct week number at the beginning of the year', () => { + expect(getWeek('2024-01-01')).toBe(1); // First day of the year + expect(getWeek('2024-01-07')).toBe(2); // Seventh day of the year + }); + + test('returns correct week number at the end of the year', () => { + expect(getWeek('2024-12-31')).toBe(53); // Last day of a leap year + }); + + test('returns correct week number for a leap year', () => { + expect(getWeek('2024-02-29')).toBe(9); // Leap day + }); + + test('returns correct week number for a date in the middle of the year', () => { + expect(getWeek('2024-06-15')).toBe(24); // A date in mid-June + }); + + test('handles string and Date inputs', () => { + expect(getWeek('2024-04-15')).toBe(16); // String input + expect(getWeek(new Date('2024-04-15'))).toBe(16); // Date object input + }); + + test('throws an error for invalid date inputs', () => { + expect(() => getWeek('invalid-date')).toThrow('Date format not recognized'); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/getWeekdays.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/getWeekdays.test.ts new file mode 100644 index 00000000..c546ac16 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/getWeekdays.test.ts @@ -0,0 +1,63 @@ +import getWeekdays from '../getWeekdays'; +import { DateTimeFormatOptions } from '../types'; + +describe('dateTime - getWeekdays', () => { + const testCases = [ + { + locale: 'en-US', + expected: [ + 'Sunday', + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + ], + options: {}, + }, + { + locale: 'de-DE', + expected: [ + 'Sonntag', + 'Montag', + 'Dienstag', + 'Mittwoch', + 'Donnerstag', + 'Freitag', + 'Samstag', + ], + options: {}, + }, + { + locale: 'fr-FR', + expected: [ + 'dimanche', + 'lundi', + 'mardi', + 'mercredi', + 'jeudi', + 'vendredi', + 'samedi', + ], + options: {}, + }, + { + locale: 'en-US', + expected: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'], + options: { weekday: 'short' }, + }, + ]; + + testCases.forEach(({ locale, expected, options }) => { + test(`returns correct weekdays for ${locale} locale`, () => { + const weekdays = getWeekdays({ + locale, + intlOptions :options as DateTimeFormatOptions, + } + ); + expect(weekdays).toHaveLength(7); + expect(weekdays).toEqual(expected); + }); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/isAfter.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/isAfter.test.ts new file mode 100644 index 00000000..a4dd26c5 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/isAfter.test.ts @@ -0,0 +1,33 @@ +import isAfter from '../isAfter'; + +describe('dateTime - isAfter', () => { + test('returns true when the first date is after the second date', () => { + expect(isAfter('2024-01-02', '2024-01-01')).toBe(true); + expect(isAfter(new Date(2024, 0, 2), new Date(2024, 0, 1))).toBe(true); + }); + + test('returns false when the first date is before the second date', () => { + expect(isAfter('2024-01-01', '2024-01-02')).toBe(false); + expect(isAfter(new Date(2024, 0, 1), new Date(2024, 0, 2))).toBe(false); + }); + + test('returns false when the dates are the same', () => { + expect(isAfter('2024-01-01', '2024-01-01')).toBe(false); + expect(isAfter(new Date(2024, 0, 1), new Date(2024, 0, 1))).toBe(false); + }); + + test('handles different time units correctly', () => { + expect(isAfter('2024-01-01T01:00:00', '2024-01-01T00:59:59')).toBe(true); + expect(isAfter('2024-01-01T00:00:01', '2024-01-01T00:00:00')).toBe(true); + }); + + test('handles different date formats', () => { + expect(isAfter('01/02/2024', '01/01/2024')).toBe(true); // MM/DD/YYYY format + expect(isAfter('2024.01.02', '2024.01.01')).toBe(true); // YYYY.MM.DD format + }); + + test('throws an error for invalid date formats', () => { + expect(() => isAfter('invalid-date', '2024-01-01')).toThrow(Error); + expect(() => isAfter('2024-01-01', 'invalid-date')).toThrow(Error); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/isBefore.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/isBefore.test.ts new file mode 100644 index 00000000..68522cfd --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/isBefore.test.ts @@ -0,0 +1,33 @@ +import isBefore from '../isBefore'; + +describe('dateTime - isBefore', () => { + test('returns true when the first date is before the second date', () => { + expect(isBefore('2024-01-01', '2024-01-02')).toBe(true); + expect(isBefore(new Date(2024, 0, 1), new Date(2024, 0, 2))).toBe(true); + }); + + test('returns false when the first date is after the second date', () => { + expect(isBefore('2024-01-02', '2024-01-01')).toBe(false); + expect(isBefore(new Date(2024, 0, 2), new Date(2024, 0, 1))).toBe(false); + }); + + test('returns false when the dates are the same', () => { + expect(isBefore('2024-01-01', '2024-01-01')).toBe(false); + expect(isBefore(new Date(2024, 0, 1), new Date(2024, 0, 1))).toBe(false); + }); + + test('handles different time units correctly', () => { + expect(isBefore('2024-01-01T00:00:00', '2024-01-01T00:00:01')).toBe(true); + expect(isBefore('2024-01-01T23:59:59', '2024-01-02T00:00:00')).toBe(true); + }); + + test('handles different date formats', () => { + expect(isBefore('01/01/2024', '01/02/2024')).toBe(true); // MM/DD/YYYY format + expect(isBefore('2024.01.01', '2024.01.02')).toBe(true); // YYYY.MM.DD format + }); + + test('throws an error for invalid date formats', () => { + expect(() => isBefore('invalid-date', '2024-01-01')).toThrow(Error); + expect(() => isBefore('2024-01-01', 'invalid-date')).toThrow(Error); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/isLeapYear.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/isLeapYear.test.ts new file mode 100644 index 00000000..d8b4e50a --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/isLeapYear.test.ts @@ -0,0 +1,24 @@ +import isLeapYear from '../isLeapYear'; + +describe('dateTime - isLeapYear', () => { + test('returns true for leap years', () => { + expect(isLeapYear(2024)).toBe(true); // Divisible by 4 + expect(isLeapYear(2000)).toBe(true); // Divisible by 100 and 400 + }); + + test('returns false for non-leap years', () => { + expect(isLeapYear(2023)).toBe(false); // Not divisible by 4 + expect(isLeapYear(2100)).toBe(false); // Divisible by 100 but not by 400 + }); + + test('works correctly for century years', () => { + expect(isLeapYear(1900)).toBe(false); // Century year, divisible by 100 but not 400 + expect(isLeapYear(2000)).toBe(true); // Century year, divisible by 100 and 400 + }); + + test('handles cases like Year 0 is considered a leap year, Negative leap year, Negative non-leap century year', () => { + expect(isLeapYear(0)).toBe(true); // Year 0 is considered a leap year + expect(isLeapYear(-4)).toBe(true); // Negative leap year + expect(isLeapYear(-100)).toBe(false); // Negative non-leap century year + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/isSameDay.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/isSameDay.test.ts new file mode 100644 index 00000000..8a013510 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/isSameDay.test.ts @@ -0,0 +1,30 @@ +import isSameDay from '../isSameDay'; + +describe('isSameDay function', () => { + test('returns true for the same dates', () => { + expect(isSameDay('2024-01-01', '2024-01-01')).toBe(true); + expect(isSameDay(new Date(2024, 0, 1), new Date(2024, 0, 1))).toBe(true); + }); + + test('returns true for dates with different times but the same day', () => { + expect(isSameDay('2024-01-01T08:30:00', '2024-01-01T17:45:00')).toBe(true); + expect( + isSameDay(new Date(2024, 0, 1, 8, 30), new Date(2024, 0, 1, 17, 45)), + ).toBe(true); + }); + + test('returns false for different dates', () => { + expect(isSameDay('2024-01-01', '2024-01-02')).toBe(false); + expect(isSameDay(new Date(2024, 0, 1), new Date(2024, 0, 2))).toBe(false); + }); + + test('handles cases around month and year transitions', () => { + expect(isSameDay('2024-01-01', '2023-12-31')).toBe(false); // Year transition + expect(isSameDay('2024-01-31', '2024-02-01')).toBe(false); // Month transition + }); + + test('throws an error for invalid date formats', () => { + expect(() => isSameDay('invalid-date', '2024-01-01')).toThrow(Error); + expect(() => isSameDay('2024-01-01', 'invalid-date')).toThrow(Error); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/isValidDate.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/isValidDate.test.ts new file mode 100644 index 00000000..86be071d --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/isValidDate.test.ts @@ -0,0 +1,34 @@ +import isValidDate from '../isValidDate'; + +describe('dateTime - isValidDate', () => { + test.each([ + // Valid dates for different locales + ['31/12/2020', 'GB', true], // Valid date in British format (DD/MM/YYYY) + ['2020-12-31', 'SE', true], // Valid date in Swedish format (YYYY-MM-DD) + ['12-31-2020', 'US', true], // Valid date in US format (MM-DD-YYYY) + + // Invalid dates + ['31/02/2020', 'GB', false], // Invalid date, February doesn't have 31 days + ['2020-13-01', 'SE', false], // Invalid month + ['02/29/2019', 'US', false], // 2019 is not a leap year + + // Leap year dates + ['29/02/2020', 'GB', true], // Leap year date in British format + ['2020-02-29', 'SE', true], // Leap year date in Swedish format + + // Invalid inputs + ['invalid-date', 'GB', false], // Non-date string + ['2020/02/31', 'SE', false], // Incorrectly formatted date + ['12-31-2020', 'GB', false], // Format mismatch for British + ])( + 'validates date "%s" for countryCode "%s"', + (dateString, countryCode, expected) => { + expect(isValidDate(dateString, {countryCode})).toBe(expected); + }, + ); + + test('handles non-string inputs gracefully', () => { + expect(isValidDate(null as unknown as string, {countryCode: 'GB'})).toBe(false); + expect(isValidDate(12345 as unknown as string, {countryCode: 'GB'})).toBe(false); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/parseDateTime.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/parseDateTime.test.ts new file mode 100644 index 00000000..caccdf81 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/parseDateTime.test.ts @@ -0,0 +1,103 @@ +import parseDateTime from '../parseDateTime'; +import { DateTimeFormatOptions } from '../types'; + +describe('dateTime - parseDateTime', () => { + test('parses standard date input correctly', () => { + const date = '2024-01-01'; + const result = parseDateTime(date, {intlOptions: { + year: 'numeric', + month: 'long', + day: 'numeric', + }}); + expect(result.dateObj).toBeDefined(); + expect(result.formattedDate).toBe('January 1, 2024'); + expect(result.year).toBe('2024'); + expect(result.month).toBe('January'); + expect(result.day).toBe('1'); + }); + + test('formats date according to specified locale', () => { + const date = '2024-01-01'; + const locale = 'de-DE'; // German locale + const result = parseDateTime( + date, + { + intlOptions: { + year: 'numeric', + month: 'long', + day: 'numeric', + }, + locale, + } + ); + expect(result.formattedDate).toBe('1. Januar 2024'); // Format in German + }); + + test('throws error for invalid date input', () => { + const date = 'invalid-date'; + expect(() => parseDateTime(date)).toThrow(Error); + }); + + test('handles leap year correctly', () => { + const date = '2024-02-29'; + const result = parseDateTime(date); + if (result.dateObj) { + expect(result.dateObj.getFullYear()).toBe(2024); + expect(result.dateObj.getMonth()).toBe(1); // Month is 0-indexed + expect(result.dateObj.getDate()).toBe(29); + } + }); + + test('parses time components correctly', () => { + const dateTime = '2024-01-01 23:59:59'; + const result = parseDateTime(dateTime, {intlOptions: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false, + }}); + expect(result.hour).toBe('23'); + expect(result.minute).toBe('59'); + expect(result.second).toBe('59'); + }); + + test('verifies each date component', () => { + const date = '2024-01-01'; + const result = parseDateTime(date, {intlOptions: { + year: 'numeric', + month: 'long', + day: 'numeric', + }}); + expect(result.year).toBe('2024'); + expect(result.month).toBe('January'); + expect(result.day).toBe('1'); + }); + + test('respects intlOptions settings', () => { + const date = '2024-01-01'; + const intlOptions = { year: '2-digit', month: '2-digit', day: '2-digit' }; + const result = parseDateTime(date, {intlOptions: intlOptions as DateTimeFormatOptions}); + expect(result.formattedDate).toBe('01/01/24'); + }); + + test('falls back to system locale if none provided', () => { + const date = '2024-01-01'; + const result = parseDateTime(date); + // This test's outcome may vary based on the system's locale + expect(result.month).toBeDefined(); + }); + + test('handles different date formats', () => { + const dates = ['2024-01-01', '01/01/2024', '01.01.2024']; + dates.forEach((date) => { + const result = parseDateTime(date, {intlOptions: { + year: 'numeric', + month: 'long', + day: 'numeric', + }}); + expect(result.year).toBe('2024'); + expect(result.month).toBe('January'); + expect(result.day).toBe('1'); + }); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/subtract.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/subtract.test.ts new file mode 100644 index 00000000..51f24f2c --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/subtract.test.ts @@ -0,0 +1,53 @@ +import subtract from '../subtract'; + +describe('dateTime - subtract', () => { + // Test subtracting days + test('subtracts days correctly', () => { + const date = new Date(2024, 0, 31); // January 31, 2024 + const result = subtract(date, {value: 10, unit: 'days'}); + expect(result).toEqual(new Date(2024, 0, 21)); // January 21, 2024 + }); + + // Test subtracting months + test('subtracts months correctly', () => { + const date = new Date(2024, 5, 15); // June 15, 2024 + const result = subtract(date, {value: 2, unit: 'months'}); + expect(result).toEqual(new Date(2024, 3, 15)); // April 15, 2024 + }); + + // Test subtracting years + test('subtracts years correctly', () => { + const date = new Date(2024, 0, 1); // January 1, 2024 + const result = subtract(date, {value: 2, unit: 'years'}); + expect(result).toEqual(new Date(2022, 0, 1)); // January 1, 2022 + }); + + // Test leap year + test('handles leap year correctly when subtracting years', () => { + const date = new Date(2024, 1, 29); // February 29, 2024 + const result = subtract(date, {value: 1, unit: 'years'}); + expect(result).toEqual(new Date(2023, 1, 28)); // February 28, 2023 + }); + + // Test month end + test('handles month-end correctly when subtracting months', () => { + const date = new Date(2024, 2, 31); // March 31, 2024 + const result = subtract(date, {value: 1, unit: 'months'}); + expect(result).toEqual(new Date(2024, 1, 29)); // February 29, 2024 + }); + + // Test invalid value + test('throws error for invalid value', () => { + const date = new Date(2024, 0, 1); + expect(() => subtract(date, {value: Infinity, unit: 'days'})).toThrow( + 'Invalid value passed!', + ); + }); + + // Test invalid date + test('throws error for invalid date', () => { + expect(() => subtract('invalid-date', {value: 1, unit: 'days'})).toThrow( + 'Date format not recognized', + ); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/__tests__/utils.test.ts b/packages/i18nify-js/src/modules/dateTime/__tests__/utils.test.ts new file mode 100644 index 00000000..018b4d4a --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/__tests__/utils.test.ts @@ -0,0 +1,59 @@ +import { stringToDate } from '../utils'; + +describe('dateTime - utils - stringToDate', () => { + // Test valid date formats + const validDates = [ + { input: '2024/02/29', expected: new Date(2024, 1, 29) }, + { input: '29/02/2024', expected: new Date(2024, 1, 29) }, + { input: '2024.02.29', expected: new Date(2024, 1, 29) }, + { input: '29-02-2024', expected: new Date(2024, 1, 29) }, + { input: '02/29/2024', expected: new Date(2024, 1, 29) }, + { input: '2024-02-29', expected: new Date(2024, 1, 29) }, + { input: '2024. 02. 29.', expected: new Date(2024, 1, 29) }, + { input: '02.29.2024', expected: new Date(2024, 1, 29) }, + { input: '2024-02-29T12:00:00', expected: new Date(2024, 1, 29, 12, 0, 0) }, + { input: '13/01/2024', expected: new Date('01/13/2024') }, + ]; + + validDates.forEach(({ input, expected }) => { + test(`correctly converts '${input}' to a Date object`, () => { + expect(stringToDate(input)).toEqual(expected); + }); + }); + + // Test invalid date formats + const invalidDateFormats = [ + '2024/13/01', + '2024.13.01', + '01-32-2024', + '2024-13-01', + '2024. 13. 01.', + ]; + + invalidDateFormats.forEach((input) => { + test(`throws an error for invalid date format string '${input}'`, () => { + expect(() => stringToDate(input)).toThrow('Date format not recognized'); + }); + }); + + // Test invalid dates + const invalidDates = ['32/01/2024', '2024-02-29T25:00:00']; + + invalidDates.forEach((input) => { + test(`throws an error for invalid date string '${input}'`, () => { + expect(() => stringToDate(input)).toThrow('Invalid Date!'); + }); + }); + + // Test valid leap year date + test('correctly identifies leap year dates', () => { + expect(stringToDate('2024/02/29')).toEqual(new Date(2024, 1, 29)); + }); + + // Test for timestamps + test('correctly converts timestamps', () => { + expect(stringToDate('2024-02-29T23:59:59')).toEqual( + new Date(2024, 1, 29, 23, 59, 59), + ); + }); +}); diff --git a/packages/i18nify-js/src/modules/dateTime/add.ts b/packages/i18nify-js/src/modules/dateTime/add.ts new file mode 100644 index 00000000..4fa3ea49 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/add.ts @@ -0,0 +1,64 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import { DateInput } from './types'; +import { stringToDate } from './utils'; + +/** + * Adds a specified amount of time to a date. + * + * @param date The original date. + * @param options config object. + * @returns A new Date object with the time added. + */ +const add = ( + date: DateInput, + options: { + value: number, + unit: 'days' | 'months' | 'years', + }, +): Date => { + const {value, unit} = options; + if ( + (value !== 0 && !Number(value)) || + value === Infinity || + value === -Infinity + ) + throw new Error('Invalid value passed!'); + + date = + typeof date === 'string' ? new Date(stringToDate(date)) : new Date(date); + + let initialDay; + + switch (unit) { + case 'days': + date.setDate(date.getDate() + value); + break; + case 'months': + initialDay = date.getDate(); + date.setMonth(date.getMonth() + value); + // Adjust for month-end dates + if (date.getDate() !== initialDay) { + date.setDate(0); // Set to the last day of the previous month + } + break; + case 'years': + // Special handling for leap years + if (date.getMonth() === 1 && date.getDate() === 29) { + // February 29 + const year = date.getFullYear() + value; + if (new Date(year, 1, 29).getMonth() === 1) { + // If the new year is a leap year, set to February 29 + date.setFullYear(year); + } else { + // If the new year is not a leap year, set to February 28 + date.setFullYear(year, 1, 28); + } + } else { + date.setFullYear(date.getFullYear() + value); + } + break; + } + return date; +}; + +export default withErrorBoundary(add); diff --git a/packages/i18nify-js/src/modules/dateTime/constants.ts b/packages/i18nify-js/src/modules/dateTime/constants.ts new file mode 100644 index 00000000..32def42c --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/constants.ts @@ -0,0 +1,15 @@ +export const ALLOWED_FORMAT_PARTS_KEYS = [ + 'day', + 'dayPeriod', + 'era', + 'fractionalSecond', + 'hour', + 'minute', + 'month', + 'relatedYear', + 'second', + 'timeZone', + 'weekday', + 'year', + 'yearName', +] as const; diff --git a/packages/i18nify-js/src/modules/dateTime/data/localeDateFormats.ts b/packages/i18nify-js/src/modules/dateTime/data/localeDateFormats.ts new file mode 100644 index 00000000..5a082a96 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/data/localeDateFormats.ts @@ -0,0 +1,100 @@ +export const LOCALE_DATE_FORMATS: { [key: string]: string } = { + 'ar-AE': 'DD/MM/YYYY', // Arabic (U.A.E.) + 'sq-AL': 'DD.MM.YYYY', // Albanian (Albania) + 'hy-AM': 'DD.MM.YYYY', // Armenian (Armenia) + 'es-AR': 'DD/MM/YYYY', // Spanish (Argentina) + 'en-AU': 'DD/MM/YYYY', // English (Australia) + 'nl-AW': 'DD-MM-YYYY', // Dutch (Aruba) + 'en-BB': 'MM/DD/YYYY', // English (Barbados) + 'bn-BD': 'DD/MM/YYYY', // Bengali (Bangladesh) + 'en-BM': 'MM/DD/YYYY', // English (Bermuda) + 'ms-BN': 'DD/MM/YYYY', // Malay (Brunei) + 'es-BO': 'DD/MM/YYYY', // Spanish (Bolivia) + 'en-BS': 'MM/DD/YYYY', // English (Bahamas) + 'en-BW': 'DD/MM/YYYY', // English (Botswana) + 'en-BZ': 'MM/DD/YYYY', // English (Belize) + 'en-CA': 'DD/MM/YYYY', // English (Canada) + 'de-CH': 'DD.MM.YYYY', // German (Switzerland) + 'zh-CN': 'YYYY/MM/DD', // Chinese (China) + 'es-CO': 'DD/MM/YYYY', // Spanish (Colombia) + 'es-CR': 'DD/MM/YYYY', // Spanish (Costa Rica) + 'es-CU': 'DD/MM/YYYY', // Spanish (Cuba) + 'cs-CZ': 'DD.MM.YYYY', // Czech (Czech Republic) + 'da-DK': 'DD-MM-YYYY', // Danish (Denmark) + 'es-DO': 'DD/MM/YYYY', // Spanish (Dominican Republic) + 'ar-DZ': 'DD/MM/YYYY', // Arabic (Algeria) + 'ar-EG': 'DD/MM/YYYY', // Arabic (Egypt) + 'am-ET': 'DD/MM/YYYY', // Amharic (Ethiopia) + 'en-EU': 'DD/MM/YYYY', // English (European Union) + 'en-FJ': 'DD/MM/YYYY', // English (Fiji) + 'en-GB': 'DD/MM/YYYY', // English (United Kingdom) + 'en-GH': 'DD/MM/YYYY', // English (Ghana) + 'en-GI': 'DD/MM/YYYY', // English (Gibraltar) + 'en-GM': 'DD/MM/YYYY', // English (Gambia) + 'es-GT': 'DD/MM/YYYY', // Spanish (Guatemala) + 'en-GY': 'DD/MM/YYYY', // English (Guyana) + 'en-HK': 'DD/MM/YYYY', // English (Hong Kong) + 'es-HN': 'DD/MM/YYYY', // Spanish (Honduras) + 'hr-HR': 'DD.MM.YYYY', // Croatian (Croatia) + 'ht-HT': 'MM/DD/YYYY', // Haitian (Haiti) + 'hu-HU': 'YYYY. MM. DD.', // Hungarian (Hungary) + 'id-ID': 'DD/MM/YYYY', // Indonesian (Indonesia) + 'he-IL': 'DD/MM/YYYY', // Hebrew (Israel) + 'en-IN': 'DD-MM-YYYY', // English (India) + 'en-JM': 'MM/DD/YYYY', // English (Jamaica) + 'en-KE': 'DD/MM/YYYY', // English (Kenya) + 'ky-KG': 'DD.MM.YYYY', // Kyrgyz (Kyrgyzstan) + 'km-KH': 'DD/MM/YYYY', // Khmer (Cambodia) + 'en-KY': 'MM/DD/YYYY', // English (Cayman Islands) + 'kk-KZ': 'DD.MM.YYYY', // Kazakh (Kazakhstan) + 'lo-LA': 'DD/MM/YYYY', // Lao (Laos) + 'si-LK': 'YYYY-MM-DD', // Sinhala (Sri Lanka) + 'en-LR': 'MM/DD/YYYY', // English (Liberia) + 'en-LS': 'DD/MM/YYYY', // English (Lesotho) + 'ar-MA': 'DD/MM/YYYY', // Arabic (Morocco) + 'ro-MD': 'DD.MM.YYYY', // Romanian (Moldova) + 'mk-MK': 'DD.MM.YYYY', // Macedonian (North Macedonia) + 'my-MM': 'DD/MM/YYYY', // Burmese (Myanmar) + 'mn-MN': 'YYYY.MM.DD', // Mongolian (Mongolia) + 'zh-MO': 'DD/MM/YYYY', // Chinese (Macao) + 'en-MU': 'DD/MM/YYYY', // English (Mauritius) + 'dv-MV': 'DD/MM/YYYY', // Divehi (Maldives) + 'en-MW': 'DD/MM/YYYY', // English (Malawi) + 'es-MX': 'DD/MM/YYYY', // Spanish (Mexico) + 'ms-MY': 'DD/MM/YYYY', // Malay (Malaysia) + 'en-NA': 'DD/MM/YYYY', // English (Namibia) + 'en-NG': 'DD/MM/YYYY', // English (Nigeria) + 'es-NI': 'DD/MM/YYYY', // Spanish (Nicaragua) + 'no-NO': 'DD.MM.YYYY', // Norwegian (Norway) + 'ne-NP': 'YYYY/MM/DD', // Nepali (Nepal) + 'en-NZ': 'DD/MM/YYYY', // English (New Zealand) + 'es-PE': 'DD/MM/YYYY', // Spanish (Peru) + 'en-PG': 'DD/MM/YYYY', // English (Papua New Guinea) + 'en-PH': 'MM/DD/YYYY', // English (Philippines) + 'en-PK': 'DD/MM/YYYY', // English (Pakistan) + 'ar-QA': 'DD/MM/YYYY', // Arabic (Qatar) + 'ru-RU': 'DD.MM.YYYY', // Russian (Russia) + 'ar-SA': 'DD/MM/YYYY', // Arabic (Saudi Arabia) + 'en-SC': 'DD/MM/YYYY', // English (Seychelles) + 'sv-SE': 'YYYY-MM-DD', // Swedish (Sweden) + 'en-SG': 'DD/MM/YYYY', // English (Singapore) + 'en-SL': 'DD/MM/YYYY', // English (Sierra Leone) + 'so-SO': 'DD/MM/YYYY', // Somali (Somalia) + 'en-SS': 'DD/MM/YYYY', // English (South Sudan) + 'es-SV': 'DD/MM/YYYY', // Spanish (El Salvador) + 'en-SZ': 'DD/MM/YYYY', // English (Eswatini) + 'th-TH': 'DD/MM/YYYY', // Thai (Thailand) + 'en-TT': 'MM/DD/YYYY', // English (Trinidad and Tobago) + 'sw-TZ': 'DD/MM/YYYY', // Swahili (Tanzania) + 'en-US': 'MM-DD-YYYY', // English (United States) + 'es-UY': 'DD/MM/YYYY', // Spanish (Uruguay) + 'uz-UZ': 'DD/MM/YYYY', // Uzbek (Uzbekistan) + 'ar-YE': 'DD/MM/YYYY', // Arabic (Yemen) + 'en-ZA': 'YYYY/MM/DD', // English (South Africa) + 'ar-KW': 'DD/MM/YYYY', // Arabic (Kuwait) + 'ar-BH': 'DD/MM/YYYY', // Arabic (Bahrain) + 'ar-OM': 'DD/MM/YYYY', // Arabic (Oman) + 'fr-FR': 'DD/MM/YYYY', // French (France) + 'it-IT': 'DD/MM/YYYY', // Italian (Italy) + 'es-ES': 'DD/MM/YYYY', // Spanish (Spain) +}; diff --git a/packages/i18nify-js/src/modules/dateTime/data/supportedDateFormats.ts b/packages/i18nify-js/src/modules/dateTime/data/supportedDateFormats.ts new file mode 100644 index 00000000..cebd516c --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/data/supportedDateFormats.ts @@ -0,0 +1,142 @@ +import { SupportedDateFormats } from '../types'; + +export const supportedDateFormats: SupportedDateFormats[] = [ + // Date formats + { + regex: /^(\d{4})\/(0[1-9]|1[0-2])\/(\d{2})$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + format: 'YYYY/MM/DD', + }, + { + regex: /^(\d{2})\/(0[1-9]|1[0-2])\/(\d{4})$/, + yearIndex: 3, + monthIndex: 2, + dayIndex: 1, + format: 'DD/MM/YYYY', + }, + { + regex: /^(\d{4})\.(0[1-9]|1[0-2])\.(\d{2})$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + format: 'YYYY.MM.DD', + }, + { + regex: /^(\d{2})-(0[1-9]|1[0-2])-(\d{4})$/, + yearIndex: 3, + monthIndex: 2, + dayIndex: 1, + format: 'DD-MM-YYYY', + }, + { + regex: /^(0[1-9]|1[0-2])\/(\d{2})\/(\d{4})$/, + yearIndex: 3, + monthIndex: 1, + dayIndex: 2, + format: 'MM/DD/YYYY', + }, + { + regex: /^(\d{4})-(0[1-9]|1[0-2])-(\d{2})$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + format: 'YYYY-MM-DD', + }, + { + regex: /^(\d{4})\.\s*(0[1-9]|1[0-2])\.\s*(\d{2})\.\s*$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + format: 'YYYY. MM. DD.', + }, + { + regex: /^(\d{2})\.(0[1-9]|1[0-2])\.(\d{4})$/, + yearIndex: 3, + monthIndex: 2, + dayIndex: 1, + format: 'DD.MM.YYYY', + }, + { + regex: /^(0[1-9]|1[0-2])\.(\d{2})\.(\d{4})$/, + yearIndex: 3, + monthIndex: 1, + dayIndex: 2, + format: 'MM.DD.YYYY', + }, + + // Timestamp formats + { + regex: /^(\d{4})\/(0[1-9]|1[0-2])\/(\d{2}) (\d{2}):(\d{2}):(\d{2})$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + hourIndex: 4, + minuteIndex: 5, + secondIndex: 6, + format: 'YYYY/MM/DD HH:MM:SS', + }, + { + regex: /^(\d{2})\/(0[1-9]|1[0-2])\/(\d{4}) (\d{2}):(\d{2}):(\d{2})$/, + yearIndex: 3, + monthIndex: 2, + dayIndex: 1, + hourIndex: 4, + minuteIndex: 5, + secondIndex: 6, + format: 'DD/MM/YYYY HH:MM:SS', + }, + { + regex: /^(\d{4})-(0[1-9]|1[0-2])-(\d{2}) (\d{2}):(\d{2}):(\d{2})$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + hourIndex: 4, + minuteIndex: 5, + secondIndex: 6, + format: 'YYYY-MM-DD HH:MM:SS', + }, + { + regex: /^(\d{2})-(0[1-9]|1[0-2])-(\d{4}) (\d{2}):(\d{2}):(\d{2})$/, + yearIndex: 3, + monthIndex: 2, + dayIndex: 1, + hourIndex: 4, + minuteIndex: 5, + secondIndex: 6, + format: 'DD-MM-YYYY HH:MM:SS', + }, + { + regex: /^(\d{4})\.(0[1-9]|1[0-2])\.(\d{2}) (\d{2}):(\d{2}):(\d{2})$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + hourIndex: 4, + minuteIndex: 5, + secondIndex: 6, + format: 'YYYY.MM.DD HH:MM:SS', + }, + { + regex: /^(\d{2})\.(0[1-9]|1[0-2])\.(\d{4}) (\d{2}):(\d{2}):(\d{2})$/, + yearIndex: 3, + monthIndex: 2, + dayIndex: 1, + hourIndex: 4, + minuteIndex: 5, + secondIndex: 6, + format: 'DD.MM.YYYY HH:MM:SS', + }, + + // ISO 8601 Timestamp format + { + regex: /^(\d{4})-(0[1-9]|1[0-2])-(\d{2})T(\d{2}):(\d{2}):(\d{2})$/, + yearIndex: 1, + monthIndex: 2, + dayIndex: 3, + hourIndex: 4, + minuteIndex: 5, + secondIndex: 6, + format: 'YYYY-MM-DDTHH:MM:SS', + }, +]; diff --git a/packages/i18nify-js/src/modules/dateTime/formatDate.ts b/packages/i18nify-js/src/modules/dateTime/formatDate.ts new file mode 100644 index 00000000..0171c56f --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/formatDate.ts @@ -0,0 +1,52 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import state from '../.internal/state'; +import { getLocale } from '../.internal/utils'; +import formatDateTime from './formatDateTime'; +import { + DateInput, + Locale, + DateTimeFormatOptions, + DateFormatOptions, +} from './types'; + +/** + * Formats date based on the locale. + * @param {DateInput} date - Date object or date string. + * @param options - config object. + * @returns {string} Formatted date string. + */ +const formatDate = ( + date: DateInput, + options: { + locale?: Locale, + intlOptions?: DateFormatOptions, + } = {}, +): string => { + /** retrieve locale from below areas in order of preference + * 1. locale (used in case if someone wants to override locale just for a specific area and not globally) + * 2. i18nState.locale (uses locale set globally) + * 3. navigator (in case locale is not passed or set, use it from browser's navigator) + * */ + if (!options.locale) options.locale = state.getState().locale || getLocale(); + + const fullOptions: DateTimeFormatOptions = { + ...options.intlOptions, + timeStyle: undefined, + }; + + let formattedDate; + + try { + formattedDate = formatDateTime(date, {locale: options.locale, intlOptions: fullOptions}).split(',')[0]; + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); + } else { + throw new Error(`An unknown error occurred = ${err}`); + } + } + + return formattedDate; +}; + +export default withErrorBoundary(formatDate); diff --git a/packages/i18nify-js/src/modules/dateTime/formatDateTime.ts b/packages/i18nify-js/src/modules/dateTime/formatDateTime.ts new file mode 100644 index 00000000..17d5fb4f --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/formatDateTime.ts @@ -0,0 +1,57 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import state from '../.internal/state'; +import { getLocale } from '../.internal/utils'; +import { DateInput, Locale } from './types'; +import { stringToDate } from './utils'; + +/** + * Formats date and time based on the locale. + * @param {DateInput} date - Date object or date string. + * @param options - Config object. + * @returns {string} Formatted date and time string. + */ +const formatDateTime = ( + date: DateInput, + options: { + locale?: Locale, + intlOptions?: Intl.DateTimeFormatOptions, + } = {}, +): string => { + /** retrieve locale from below areas in order of preference + * 1. locale (used in case if someone wants to override locale just for a specific area and not globally) + * 2. i18nState.locale (uses locale set globally) + * 3. navigator (in case locale is not passed or set, use it from browser's navigator) + * */ + if (!options.locale) options.locale = state.getState().locale || getLocale(); + + date = + typeof date === 'string' ? new Date(stringToDate(date)) : new Date(date); + + // Ensure default options include date and time components + const defaultOptions: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: true, // Use 12-hour format by default, can be overridden by intlOptions + ...options.intlOptions, + }; + + let formatter; + + try { + formatter = new Intl.DateTimeFormat(options.locale, defaultOptions); + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); + } else { + throw new Error(`An unknown error occurred = ${err}`); + } + } + + return formatter.format(date); +}; + +export default withErrorBoundary(formatDateTime); diff --git a/packages/i18nify-js/src/modules/dateTime/formatTime.ts b/packages/i18nify-js/src/modules/dateTime/formatTime.ts new file mode 100644 index 00000000..4e5b71c9 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/formatTime.ts @@ -0,0 +1,54 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import state from '../.internal/state'; +import { getLocale } from '../.internal/utils'; +import formatDateTime from './formatDateTime'; +import { + DateInput, + Locale, + DateTimeFormatOptions, + TimeFormatOptions, +} from './types'; + +/** + * Formats time based on the locale. + * @param {DateInput} date - Date object or date string. + * @param options - Config object + * @returns {string} Formatted time string. + */ +const formatTime = ( + date: DateInput, + options: { + locale?: Locale, + intlOptions?: TimeFormatOptions, + } = {}, +): string => { + /** retrieve locale from below areas in order of preference + * 1. locale (used in case if someone wants to override locale just for a specific area and not globally) + * 2. i18nState.locale (uses locale set globally) + * 3. navigator (in case locale is not passed or set, use it from browser's navigator) + * */ + if (!options.locale) options.locale = state.getState().locale || getLocale(); + + const fullOptions: DateTimeFormatOptions = { + ...options.intlOptions, + dateStyle: undefined, + }; + + let formattedTime; + + try { + formattedTime = formatDateTime(date, {locale: options.locale, intlOptions: fullOptions}) + .split(',')[1] + .trim(); + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); + } else { + throw new Error(`An unknown error occurred = ${err}`); + } + } + + return formattedTime; +}; + +export default withErrorBoundary(formatTime); diff --git a/packages/i18nify-js/src/modules/dateTime/getQuarter.ts b/packages/i18nify-js/src/modules/dateTime/getQuarter.ts new file mode 100644 index 00000000..b3e28eba --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/getQuarter.ts @@ -0,0 +1,17 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import { DateInput } from './types'; +import { stringToDate } from './utils'; + +/** + * Determines the quarter of the year for a given date. + * + * @param date The date to determine the quarter for. + * @returns The quarter of the year (1-4). + */ +const getQuarter = (date: DateInput): number => { + date = + typeof date === 'string' ? new Date(stringToDate(date)) : new Date(date); + return Math.ceil((date.getMonth() + 1) / 3); +}; + +export default withErrorBoundary(getQuarter); diff --git a/packages/i18nify-js/src/modules/dateTime/getRelativeTime.ts b/packages/i18nify-js/src/modules/dateTime/getRelativeTime.ts new file mode 100644 index 00000000..1e46663c --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/getRelativeTime.ts @@ -0,0 +1,92 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import state from '../.internal/state'; +import { getLocale } from '../.internal/utils'; +import { DateInput, Locale } from './types'; +import { stringToDate } from './utils'; + +/** + * Provides a relative time string (e.g., '3 hours ago', 'in 2 days'). + * This function calculates the difference between the given date and the base date, + * then formats it in a locale-sensitive manner. It allows customization of the output + * through Intl.RelativeTimeFormat options. + * + * @param date - The date to compare. + * @param baseDate - The date to compare against (default: current date). + * @param options - Config object. + * @returns The relative time as a string. + */ +const getRelativeTime = ( + date: DateInput, + baseDate: DateInput = new Date(), + options: { + locale?: Locale, + intlOptions?: Intl.RelativeTimeFormatOptions, + } = {}, +): string => { + date = + typeof date === 'string' ? new Date(stringToDate(date)) : new Date(date); + + baseDate = + typeof baseDate === 'string' + ? new Date(stringToDate(baseDate)) + : new Date(baseDate); + /** retrieve locale from below areas in order of preference + * 1. locale (used in case if someone wants to override locale just for a specific area and not globally) + * 2. i18nState.locale (uses locale set globally) + * 3. navigator (in case locale is not passed or set, use it from browser's navigator) + * */ + if (!options.locale) options.locale = state.getState().locale || getLocale(); + + const diffInSeconds = (date.getTime() - baseDate.getTime()) / 1000; + + // Define time units in seconds + const minute = 60; + const hour = minute * 60; + const day = hour * 24; + const week = day * 7; + const month = day * 30; + const year = day * 365; + + let value: number; + let unit: Intl.RelativeTimeFormatUnit; + + if (Math.abs(diffInSeconds) < minute) { + value = diffInSeconds; + unit = 'second'; + } else if (Math.abs(diffInSeconds) < hour) { + value = diffInSeconds / minute; + unit = 'minute'; + } else if (Math.abs(diffInSeconds) < day) { + value = diffInSeconds / hour; + unit = 'hour'; + } else if (Math.abs(diffInSeconds) < week) { + value = diffInSeconds / day; + unit = 'day'; + } else if (Math.abs(diffInSeconds) < month) { + value = diffInSeconds / week; + unit = 'week'; + } else if (Math.abs(diffInSeconds) < year) { + value = diffInSeconds / month; + unit = 'month'; + } else { + value = diffInSeconds / year; + unit = 'year'; + } + + let relativeTime; + + try { + const rtf = new Intl.RelativeTimeFormat(options.locale, options.intlOptions); + relativeTime = rtf.format(Math.round(value), unit); + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); + } else { + throw new Error(`An unknown error occurred = ${err}`); + } + } + + return relativeTime; +}; + +export default withErrorBoundary(getRelativeTime); diff --git a/packages/i18nify-js/src/modules/dateTime/getWeek.ts b/packages/i18nify-js/src/modules/dateTime/getWeek.ts new file mode 100644 index 00000000..761efd15 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/getWeek.ts @@ -0,0 +1,19 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import { DateInput } from './types'; +import { stringToDate } from './utils'; + +/** + * Calculates the week number of the year for a given date. + * + * @param date The date to calculate the week number for. + * @returns The week number of the year. + */ +const getWeek = (date: DateInput): number => { + date = + typeof date === 'string' ? new Date(stringToDate(date)) : new Date(date); + const firstDayOfYear = new Date(date.getFullYear(), 0, 1); + const pastDaysOfYear = (date.getTime() - firstDayOfYear.getTime()) / 86400000; // 86400000 represents the number of milliseconds in a day + return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7); +}; + +export default withErrorBoundary(getWeek); diff --git a/packages/i18nify-js/src/modules/dateTime/getWeekdays.ts b/packages/i18nify-js/src/modules/dateTime/getWeekdays.ts new file mode 100644 index 00000000..ba50581e --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/getWeekdays.ts @@ -0,0 +1,47 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import state from '../.internal/state'; +import { getLocale } from '../.internal/utils'; +import { Locale } from './types'; + +/** + * Returns an array of weekdays according to the specified locale. + * + * @param options Config object + * @returns An array of weekday names. + */ +const getWeekdays = ( + options: { + locale?: Locale, + intlOptions: Intl.DateTimeFormatOptions, + }, +): string[] => { + try { + /** retrieve locale from below areas in order of preference + * 1. locale (used in case if someone wants to override locale just for a specific area and not globally) + * 2. i18nState.locale (uses locale set globally) + * 3. navigator (in case locale is not passed or set, use it from browser's navigator) + * */ + if (!options.locale) options.locale = state.getState().locale || getLocale(); + if (!options.intlOptions.weekday) options.intlOptions.weekday = 'long'; + + const formatter = new Intl.DateTimeFormat(options.locale, options.intlOptions); + + /** The date January 1, 1970, is a well-known reference point in computing known as the Unix epoch. + * It's the date at which time is measured for Unix systems, making it a consistent and reliable choice for date calculations. + * The choice of the date January 4, 1970, as the starting point is significant. + * January 4, 1970, was a Sunday. + * Since weeks typically start on Sunday or Monday in most locales, starting from a known Sunday allows the function to cycle through a complete week, capturing all weekdays in the order they appear for the given locale. + * */ + return Array.from({ length: 7 }, (_, i) => + formatter.format(new Date(1970, 0, 4 + i)), + ); + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); + } else { + throw new Error(`An unknown error occurred = ${err}`); + } + } +}; + +export default withErrorBoundary(getWeekdays); diff --git a/packages/i18nify-js/src/modules/dateTime/index.ts b/packages/i18nify-js/src/modules/dateTime/index.ts new file mode 100644 index 00000000..4f6b7f9f --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/index.ts @@ -0,0 +1,20 @@ +/** + * This module provides functions for formatting and manipulating dates and times + * in a locale-sensitive manner using the JavaScript Intl API & Date object. + */ + +export { default as add } from './add'; +export { default as formatDate } from './formatDate'; +export { default as formatDateTime } from './formatDateTime'; +export { default as formatTime } from './formatTime'; +export { default as getQuarter } from './getQuarter'; +export { default as getRelativeTime } from './getRelativeTime'; +export { default as getWeek } from './getWeek'; +export { default as getWeekdays } from './getWeekdays'; +export { default as isAfter } from './isAfter'; +export { default as isBefore } from './isBefore'; +export { default as isLeapYear } from './isLeapYear'; +export { default as isSameDay } from './isSameDay'; +export { default as isValidDate } from './isValidDate'; +export { default as parseDateTime } from './parseDateTime'; +export { default as subtract } from './subtract'; diff --git a/packages/i18nify-js/src/modules/dateTime/isAfter.ts b/packages/i18nify-js/src/modules/dateTime/isAfter.ts new file mode 100644 index 00000000..6cc0919f --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/isAfter.ts @@ -0,0 +1,22 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import { DateInput } from './types'; +import { stringToDate } from './utils'; + +/** + * Compares two dates to determine if the first is after the second. + * @param {DateInput} date1 - First date object or date string. + * @param {DateInput} date2 - Second date object or date string. + * @returns {boolean} True if date1 is after date2. + */ +const isAfter = (date1: DateInput, date2: DateInput): boolean => { + date1 = + typeof date1 === 'string' ? new Date(stringToDate(date1)) : new Date(date1); + date2 = + typeof date2 === 'string' ? new Date(stringToDate(date2)) : new Date(date2); + + const dateObj1: Date = date1 instanceof Date ? date1 : new Date(date1); + const dateObj2: Date = date2 instanceof Date ? date2 : new Date(date2); + return dateObj1 > dateObj2; +}; + +export default withErrorBoundary(isAfter); diff --git a/packages/i18nify-js/src/modules/dateTime/isBefore.ts b/packages/i18nify-js/src/modules/dateTime/isBefore.ts new file mode 100644 index 00000000..3fbdf441 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/isBefore.ts @@ -0,0 +1,22 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import { DateInput } from './types'; +import { stringToDate } from './utils'; + +/** + * Compares two dates to determine if the first is before the second. + * @param {DateInput} date1 - First date object or date string. + * @param {DateInput} date2 - Second date object or date string. + * @returns {boolean} True if date1 is before date2. + */ +const isBefore = (date1: DateInput, date2: DateInput): boolean => { + date1 = + typeof date1 === 'string' ? new Date(stringToDate(date1)) : new Date(date1); + date2 = + typeof date2 === 'string' ? new Date(stringToDate(date2)) : new Date(date2); + + const dateObj1: Date = date1 instanceof Date ? date1 : new Date(date1); + const dateObj2: Date = date2 instanceof Date ? date2 : new Date(date2); + return dateObj1 < dateObj2; +}; + +export default withErrorBoundary(isBefore); diff --git a/packages/i18nify-js/src/modules/dateTime/isLeapYear.ts b/packages/i18nify-js/src/modules/dateTime/isLeapYear.ts new file mode 100644 index 00000000..4ba58443 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/isLeapYear.ts @@ -0,0 +1,13 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; + +/** + * Checks if a given year is a leap year. + * + * @param year The year to check. + * @returns True if the year is a leap year, false otherwise. + */ +const isLeapYear = (year: number): boolean => { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; +}; + +export default withErrorBoundary(isLeapYear); diff --git a/packages/i18nify-js/src/modules/dateTime/isSameDay.ts b/packages/i18nify-js/src/modules/dateTime/isSameDay.ts new file mode 100644 index 00000000..658a1d46 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/isSameDay.ts @@ -0,0 +1,25 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import { DateInput } from './types'; +import { stringToDate } from './utils'; + +/** + * Checks if two dates fall on the same day. + * + * @param date1 The first date. + * @param date2 The second date. + * @returns True if both dates are on the same day, false otherwise. + */ +const isSameDay = (date1: DateInput, date2: DateInput): boolean => { + date1 = + typeof date1 === 'string' ? new Date(stringToDate(date1)) : new Date(date1); + date2 = + typeof date2 === 'string' ? new Date(stringToDate(date2)) : new Date(date2); + + return ( + date1.getDate() === date2.getDate() && + date1.getMonth() === date2.getMonth() && + date1.getFullYear() === date2.getFullYear() + ); +}; + +export default withErrorBoundary(isSameDay); diff --git a/packages/i18nify-js/src/modules/dateTime/isValidDate.ts b/packages/i18nify-js/src/modules/dateTime/isValidDate.ts new file mode 100644 index 00000000..ab059722 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/isValidDate.ts @@ -0,0 +1,66 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import { COUNTRY_CODES_TO_LOCALE } from '../.internal/constants'; +import { LOCALE_DATE_FORMATS } from './data/localeDateFormats'; + +/** + * Checks if a given string is a valid date according to a specific locale's date format. + * + * @param dateString The date string to validate. + * @param options Config object + * @returns True if the dateString is a valid date according to the locale's format, false otherwise. + */ +const isValidDate = (dateString: string, options: {countryCode: string}): boolean => { + const locale = COUNTRY_CODES_TO_LOCALE[options.countryCode]; + + // Type guard to ensure dateString is a string + if (typeof dateString !== 'string') { + return false; + } + + // Determine the date format based on the given locale + const dateFormat = LOCALE_DATE_FORMATS[locale]; + const delimiter = /[-/.]/; + const [part1, part2, part3] = dateString.split(delimiter); + + let year: string, month: string, day: string; + switch (dateFormat) { + case 'DD/MM/YYYY': + case 'DD.MM.YYYY': + case 'DD-MM-YYYY': + // Extract day, month, and year for formats where the day comes first + [day, month, year] = [part1, part2, part3]; + break; + case 'YYYY/MM/DD': + case 'YYYY-MM-DD': + case 'YYYY.MM.DD': + // Extract year, month, and day for formats where the year comes first + [year, month, day] = [part1, part2, part3]; + break; + case 'MM-DD-YYYY': + // Extract month, day and year for formats where the year comes first + [month, day, year] = [part1, part2, part3]; + break; + default: + // Return false for unrecognized formats + return false; + } + + try { + // Parsing and validating the date components + const parsedDate = new Date( + parseInt(year), + parseInt(month) - 1, + parseInt(day), + ); + return ( + parsedDate.getFullYear() === parseInt(year) && + parsedDate.getMonth() === parseInt(month) - 1 && + parsedDate.getDate() === parseInt(day) + ); + } catch (e) { + // Return false in case of any parsing errors + return false; + } +}; + +export default withErrorBoundary(isValidDate); diff --git a/packages/i18nify-js/src/modules/dateTime/parseDateTime.ts b/packages/i18nify-js/src/modules/dateTime/parseDateTime.ts new file mode 100644 index 00000000..5c5399d4 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/parseDateTime.ts @@ -0,0 +1,70 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import state from '../.internal/state'; +import { getLocale } from '../.internal/utils'; +import { + DateInput, + FormattedPartsObject, + Locale, + ParsedDateTime, +} from './types'; +import { ALLOWED_FORMAT_PARTS_KEYS } from './constants'; +import { stringToDate } from './utils'; + +/** + * Parses a date input and returns a detailed object containing various date components + * and their formatted representations. + * + * @param {DateInput} dateInput - The date input, can be a string or a Date object. + * @param options - Config object. + * @returns {ParsedDateTime} An object containing the parsed date and its components. + */ +const parseDateTime = ( + dateInput: DateInput, + options: { + locale?: Locale, + intlOptions?: Intl.DateTimeFormatOptions, + } = {}, +): ParsedDateTime => { + // Parse the input date, converting strings to Date objects if necessary + const date = + typeof dateInput === 'string' + ? new Date(stringToDate(dateInput)) + : new Date(dateInput); + + // Use the provided locale or fallback to the system's default locale + options.locale = options.locale || state.getState().locale || getLocale(); + + try { + // Create an Intl.DateTimeFormat instance for formatting + const dateTimeFormat = new Intl.DateTimeFormat(options.locale, options.intlOptions); + const formattedParts = dateTimeFormat.formatToParts(date); + const formattedObj: FormattedPartsObject = {}; + + // Iterate over each part of the formatted date + formattedParts.forEach((part) => { + // If the part is allowed, add it to the formatted object + // @ts-expect-error only allowed keys are added to the formattedObj. For eg, key 'literal', 'unknown' is skipped + if (ALLOWED_FORMAT_PARTS_KEYS.includes(part.type)) { + // @ts-expect-error only allowed keys are added to the formattedObj. For eg, key 'literal', 'unknown' is skipped + formattedObj[part.type] = (formattedObj[part.type] || '') + part.value; + } + }); + + // Return the detailed parsed date object + return { + ...formattedObj, + rawParts: formattedParts, + formattedDate: formattedParts.map((p) => p.value).join(''), + dateObj: date, + }; + } catch (err) { + // Handle any errors that occur during parsing + if (err instanceof Error) { + throw err; + } else { + throw new Error(`An unknown error occurred: ${err}`); + } + } +}; + +export default withErrorBoundary(parseDateTime); diff --git a/packages/i18nify-js/src/modules/dateTime/subtract.ts b/packages/i18nify-js/src/modules/dateTime/subtract.ts new file mode 100644 index 00000000..645405db --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/subtract.ts @@ -0,0 +1,27 @@ +import { withErrorBoundary } from '../../common/errorBoundary'; +import add from './add'; +import { DateInput } from './types'; +import { stringToDate } from './utils'; + +/** + * Subtracts a specified amount of time from a date. + * + * @param date The original date. + * @param options config object. + * @returns A new Date object with the time subtracted. + */ +const subtract = ( + date: DateInput, + options:{ + value: number, + unit: 'days' | 'months' | 'years', + } +): Date => { + const {value, unit} = options; + date = + typeof date === 'string' ? new Date(stringToDate(date)) : new Date(date); + + return add(date, {value: -value, unit: unit}); // Reuse the add function with negative value +}; + +export default withErrorBoundary(subtract); diff --git a/packages/i18nify-js/src/modules/dateTime/types.ts b/packages/i18nify-js/src/modules/dateTime/types.ts new file mode 100644 index 00000000..217bf4a5 --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/types.ts @@ -0,0 +1,33 @@ +import { ALLOWED_FORMAT_PARTS_KEYS } from './constants'; + +export type DateInput = Date | string; +export type Locale = string; + +export interface DateTimeFormatOptions extends Intl.DateTimeFormatOptions {} + +export interface DateFormatOptions + extends Omit {} + +export interface TimeFormatOptions + extends Omit {} + +export type FormattedPartsObject = { + [key in (typeof ALLOWED_FORMAT_PARTS_KEYS)[number]]?: string | undefined; +}; + +export interface ParsedDateTime extends FormattedPartsObject { + rawParts: Array<{ type: string; value: unknown }>; + formattedDate: string; + dateObj: Date | null; +} + +export interface SupportedDateFormats { + regex: RegExp; + yearIndex: number; + monthIndex: number; + dayIndex: number; + hourIndex?: number; + minuteIndex?: number; + secondIndex?: number; + format: string; +} diff --git a/packages/i18nify-js/src/modules/dateTime/utils.ts b/packages/i18nify-js/src/modules/dateTime/utils.ts new file mode 100644 index 00000000..89128e3c --- /dev/null +++ b/packages/i18nify-js/src/modules/dateTime/utils.ts @@ -0,0 +1,49 @@ +import { supportedDateFormats } from './data/supportedDateFormats'; + +/** + * Converts a string representation of a date into a Date object. + * The function supports various date and timestamp formats, + * including both American and European styles, with or without time components. + * If the provided string doesn't match any of the supported formats, + * the function throws an error. + * + * @param {string} dateString - The date string to be converted to a Date object. + * @returns {Date} A Date object representing the date and time specified in the dateString. + * @throws {Error} If the date format is not recognized. + */ +export const stringToDate = (dateString: string): Date => { + // Iterate through each supported date format. + for (const format of supportedDateFormats) { + const match = dateString.match(format.regex); + if (match) { + // Extract year, month, and day from the matched groups. + const year = match[format.yearIndex]; + const month = match[format.monthIndex]; + const day = match[format.dayIndex]; + + // Extract time components if available, defaulting to '00' if not present. + const hour = format.hourIndex ? match[format.hourIndex] : '00'; + const minute = format.minuteIndex ? match[format.minuteIndex] : '00'; + const second = format.secondIndex ? match[format.secondIndex] : '00'; + + // Construct and return the Date object. + try { + const dateObj = new Date( + `${year}-${month}-${day}T${hour}:${minute}:${second}`, + ); + + if (dateObj.getTime()) return dateObj; + else throw new Error('Invalid Date!'); + } catch (err) { + if (err instanceof Error) { + throw new Error(err.message); + } else { + throw new Error(`An unknown error occurred = ${err}`); + } + } + } + } + + // If no format matches, throw an error. + throw new Error('Date format not recognized'); +};