| | import type { Dayjs, ManipulateType, OptionType } from 'dayjs'; |
| | import dayjs from 'dayjs'; |
| | import customParseFormat from 'dayjs/plugin/customParseFormat.js'; |
| | import duration from 'dayjs/plugin/duration.js'; |
| | import isSameOrBefore from 'dayjs/plugin/isSameOrBefore.js'; |
| | import weekday from 'dayjs/plugin/weekday.js'; |
| |
|
| | dayjs.extend(customParseFormat); |
| | dayjs.extend(duration); |
| | dayjs.extend(isSameOrBefore); |
| | dayjs.extend(weekday); |
| |
|
| | |
| | |
| | |
| | interface KeywordDefinition { |
| | |
| | |
| | |
| | regExp: RegExp; |
| | |
| | |
| | |
| | calc: () => Dayjs; |
| | } |
| |
|
| | |
| | |
| | |
| | interface UnitPattern { |
| | |
| | |
| | |
| | unit: ManipulateType; |
| | |
| | |
| | |
| | regExp: RegExp; |
| | } |
| |
|
| | |
| |
|
| | const REGEX_JUST_NOW = /^(?:just\s?now|εε|εε)$/i; |
| | const REGEX_AGO = /(.*)(?:ago|[δΉδ»₯]?ε)$/i; |
| | const REGEX_IN = /^(?:in\s*)(.*)|(.*)(?:\s*later|\s*after|[δΉδ»₯]?[εεΎ])$/i; |
| | const REGEX_STICKY_AMPM = /(\d+)\s*(a|p)m$/i; |
| | const REGEX_IS_PM = /(?:δΈε|ζδΈ|ζ|pm|p\.m\.)/i; |
| | const REGEX_IS_AM = /(?:δΈε|εζ¨|ζ©|ζ¨|am|a\.m\.)/i; |
| |
|
| | const UNIT_PATTERNS: UnitPattern[] = [ |
| | { unit: 'years', regExp: /(\d+)\s*(?:εΉ΄|y(?:ea)?rs?)/i }, |
| | { unit: 'months', regExp: /(\d+)\s*(?:[δΈͺε]?ζ|months?)/i }, |
| | { unit: 'weeks', regExp: /(\d+)\s*(?:ε¨|[δΈͺε]?ζζ|weeks?)/i }, |
| | { unit: 'days', regExp: /(\d+)\s*(?:倩|ζ₯|d(?:ay)?s?)/i }, |
| | { unit: 'hours', regExp: /(\d+)\s*(?:[δΈͺε]?(?:ε°?ζΆ|ζ|ηΉ|ι»)|h(?:(?:ou)?r)?s?)/i }, |
| | { unit: 'minutes', regExp: /(\d+)\s*(?:ε[ιι]?|m(?:in(?:ute)?)?s?)/i }, |
| | { unit: 'seconds', regExp: /(\d+)\s*(?:η§[ιι]?|s(?:ec(?:ond)?)?s?)/i }, |
| | ]; |
| |
|
| | const CN_NUM_MAP: Record<string, string> = { |
| | δΈ: '1', |
| | δΊ: '2', |
| | δΈ€: '2', |
| | δΈ: '3', |
| | ε: '4', |
| | δΊ: '5', |
| | ε
: '6', |
| | δΈ: '7', |
| | ε
«: '8', |
| | δΉ: '9', |
| | ε: '10', |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const getLastWeekday = (targetDay: number): Dayjs => { |
| | const today = dayjs(); |
| | const currentDayIndex = today.day(); |
| |
|
| | |
| | const targetDayIndex = targetDay === 7 ? 0 : targetDay; |
| |
|
| | |
| | let daysToAdd = targetDayIndex - currentDayIndex; |
| |
|
| | |
| | |
| | if (daysToAdd >= 0) { |
| | daysToAdd -= 7; |
| | } |
| |
|
| | return today.add(daysToAdd, 'day').startOf('day'); |
| | }; |
| |
|
| | |
| | const KEYWORDS: KeywordDefinition[] = [ |
| | { |
| | regExp: /^(?:δ»[倩ζ₯ζ©ζ¨ζ]|to?day?)/i, |
| | calc: () => dayjs().startOf('day'), |
| | }, |
| | { |
| | regExp: /^(?:ζ¨[倩ζ₯ζ©ζ¨ζ]|y(?:ester)?day?)/i, |
| | calc: () => dayjs().subtract(1, 'days').startOf('day'), |
| | }, |
| | { |
| | regExp: /^(?:ε倩|(?:the)?d(?:ay)?b(?:eforeyesterda)?y)/i, |
| | calc: () => dayjs().subtract(2, 'days').startOf('day'), |
| | }, |
| | { |
| | regExp: /^(?:ζ[倩ζ₯ζ©ζ¨ζ]|t(?:omorrow)?)/i, |
| | calc: () => dayjs().add(1, 'days').startOf('day'), |
| | }, |
| | { |
| | regExp: /^(?:[εεΎ][倩ζ₯]|(?:the)?d(?:ay)?a(?:fter)?t(?:omrrow)?)/i, |
| | calc: () => dayjs().add(2, 'days').startOf('day'), |
| | }, |
| | |
| | { regExp: /^(?:mon(?:day)?|(?:ε¨|ζζ)[1δΈ])/i, calc: () => getLastWeekday(1) }, |
| | { regExp: /^(?:tue(?:sday)?|(?:ε¨|ζζ)[2δΊδΈ€])/i, calc: () => getLastWeekday(2) }, |
| | { regExp: /^(?:wed(?:nesday)?|(?:ε¨|ζζ)[3δΈ])/i, calc: () => getLastWeekday(3) }, |
| | { regExp: /^(?:thu(?:rsday)?|(?:ε¨|ζζ)[4ε])/i, calc: () => getLastWeekday(4) }, |
| | { regExp: /^(?:fri(?:day)?|(?:ε¨|ζζ)[5δΊ])/i, calc: () => getLastWeekday(5) }, |
| | { regExp: /^(?:sat(?:urday)?|(?:ε¨|ζζ)[6ε
])/i, calc: () => getLastWeekday(6) }, |
| | { regExp: /^(?:sun(?:day)?|(?:ε¨|ζζ)[7ζ₯倩])/i, calc: () => getLastWeekday(7) }, |
| | ]; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const normalize = (date: string): string => { |
| | let str = date.toLowerCase().trim(); |
| |
|
| | if (REGEX_JUST_NOW.test(str)) { |
| | return 'just now'; |
| | } |
| |
|
| | |
| | str = str.replaceAll(/(^|\s)an?(\s)/g, '$11$2'); |
| |
|
| | |
| | str = str.replaceAll(/(^|\s)x(\s|$)/g, '$13$2'); |
| |
|
| | |
| | str = str.replaceAll(/ε |εΉΎ|ζ°/g, '3'); |
| |
|
| | |
| | str = str.replaceAll(/[δΈδΊδΈ€δΈεδΊε
δΈε
«δΉε]/g, (match) => CN_NUM_MAP[match] || match); |
| |
|
| | |
| | str = str.replaceAll(',', ''); |
| |
|
| | return str; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | const parseDuration = (str: string): plugin.Duration => { |
| | let totalDuration = dayjs.duration(0); |
| | |
| | const cleanStr = str.replaceAll(/\s+/g, ''); |
| |
|
| | for (const { unit, regExp } of UNIT_PATTERNS) { |
| | const match = regExp.exec(cleanStr); |
| | if (match) { |
| | const val = Number.parseInt(match[1], 10); |
| | if (!Number.isNaN(val)) { |
| | totalDuration = totalDuration.add(val, unit); |
| | } |
| | } |
| | } |
| | return totalDuration; |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export const parseDate = (date: string | number | Date, ...options: OptionType[]): Date => dayjs(date, ...options).toDate(); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | const processSemanticKeyword = (baseTime: Dayjs, timePart: string, originalContext: string): Date => { |
| | if (!timePart) { |
| | return baseTime.toDate(); |
| | } |
| |
|
| | const isPM = REGEX_IS_PM.test(originalContext); |
| | const isAM = REGEX_IS_AM.test(originalContext); |
| |
|
| | |
| | const fixTimePart = timePart.replace(REGEX_STICKY_AMPM, '$1 $2m'); |
| |
|
| | |
| | const extraDuration = parseDuration(fixTimePart); |
| | let addedMillis = extraDuration.asMilliseconds(); |
| |
|
| | |
| | const stickyMatch = REGEX_STICKY_AMPM.exec(timePart); |
| | if (addedMillis === 0 && stickyMatch) { |
| | addedMillis = Number.parseInt(stickyMatch[1], 10) * 60 * 60 * 1000; |
| | } |
| |
|
| | if (addedMillis > 0) { |
| | const hours = dayjs.duration(addedMillis).asHours(); |
| | |
| | if (isPM && hours < 12) { |
| | addedMillis += 12 * 60 * 60 * 1000; |
| | } else if (isAM && hours === 12) { |
| | addedMillis -= 12 * 60 * 60 * 1000; |
| | } |
| | return baseTime.add(addedMillis, 'ms').toDate(); |
| | } |
| |
|
| | |
| | const composedDateStr = `${baseTime.format('YYYY-MM-DD')} ${fixTimePart}`; |
| | const tried = dayjs(composedDateStr, ['YYYY-MM-DD HH:mm:ss', 'YYYY-MM-DD HH:mm', 'YYYY-MM-DD H:m', 'YYYY-MM-DD h:m a', 'YYYY-MM-DD h a']); |
| |
|
| | if (tried.isValid()) { |
| | let result = tried; |
| | |
| | if (isPM && result.hour() < 12) { |
| | result = result.add(12, 'hours'); |
| | } |
| | return result.toDate(); |
| | } |
| |
|
| | return baseTime.toDate(); |
| | }; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | export const parseRelativeDate = (date: string, ...options: OptionType[]): Date => { |
| | if (!date) { |
| | return new Date(); |
| | } |
| |
|
| | const normalized = normalize(date); |
| |
|
| | |
| | if (normalized === 'just now') { |
| | return dayjs().subtract(3, 'seconds').toDate(); |
| | } |
| |
|
| | |
| | const agoMatch = REGEX_AGO.exec(normalized); |
| | if (agoMatch) { |
| | const duration = parseDuration(agoMatch[1]); |
| | if (duration.asMilliseconds() > 0) { |
| | return dayjs().subtract(duration).toDate(); |
| | } |
| | } |
| |
|
| | const inMatch = REGEX_IN.exec(normalized); |
| | if (inMatch) { |
| | const duration = parseDuration(inMatch[1] || inMatch[2]); |
| | if (duration.asMilliseconds() > 0) { |
| | return dayjs().add(duration).toDate(); |
| | } |
| | } |
| |
|
| | |
| | const cleanStr = normalized.replaceAll(/\s+/g, ''); |
| | for (const word of KEYWORDS) { |
| | const match = word.regExp.exec(cleanStr); |
| | if (match) { |
| | const baseTime = word.calc(); |
| | const timePart = cleanStr.replace(word.regExp, ''); |
| | return processSemanticKeyword(baseTime, timePart, normalized); |
| | } |
| | } |
| |
|
| | |
| | return parseDate(date, ...options); |
| | }; |
| |
|