mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-21 03:03:15 +08:00
fix(core): clamp journal date suggestions to the length of the month (#15606)
## Description
I was reading the journal date suggestions and noticed that
`suggestJournalDate` bounds the parsed day with a hardcoded 31 before
building the date string:
```ts
let day = numbers ? parseInt(numbers) : dayjs().date();
const invalidDay = day < 1 || day > 31;
...
dayjs(`${year}-${month}-${day}`)
```
31 is the length of the longest month, so the guard lets through every
day that is valid in December but not in the month the user actually
typed. dayjs does not reject the overflow — it rolls it into the
following month. On `canary` that gives:
- `feb 30` → `2026-03-02`
- `apr 31` → `2026-05-01`
- on the 31st of a month, a plain `feb` (the case that is meant to keep
today's day) → `2026-03-03`
So a query naming February can land in March, and the suggestion quietly
points at a month the user did not ask for. The existing `dec 33` test
covers the intent — an out-of-range day falls back to today's day — but
December has 31 days, so it never reaches this path.
This bounds the day by the length of the matched month instead, and
clamps the fallback the same way, so asking for `feb` on the 31st of a
month stays inside February (`2026-02-28`) rather than jumping to
`2026-03-03`. Nothing outside `suggestJournalDate` changes.
The two tests added to `suggest-date.spec.ts` fail on the current code:
```
× a day past the end of the month falls back inside that month
expected { dateString: '2026-03-02' } to deeply equal { dateString: '2026-02-16' }
× today's day is clamped to the end of a shorter month
expected { dateString: '2026-03-03' } to deeply equal { dateString: '2026-02-28' }
Tests 2 failed | 16 passed (18)
```
and pass with the fix, along with the rest of the journal module (`23
passed`). `oxlint --deny-warnings` and `oxfmt --check` are clean on both
files. I left the `yarn lint` / `yarn typecheck` box unchecked: `yarn
typecheck` reports 684 errors in my environment, all in
`packages/backend/server` (the Prisma client isn't generated here) and
`packages/frontend/templates`, none in `packages/frontend/core` or in
the two files touched.
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
- **Bug Fixes**
- Improved journal date suggestions for months with fewer than 31 days.
- Invalid dates, such as February 30 or 31, now resolve to valid dates
within the selected month.
- Suggested dates are capped at the final day of the selected month when
today’s day number exceeds that month’s length.
- February suggestions now correctly account for both leap years and
non-leap years.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
---------
Co-authored-by: DarkSky <darksky2048@gmail.com>
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import { JOURNAL_DATE_FORMAT } from '@affine/core/modules/journal';
|
||||
import { I18n } from '@affine/i18n';
|
||||
import dayjs from 'dayjs';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
vi.mock('emoji-mart', () => {
|
||||
return {
|
||||
@@ -16,6 +16,10 @@ vi.mock('emoji-mart', () => {
|
||||
import { suggestJournalDate } from '../suggest-journal-date';
|
||||
|
||||
describe('suggestJournalDate', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('today', () => {
|
||||
expect(suggestJournalDate('t')).toEqual({
|
||||
dateString: dayjs().format(JOURNAL_DATE_FORMAT),
|
||||
@@ -147,26 +151,43 @@ describe('suggestJournalDate', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('dec', () => {
|
||||
const year = dayjs().year();
|
||||
const date = dayjs().date();
|
||||
expect(suggestJournalDate(`dec`)).toEqual({
|
||||
dateString: dayjs(`${year}-12-${date}`).format(JOURNAL_DATE_FORMAT),
|
||||
});
|
||||
});
|
||||
test.each([
|
||||
{
|
||||
now: new Date(2026, 8, 16, 12, 0, 0),
|
||||
query: 'dec',
|
||||
expected: '2026-12-16',
|
||||
},
|
||||
{
|
||||
now: new Date(2026, 8, 16, 12, 0, 0),
|
||||
query: 'dec 10',
|
||||
expected: '2026-12-10',
|
||||
},
|
||||
{
|
||||
now: new Date(2026, 8, 16, 12, 0, 0),
|
||||
query: 'feb 30',
|
||||
expected: '2026-02-16',
|
||||
},
|
||||
{
|
||||
now: new Date(2026, 0, 31, 12, 0, 0),
|
||||
query: 'feb',
|
||||
expected: '2026-02-28',
|
||||
},
|
||||
{
|
||||
now: new Date(2028, 0, 31, 12, 0, 0),
|
||||
query: 'feb',
|
||||
expected: '2028-02-29',
|
||||
},
|
||||
{
|
||||
now: new Date(2028, 8, 16, 12, 0, 0),
|
||||
query: 'feb 29',
|
||||
expected: '2028-02-29',
|
||||
},
|
||||
])('$query resolves to $expected', ({ now, query, expected }) => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(now);
|
||||
|
||||
test('dec 1', () => {
|
||||
const year = dayjs().year();
|
||||
expect(suggestJournalDate(`dec 10`)).toEqual({
|
||||
dateString: dayjs(`${year}-12-10`).format(JOURNAL_DATE_FORMAT),
|
||||
});
|
||||
});
|
||||
|
||||
test('dec 33', () => {
|
||||
const year = dayjs().year();
|
||||
const date = dayjs().date();
|
||||
expect(suggestJournalDate(`dec 33`)).toEqual({
|
||||
dateString: dayjs(`${year}-12-${date}`).format(JOURNAL_DATE_FORMAT),
|
||||
expect(suggestJournalDate(query)).toEqual({
|
||||
dateString: expected,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -99,21 +99,19 @@ export function suggestJournalDate(query: string): {
|
||||
|
||||
if (matched) {
|
||||
const [_, letters, numbers] = matched;
|
||||
const now = dayjs();
|
||||
|
||||
for (const month of monthNames) {
|
||||
for (const [monthIndex, month] of monthNames.entries()) {
|
||||
const monthMatched = fuzzyMatch(month, letters, true);
|
||||
if (monthMatched) {
|
||||
let day = numbers ? parseInt(numbers) : dayjs().date();
|
||||
const invalidDay = day < 1 || day > 31;
|
||||
if (invalidDay) {
|
||||
// fallback to today's day
|
||||
day = dayjs().date();
|
||||
}
|
||||
const year = dayjs().year();
|
||||
const targetMonth = now.month(monthIndex);
|
||||
const daysInMonth = targetMonth.daysInMonth();
|
||||
const fallbackDay = Math.min(now.date(), daysInMonth);
|
||||
const parsedDay = numbers ? parseInt(numbers) : fallbackDay;
|
||||
const day =
|
||||
parsedDay < 1 || parsedDay > daysInMonth ? fallbackDay : parsedDay;
|
||||
return {
|
||||
dateString: dayjs(`${year}-${month}-${day}`).format(
|
||||
JOURNAL_DATE_FORMAT
|
||||
),
|
||||
dateString: targetMonth.date(day).format(JOURNAL_DATE_FORMAT),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user