| | import dayjs from 'dayjs'; |
| | import weekday from 'dayjs/plugin/weekday.js'; |
| | import MockDate from 'mockdate'; |
| | import { afterEach, beforeEach, describe, expect, it } from 'vitest'; |
| |
|
| | import { parseRelativeDate } from './parse-date'; |
| |
|
| | dayjs.extend(weekday); |
| |
|
| | describe('parseRelativeDate', () => { |
| | |
| | const second = 1000; |
| | const minute = 60 * second; |
| | const hour = 60 * minute; |
| | const day = 24 * hour; |
| | const week = 7 * day; |
| |
|
| | |
| | const BASE_STR = '2026-02-02T12:00:00'; |
| | const NOW_TIMESTAMP = new Date(BASE_STR).getTime(); |
| |
|
| | |
| | const TODAY_START = new Date('2026-02-02T00:00:00').getTime(); |
| | const YESTERDAY_START = TODAY_START - day; |
| | const TOMORROW_START = TODAY_START + day; |
| |
|
| | |
| | const PREVIOUS_MONDAY = TODAY_START - week; |
| | const PREVIOUS_WEDNESDAY = TODAY_START + 2 * day - week; |
| | const LAST_SUNDAY = new Date('2026-02-01T00:00:00').getTime(); |
| | const LAST_FRIDAY = new Date('2026-01-30T00:00:00').getTime(); |
| |
|
| | const p = (str: string, ...opts: any[]) => parseRelativeDate(str, ...opts).getTime(); |
| |
|
| | beforeEach(() => { |
| | MockDate.set(NOW_TIMESTAMP); |
| | }); |
| |
|
| | afterEach(() => { |
| | MockDate.reset(); |
| | }); |
| |
|
| | |
| | |
| | |
| | describe('Immediate & Fuzzy Semantics', () => { |
| | it('handles "Just now" / "εε"', () => { |
| | const expected = NOW_TIMESTAMP - 3 * second; |
| | expect(p('Just now')).toBe(expected); |
| | expect(p('just now')).toBe(expected); |
| | expect(p('εε')).toBe(expected); |
| | }); |
| |
|
| | it('handles vague quantifiers (x / ε )', () => { |
| | |
| | expect(p('ε ειε')).toBe(NOW_TIMESTAMP - 3 * minute); |
| | expect(p('εΉΎειε')).toBe(NOW_TIMESTAMP - 3 * minute); |
| | expect(p('ζ°η§ε')).toBe(NOW_TIMESTAMP - 3 * second); |
| |
|
| | |
| | expect(p('x days ago')).toBe(NOW_TIMESTAMP - 3 * day); |
| | expect(p('X seconds ago')).toBe(NOW_TIMESTAMP - 3 * second); |
| |
|
| | |
| | expect(p('a minute ago')).toBe(NOW_TIMESTAMP - 1 * minute); |
| | }); |
| | }); |
| |
|
| | |
| | |
| | |
| | describe('Relative Duration', () => { |
| | it('handles past (ago / ε)', () => { |
| | expect(p('10m ago')).toBe(NOW_TIMESTAMP - 10 * minute); |
| | expect(p('2 hours ago')).toBe(NOW_TIMESTAMP - 2 * hour); |
| | expect(p('10η§ε')).toBe(NOW_TIMESTAMP - 10 * second); |
| | }); |
| |
|
| | it('handles future (in / later / ε)', () => { |
| | expect(p('in 10m')).toBe(NOW_TIMESTAMP + 10 * minute); |
| | expect(p('2 hours later')).toBe(NOW_TIMESTAMP + 2 * hour); |
| | expect(p('10ειε')).toBe(NOW_TIMESTAMP + 10 * minute); |
| | expect(p('10 ειεΎ')).toBe(NOW_TIMESTAMP + 10 * minute); |
| | }); |
| |
|
| | it('handles mixed units', () => { |
| | expect(p('1d 1h ago')).toBe(NOW_TIMESTAMP - (day + hour)); |
| | }); |
| | }); |
| |
|
| | |
| | |
| | |
| | |
| | describe('Semantic Keywords & Weekdays', () => { |
| | it('handles Today/Yesterday/Tomorrow', () => { |
| | expect(p('Today')).toBe(TODAY_START); |
| | expect(p('Yesterday')).toBe(YESTERDAY_START); |
| | expect(p('Tomorrow')).toBe(TOMORROW_START); |
| | }); |
| |
|
| | it('handles "Monday" (Strict Past: Today is Mon -> Last Mon)', () => { |
| | |
| | expect(p('Monday')).toBe(PREVIOUS_MONDAY); |
| | expect(p('ε¨δΈ')).toBe(PREVIOUS_MONDAY); |
| | expect(p('ζζδΈ')).toBe(PREVIOUS_MONDAY); |
| | }); |
| |
|
| | it('handles "Monday 3pm" (Strict Past)', () => { |
| | expect(p('Monday 3pm')).toBe(PREVIOUS_MONDAY + 15 * hour); |
| | expect(p('ε¨δΈ 15:00')).toBe(PREVIOUS_MONDAY + 15 * hour); |
| | }); |
| |
|
| | it('handles "Wednesday" (Strict Past: Wed is Future -> Last Wed)', () => { |
| | |
| | |
| | expect(p('Wednesday')).toBe(PREVIOUS_WEDNESDAY); |
| | expect(p('ε¨δΈ')).toBe(PREVIOUS_WEDNESDAY); |
| | }); |
| |
|
| | it('handles "Sunday" (Past: Sun is Yesterday -> Feb 01)', () => { |
| | |
| | expect(p('Sunday')).toBe(LAST_SUNDAY); |
| | }); |
| |
|
| | it('handles "Friday" (Past: Fri is Jan 30)', () => { |
| | expect(p('Friday')).toBe(LAST_FRIDAY); |
| | }); |
| | }); |
| |
|
| | |
| | |
| | |
| | describe('Contextual & Formatted Time', () => { |
| | it('handles sticky AM/PM', () => { |
| | expect(p('Today 3pm')).toBe(TODAY_START + 15 * hour); |
| | expect(p('Yesterday 10pm')).toBe(YESTERDAY_START + 22 * hour); |
| | }); |
| |
|
| | it('handles spaced AM/PM', () => { |
| | expect(p('Today 3 pm')).toBe(TODAY_START + 15 * hour); |
| | }); |
| |
|
| | it('handles Chinese modifiers (Morning/Evening)', () => { |
| | |
| | expect(p('ζζ©8ηΉ')).toBe(TOMORROW_START + 8 * hour); |
| | |
| | expect(p('ζ¨ζ8ηΉ')).toBe(YESTERDAY_START + 20 * hour); |
| | |
| | expect(p('ε¨δΊδΈε3ηΉ')).toBe(LAST_FRIDAY + 15 * hour); |
| | |
| | expect(p('ζ¨ε€© 23:01')).toBe(YESTERDAY_START + 23 * hour + 1 * minute); |
| | }); |
| |
|
| | it('handles 12am / 12pm edge cases', () => { |
| | expect(p('Today 12pm')).toBe(TODAY_START + 12 * hour); |
| | expect(p('Today 12am')).toBe(TODAY_START); |
| | }); |
| | }); |
| |
|
| | |
| | |
| | |
| | describe('Fallback', () => { |
| | it('passes formatting options to dayjs', () => { |
| | const str = '05/02/2026'; |
| | const format = 'DD/MM/YYYY'; |
| | const expected = new Date('2026-02-05T00:00:00').getTime(); |
| | expect(p(str, format)).toBe(expected); |
| | }); |
| |
|
| | it('returns raw absolute dates', () => { |
| | const raw = '2026-01-01T00:00:00'; |
| | expect(p(raw)).toBe(new Date(raw).getTime()); |
| | }); |
| | }); |
| | }); |
| |
|