File size: 6,718 Bytes
bf48b89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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', () => {
    // === CONSTANTS ===
    const second = 1000;
    const minute = 60 * second;
    const hour = 60 * minute;
    const day = 24 * hour;
    const week = 7 * day;

    // === BASE DATE SETUP: 2026-02-02 (Monday) ===
    const BASE_STR = '2026-02-02T12:00:00';
    const NOW_TIMESTAMP = new Date(BASE_STR).getTime();

    // Derived Timestamps
    const TODAY_START = new Date('2026-02-02T00:00:00').getTime();
    const YESTERDAY_START = TODAY_START - day;
    const TOMORROW_START = TODAY_START + day;

    // Strict Past Logic Expectations (Reference is Mon Feb 02)
    const PREVIOUS_MONDAY = TODAY_START - week; // Jan 26 (Last week)
    const PREVIOUS_WEDNESDAY = TODAY_START + 2 * day - week; // Jan 28 (Last week)
    const LAST_SUNDAY = new Date('2026-02-01T00:00:00').getTime(); // Yesterday (Feb 01)
    const LAST_FRIDAY = new Date('2026-01-30T00:00:00').getTime(); // Last Friday (Jan 30)

    const p = (str: string, ...opts: any[]) => parseRelativeDate(str, ...opts).getTime();

    beforeEach(() => {
        MockDate.set(NOW_TIMESTAMP);
    });

    afterEach(() => {
        MockDate.reset();
    });

    /**
     * Category 1: Immediate & Fuzzy Semantics
     */
    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 / ε‡ )', () => {
            // "ε‡ " maps to 3
            expect(p('ε‡ εˆ†ι’Ÿε‰')).toBe(NOW_TIMESTAMP - 3 * minute);
            expect(p('εΉΎεˆ†ι˜ε‰')).toBe(NOW_TIMESTAMP - 3 * minute);
            expect(p('数秒前')).toBe(NOW_TIMESTAMP - 3 * second);

            // "x" maps to 3
            expect(p('x days ago')).toBe(NOW_TIMESTAMP - 3 * day);
            expect(p('X seconds ago')).toBe(NOW_TIMESTAMP - 3 * second);

            // "a/an" maps to 1
            expect(p('a minute ago')).toBe(NOW_TIMESTAMP - 1 * minute);
        });
    });

    /**
     * Category 2: Relative Duration (Ago / In)
     */
    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));
        });
    });

    /**
     * Category 3: Semantic Keywords & Strict Past Logic
     * Reference: Today is Monday, Feb 02.
     */
    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)', () => {
            // Strict past rule: If input weekday is Same as today, go back 1 week.
            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)', () => {
            // Strict past rule: If input weekday is Future, go back 1 week.
            // Today is Mon Feb 02. Wed is Feb 04 (Future) -> Back 1 week -> Jan 28.
            expect(p('Wednesday')).toBe(PREVIOUS_WEDNESDAY);
            expect(p('周三')).toBe(PREVIOUS_WEDNESDAY);
        });

        it('handles "Sunday" (Past: Sun is Yesterday -> Feb 01)', () => {
            // Sunday (Feb 01) is strictly before Monday (Feb 02).
            expect(p('Sunday')).toBe(LAST_SUNDAY);
        });

        it('handles "Friday" (Past: Fri is Jan 30)', () => {
            expect(p('Friday')).toBe(LAST_FRIDAY);
        });
    });

    /**
     * Category 4: Contextual & Combined Expressions
     */
    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)', () => {
            // "ζ˜Žζ—©" -> Tomorrow 8am
            expect(p('ζ˜Žζ—©8η‚Ή')).toBe(TOMORROW_START + 8 * hour);
            // "ζ˜¨ζ™š" -> Yesterday 8pm (20:00)
            expect(p('ζ˜¨ζ™š8η‚Ή')).toBe(YESTERDAY_START + 20 * hour);
            // "ε‘¨δΊ”δΈ‹εˆ3η‚Ή" -> Last Friday 15:00
            expect(p('ε‘¨δΊ”δΈ‹εˆ3η‚Ή')).toBe(LAST_FRIDAY + 15 * hour);
            // https://github.com/DIYgod/RSSHub/issues/20878
            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); // Noon
            expect(p('Today 12am')).toBe(TODAY_START); // Midnight
        });
    });

    /**
     * Category 5: Edge Cases & Fallback
     */
    describe('Fallback', () => {
        it('passes formatting options to dayjs', () => {
            const str = '05/02/2026';
            const format = 'DD/MM/YYYY'; // Feb 5th
            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());
        });
    });
});