From aeabb8dde4903114b19010cfee25b0a61a4fc227 Mon Sep 17 00:00:00 2001 From: Ajesh Sen Thapa <35629644+aj3sh@users.noreply.github.com> Date: Sat, 30 Nov 2024 21:16:59 +0545 Subject: [PATCH] feat: add method `parseEnglishDate` for English date parsing (#95) --- README.md | 57 ++++++++--- src/NepaliDate.ts | 21 +++- src/parse/index.ts | 2 + src/{ => parse}/parse.ts | 8 +- src/parse/parseEnglishDate.ts | 140 +++++++++++++++++++++++++++ tests/NepaliDate.test.ts | 22 +++++ tests/{ => parse}/parse.test.ts | 2 +- tests/parse/parseEnglishDate.test.ts | 131 +++++++++++++++++++++++++ 8 files changed, 361 insertions(+), 22 deletions(-) create mode 100644 src/parse/index.ts rename src/{ => parse}/parse.ts (97%) create mode 100644 src/parse/parseEnglishDate.ts rename tests/{ => parse}/parse.test.ts (98%) create mode 100644 tests/parse/parseEnglishDate.test.ts diff --git a/README.md b/README.md index 5d3cf42..86117b8 100644 --- a/README.md +++ b/README.md @@ -17,23 +17,23 @@ import NepaliDate from 'nepali-datetime' // Create a NepaliDate object for the current date and time const now = new NepaliDate() -console.log(now.toString()) // Outputs: "2080-03-23 15:32:03.643" +console.log(now.toString()) // 2080-03-23 15:32:03.643 // Create a NepaliDate object from a Nepali date string const date1 = new NepaliDate('2079-02-15 23:11') -console.log(date1.toString()) // Outputs: "2079-02-15 23:11:00" +console.log(date1.toString()) // 2079-02-15 23:11:00 // Parse Nepali date string const date2 = new NepaliDate('Baisakh 18, 2080', 'MMMM D, YYYY') -console.log(date1.toString()) // Outputs: "2080-01-18 00:00:00" +console.log(date2.toString()) // 2080-01-18 00:00:00 // Format a NepaliDate object const formattedDate = now.format('YYYY-MM-DD') -console.log(formattedDate) // Outputs: "2080-03-23" +console.log(formattedDate) // 2080-03-23 -// Create a NepaliDate object from an English date -const date3 = NepaliDate.fromEnglishDate(2023, 6, 8) -console.log(englishDate.toString()) // Outputs: "2080-03-23 00:00:00" +// Create a NepaliDate object from an English date string +const date3 = NepaliDate.parseEnglishDate('2023-07-08', 'YYYY-MM-DD') +console.log(date3.toString()) // 2080-03-23 00:00:00 ``` ## Installation @@ -143,9 +143,9 @@ Additionally, you can convert the corresponding English date to a string using t - `formatEnglishDateInNepali(formatStr)`: Returns a string representation in the Nepali (Devanagari script) of the English Date in the specified format. ```javascript -const now = new NepaliDate(2079, 5, 3, 16, 14) -console.log(now.format('YYYY-MM-DD hh:mm A')) // Outputs: 2079-06-03 04:14 PM -console.log(now.formatEnglishDate('YYYY-MM-DD hh:mm A')) // Outputs: 2022-08-19 04:14 PM +const date = new NepaliDate(2079, 5, 3, 16, 14) +console.log(date.format('YYYY-MM-DD hh:mm A')) // 2079-06-03 04:14 PM +console.log(date.formatEnglishDate('YYYY-MM-DD hh:mm A')) // 2022-09-19 04:14 PM ``` The date formatting will follow the format codes mentioned below, which are similar to the date formats used in day.js. @@ -205,19 +205,24 @@ console.log(now.getDateObject()) // Date 2022-09-18T18:15:00.000Z #### Creating a NepaliDate object from an English date -You can create a `NepaliDate` object from an English calendar date using the `fromEnglishDate` method. +You can create a `NepaliDate` object from an English calendar date using the `parseEnglishDate` or `fromEnglishDate` method. ```javascript -const date = NepaliDate.fromEnglishDate(2023, 6, 8) -console.log(date.toString()) // Outputs: "2080-03-23 00:00:00" +const date1 = NepaliDate.parseEnglishDate('2023-07-08', 'YYYY-MM-DD') +console.log(date1.toString()) // 2080-03-23 00:00:00 + +const date2 = NepaliDate.fromEnglishDate(2023, 6, 8, 10, 15) +console.log(date2.toString()) // 2080-03-23 10:15:00 ``` ### dateConverter -The `dateConverter` module provides functions for converting dates between the Nepali and English calendars. +The `dateConverter` module provides core functions for converting dates between the Nepali and English calendars. + +- `englishToNepali(year, month, day)`: Converts an English calendar date to a Nepali calendar date. Returns an array `[npYear, npMonth, npDay]` representing the Nepali date. +- `nepaliToEnglish(year, month, day)`: Converts a Nepali calendar date to an English calendar date. Returns an array `[enYear, enYear, enDay]` representing the English date. -- `englishToNepali(year, month, day)`: Converts an English calendar date to a Nepali calendar date. Returns an array `[yearNp, monthNp, dayNp]` representing the Nepali date. -- `nepaliToEnglish(year, month, day)`: Converts a Nepali calendar date to an English calendar date. Returns an array `[yearEn, monthEn, dayEn]` representing the English date. +> Note: Use 0 as the value for the months Baisakh and January (Javascript Logic 🤷). ```javascript import dateConverter from 'nepali-datetime/dateConverter' @@ -229,6 +234,26 @@ const [npYear, npMonth, npDay] = dateConverter.englishToNepali(2023, 5, 27) const [enYear, enMonth, enDay] = dateConverter.nepaliToEnglish(2080, 2, 15) ``` +#### Quick Date conversion using NepaliDate + +The `NepaliDate` class can also be used for direct string-to-string date conversions, eliminating the need for custom parsing or formatting logic. + +**English Date to Nepali Date** + +```javascript +const enDate = '2024-11-25' +const npDate = NepaliDate.parseEnglishDate(enDate, 'YYYY-MM-DD').format('YYYY-MM-DD') +// 2081-08-10 +``` + +**Nepali Date to English Date** + +```javascript +const npDate = '2081-08-10' +const enDate = new NepaliDate(npDate).formatEnglishDate('YYYY-MM-DD') +// 2024-11-25 +``` + ## Acknowledgements This project was inspired by [nepali-date](https://github.com/sharingapples/nepali-date). We would like to express our gratitude to their team for their excellent work and ideas, which served as a motivation for this project. diff --git a/src/NepaliDate.ts b/src/NepaliDate.ts index c861da2..33174ea 100644 --- a/src/NepaliDate.ts +++ b/src/NepaliDate.ts @@ -7,7 +7,7 @@ import { nepaliDateToString, } from './format' -import { parse, parseFormat } from './parse' +import { parse, parseFormat, parseEnglishDateFormat } from './parse' import { getDate, getNepalDateAndTime } from './utils' import { validateTime } from './validators' @@ -623,6 +623,25 @@ class NepaliDate { const englishDate = getDate(year, month0, date, hour, minute, second, ms) return new NepaliDate(englishDate) } + + /** + * Creates a NepaliDate instance by parsing a provided English Date and Time string + * with the given format. + * + * @param dateString - The English Date and time string. + * @param format - The format of the provided date-time string. + * @example + * const dateTimeString = '2024/11/23 14-05-23.789' + * const format = 'YYYY/MM/DD HH-mm-ss.SSS' + * const nepaliDate = NepaliDate.parseEnglishDate(dateTimeString, format) + */ + static parseEnglishDate(dateString: string, format: string): NepaliDate { + const [year, month0, day, hour, minute, second, ms] = parseEnglishDateFormat( + dateString, + format + ) + return NepaliDate.fromEnglishDate(year, month0, day, hour, minute, second, ms) + } } NepaliDate.minimum = () => diff --git a/src/parse/index.ts b/src/parse/index.ts new file mode 100644 index 0000000..a6a6399 --- /dev/null +++ b/src/parse/index.ts @@ -0,0 +1,2 @@ +export { parseFormat, parse } from './parse' +export { parseEnglishDateFormat } from './parseEnglishDate' diff --git a/src/parse.ts b/src/parse/parse.ts similarity index 97% rename from src/parse.ts rename to src/parse/parse.ts index a7e411e..9d52a0b 100644 --- a/src/parse.ts +++ b/src/parse/parse.ts @@ -17,8 +17,8 @@ import { NEPALI_MONTHS_SHORT_EN, WEEKDAYS_LONG_EN, WEEKDAYS_SHORT_EN, -} from './constants' -import { parseFormatTokens, seqToRE } from './utils' +} from '../constants' +import { parseFormatTokens, seqToRE } from '../utils' /** * Parses date from the given string. @@ -200,7 +200,7 @@ function getDateParams( break case 'A': case 'a': - isPM = (match[i + 1] as string).toLowerCase() === 'pm' + isPM = match[i + 1].toLowerCase() === 'pm' } } @@ -222,7 +222,7 @@ function getDateParams( export function parseFormat(dateString: string, format: string): number[] { const formatTokens = parseFormatTokens(format) const { dateTokens, regex: formatRegex } = tokensToRegex(formatTokens) - const match = dateString.match(formatRegex) + const match = RegExp(formatRegex).exec(dateString) if (!match) { throw new Error('Invalid date format') } diff --git a/src/parse/parseEnglishDate.ts b/src/parse/parseEnglishDate.ts new file mode 100644 index 0000000..53df328 --- /dev/null +++ b/src/parse/parseEnglishDate.ts @@ -0,0 +1,140 @@ +import { + ENGLISH_MONTHS_EN, + ENGLISH_MONTHS_SHORT_EN, + WEEKDAYS_LONG_EN, + WEEKDAYS_SHORT_EN, +} from '../constants' +import { parseFormatTokens, seqToRE } from '../utils' + +const TOKEN_TO_REGEX: { [key: string]: RegExp } = { + YY: /(\d\d)/, + YYYY: /(\d\d\d\d)/, + M: /(1[0-2]|0[1-9]|[1-9])/, + MM: /(1[0-2]|0[1-9]|[1-9])/, + D: /(3[0-2]|[1-2]\d|0[1-9]|[1-9]| [1-9])/, + DD: /(3[0-2]|[1-2]\d|0[1-9]|[1-9]| [1-9])/, + H: /(2[0-3]|[0-1]\d|\d)/, + HH: /(2[0-3]|[0-1]\d|\d)/, + hh: /(1[0-2]|0[1-9]|[1-9])/, + mm: /([0-5]\d|\d)/, + ss: /([0-5]\d|\d)/, + SSS: /(\d\d\d)/, + A: /(AM|PM)/, + a: /(am|pm)/, + MMMM: seqToRE(ENGLISH_MONTHS_EN), + MMM: seqToRE(ENGLISH_MONTHS_SHORT_EN), + dddd: seqToRE(WEEKDAYS_LONG_EN), + ddd: seqToRE(WEEKDAYS_SHORT_EN), + dd: seqToRE(WEEKDAYS_SHORT_EN), + d: /([0-6])/, +} + +function tokensToRegex(arr: string[]): { dateTokens: string[]; regex: RegExp } { + const dateTokens: string[] = [] + const regexParts: string[] = [] + + for (const token of arr) { + if (token in TOKEN_TO_REGEX) { + dateTokens.push(token) + regexParts.push(TOKEN_TO_REGEX[token].source) + } else { + regexParts.push(token.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&')) + } + } + + const regexString = regexParts.join('') + + return { + dateTokens, + regex: new RegExp(`^${regexString}$`), + } +} + +function getDateParams( + dateTokens: string[], + match: RegExpMatchArray +): { [key: string]: number } { + // month and day are set to 1 in default + let [year, month, day, hour, hour12, minute, second, ms] = [0, 1, 1, 0, 0, 0, 0, 0] + let isPM = false + let is12hourFormat = false + + for (let i = 0; i < dateTokens.length; i++) { + const token = dateTokens[i] + const matchData = parseInt(match[i + 1]) + switch (token) { + case 'YYYY': + year = matchData + break + case 'YY': + year = 2000 + parseInt(match[i]) + break + case 'MM': + case 'M': + month = matchData + break + case 'MMMM': + month = ENGLISH_MONTHS_EN.indexOf(match[i + 1]) + 1 + break + case 'MMM': + month = ENGLISH_MONTHS_SHORT_EN.indexOf(match[i + 1]) + 1 + break + case 'DD': + case 'D': + day = matchData + break + case 'HH': + case 'H': + hour = matchData + break + case 'hh': + case 'h': + hour12 = matchData + is12hourFormat = true + break + case 'mm': + case 'm': + minute = matchData + break + case 'ss': + case 's': + second = matchData + break + case 'SSS': + ms = matchData + break + case 'A': + case 'a': + isPM = match[i + 1].toLowerCase() === 'pm' + } + } + + if (is12hourFormat) { + hour = hour12 + (isPM ? 12 : 0) + } + + return { + year, + month0: month - 1, + day, + hour, + minute, + second, + ms, + } +} + +export function parseEnglishDateFormat(dateString: string, format: string): number[] { + const formatTokens = parseFormatTokens(format) + const { dateTokens, regex: formatRegex } = tokensToRegex(formatTokens) + const match = RegExp(formatRegex).exec(dateString) + if (!match) { + throw new Error('Invalid date format') + } + + const { year, month0, day, hour, minute, second, ms } = getDateParams( + dateTokens, + match + ) + return [year, month0, day, hour, minute, second, ms] +} diff --git a/tests/NepaliDate.test.ts b/tests/NepaliDate.test.ts index 444a89e..ed802c5 100644 --- a/tests/NepaliDate.test.ts +++ b/tests/NepaliDate.test.ts @@ -66,6 +66,28 @@ describe('NepaliDate', () => { }).toThrow('Date out of range') }) + it('should initialize by parsing English Date string with given format', () => { + const n1 = NepaliDate.parseEnglishDate( + '2024 August 13 14-05-23.789', + 'YYYY MMMM DD HH-mm-ss.SSS' + ) + expect(n1.toString()).toBe('2081-04-29 14:05:23.789') + }) + + it("should initialize by parsing English Date's year string with default month, day and time params", () => { + const n1 = NepaliDate.parseEnglishDate('2024', 'YYYY') + expect(n1.toString()).toBe('2080-09-16 00:00:00') + }) + + it("should throw error if English Date's year component is missed during parsing", () => { + expect(() => { + const _ = NepaliDate.parseEnglishDate( + '08/13 14-05-23.789', + 'MM/DD HH-mm-ss.SSS' + ) + }).toThrow('Date out of range') + }) + it('checks for nepali date validity', () => { // 373314600000 // Fri Oct 30 1981 18:30:00 GMT+0000 diff --git a/tests/parse.test.ts b/tests/parse/parse.test.ts similarity index 98% rename from tests/parse.test.ts rename to tests/parse/parse.test.ts index ff0f503..d27b9fe 100644 --- a/tests/parse.test.ts +++ b/tests/parse/parse.test.ts @@ -1,4 +1,4 @@ -import { parseFormat } from '../src/parse' +import { parseFormat } from '../../src/parse' describe('parseFormat', () => { it('should parse date string in valid format correctly', () => { diff --git a/tests/parse/parseEnglishDate.test.ts b/tests/parse/parseEnglishDate.test.ts new file mode 100644 index 0000000..61de4a9 --- /dev/null +++ b/tests/parse/parseEnglishDate.test.ts @@ -0,0 +1,131 @@ +import { parseEnglishDateFormat } from '../../src/parse' + +describe('parseEnglishDateFormat', () => { + it('should parse date string in valid format correctly', () => { + expect( + parseEnglishDateFormat('2024-08-02 12:34:56', 'YYYY-MM-DD HH:mm:ss') + ).toEqual([2024, 7, 2, 12, 34, 56, 0]) + }) + + it('should parse date string with milliseconds', () => { + expect( + parseEnglishDateFormat('2024-08-02 12:34:56.789', 'YYYY-MM-DD HH:mm:ss.SSS') + ).toEqual([2024, 7, 2, 12, 34, 56, 789]) + }) + + it('should parse date string with leading zeros in format', () => { + expect( + parseEnglishDateFormat('2024-08-02 09:05:07', 'YYYY-MM-DD HH:mm:ss') + ).toEqual([2024, 7, 2, 9, 5, 7, 0]) + }) + + it('should parse date string with 2digit year and single-digit month and day', () => { + expect(parseEnglishDateFormat('24-8-2 12:34:56', 'YY-M-D HH:mm:ss')).toEqual([ + 2024, 7, 2, 12, 34, 56, 0, + ]) + }) + + it('should parse date string with 12-hour format', () => { + expect( + parseEnglishDateFormat('2024-08-02 05:34:56 PM', 'YYYY-MM-DD hh:mm:ss A') + ).toEqual([2024, 7, 2, 17, 34, 56, 0]) + }) + + it("should parse date string with 12-hour format before 12 o'clock", () => { + expect( + parseEnglishDateFormat('2024-08-02 05:34:56 AM', 'YYYY-MM-DD hh:mm:ss A') + ).toEqual([2024, 7, 2, 5, 34, 56, 0]) + }) + + it('should parse date string with static text in format', () => { + expect( + parseEnglishDateFormat( + 'Year: 2024, Month: 08, Day: 02', + '[Year]: YYYY, [Month]: MM, [Day]: DD' + ) + ).toEqual([2024, 7, 2, 0, 0, 0, 0]) + }) + + it('should throw error for invalid format, missing time components', () => { + expect(() => { + parseEnglishDateFormat('2024-08-02', 'YYYY-MM-DD HH:mm:ss') + }).toThrowError(Error) + }) + + it('should throw error for invalid format, without static text', () => { + expect(() => { + parseEnglishDateFormat('2024-08-02 12:34:56 test', 'YYYY-MM-DD HH:mm:ss') + }).toThrowError(Error) + }) + + it('should parse date string with full month text', () => { + expect(parseEnglishDateFormat('2024, August 23', 'YYYY, MMMM DD')).toEqual([ + 2024, 7, 23, 0, 0, 0, 0, + ]) + }) + + it('should parse date string with full month text with time', () => { + expect( + parseEnglishDateFormat( + '2024, August 23, 10:16:30', + 'YYYY, MMMM DD, HH:mm:ss' + ) + ).toEqual([2024, 7, 23, 10, 16, 30, 0]) + }) + + it('should throw error with full month text and missing literals', () => { + expect(() => + parseEnglishDateFormat('2024/August/23', 'YYYY/MMMM/DD HH:mm:ss') + ).toThrowError(Error) + }) + + it('should parse date string with short month text', () => { + expect(parseEnglishDateFormat('2024, Aug 23', 'YYYY, MMM DD')).toEqual([ + 2024, 7, 23, 0, 0, 0, 0, + ]) + }) + + it('should parse date string with short month text with time', () => { + expect( + parseEnglishDateFormat('2024, Aug 23, 10:16:30', 'YYYY, MMM DD, HH:mm:ss') + ).toEqual([2024, 7, 23, 10, 16, 30, 0]) + }) + + it('should throw error with short month text and missing literals', () => { + expect(() => + parseEnglishDateFormat('2024/Aug/23', 'YYYY/MMM/DD HH:mm:ss') + ).toThrowError(Error) + }) + + it('should parse weekday fullname', () => { + expect(parseEnglishDateFormat('Wednesday', 'dddd')).toEqual([ + 0, 0, 1, 0, 0, 0, 0, + ]) + }) + + it('should parse weekday short name', () => { + expect(parseEnglishDateFormat('Wed', 'ddd')).toEqual([0, 0, 1, 0, 0, 0, 0]) + }) + + it('should parse weekday short 2 letter name', () => { + expect(parseEnglishDateFormat('Wed', 'dd')).toEqual([0, 0, 1, 0, 0, 0, 0]) + }) + + it('should parse weekday number', () => { + expect(parseEnglishDateFormat('3', 'd')).toEqual([0, 0, 1, 0, 0, 0, 0]) + }) + + it('should throw error on random weekday number', () => { + expect(() => parseEnglishDateFormat('7', 'd')).toThrowError(Error) + }) + + it('should throw error on random weekday name', () => { + expect(() => parseEnglishDateFormat('we', 'dd')).toThrowError(Error) + }) + + it('should parse nothing', () => { + expect(parseEnglishDateFormat('Yellow', 'Yellow')).toEqual([ + 0, 0, 1, 0, 0, 0, 0, + ]) + }) +})