Skip to content

Commit 5b7d646

Browse files
authored
fix: date/timepicker issue with dates in the future (#1391)
Fixes: HDX-2852 If a user used the time picker and chose today, but with a time in the future, existing code would "infer" that and assume it was for an earlier year. Since the UI time picker doesnt allow users to select a _day_ in the future, we should be smart to parse down the time to match now()
1 parent c8ec7fa commit 5b7d646

File tree

3 files changed

+79
-10
lines changed

3 files changed

+79
-10
lines changed

.changeset/tame-paws-clap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@hyperdx/app": patch
3+
---
4+
5+
fix: date/timepicker issue with dates in the future

packages/app/src/components/TimePicker/__tests__/utils.test.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import { dateParser, parseTimeRangeInput } from '../utils';
22

33
describe('dateParser', () => {
4+
let mockDate: Date;
5+
46
beforeEach(() => {
57
// Mock current date to ensure consistent test results
68
jest.useFakeTimers();
7-
jest.setSystemTime(new Date('2025-01-15T22:00'));
9+
mockDate = new Date('2025-01-15T22:00:00');
10+
jest.setSystemTime(mockDate);
811
});
912

1013
afterEach(() => {
@@ -44,6 +47,58 @@ describe('dateParser', () => {
4447
it('parses non-future month name/date with current year', () => {
4548
expect(dateParser('Jan 15')).toEqual(new Date('2025-01-15T12:00:00'));
4649
});
50+
51+
it('clamps slightly future dates to now (within 1 day) - no year specified', () => {
52+
// Input: 23:00. Now: 22:00. Should clamp to now (22:00)
53+
const result = dateParser('Jan 15 23:00:00');
54+
expect(result?.getTime()).toEqual(mockDate.getTime());
55+
expect(result?.getFullYear()).toEqual(2025);
56+
});
57+
58+
it('clamps slightly future dates to now (within 1 day) - year specified', () => {
59+
// Explicit year should be preserved even if in future, but clamped to now
60+
const result = dateParser('2025-01-15 23:00:00');
61+
expect(result?.getTime()).toEqual(mockDate.getTime());
62+
// Verify it didn't shift to 2024
63+
expect(result?.getFullYear()).toEqual(2025);
64+
});
65+
66+
it('shifts year back for dates more than 1 day in future with inferred year', () => {
67+
// mocked time is 2025-01-15 22:00
68+
// Jan 17 is more than 1 day in future, should shift to 2024
69+
const result = dateParser('Jan 17 12:00:00');
70+
expect(result).toEqual(new Date('2024-01-17T12:00:00'));
71+
});
72+
73+
it('does NOT shift year back for dates more than 1 day in future with explicit year', () => {
74+
// mocked time is 2025-01-15 22:00
75+
// Jan 17, 2025 is more than 1 day in future, but year is explicit
76+
const result = dateParser('2025-01-17 12:00:00');
77+
expect(result).toEqual(new Date('2025-01-17T12:00:00'));
78+
expect(result?.getFullYear()).toEqual(2025);
79+
});
80+
81+
it('handles dates in the past correctly', () => {
82+
// mocked time is 2025-01-15 22:00
83+
const result = dateParser('Jan 10 12:00:00');
84+
expect(result).toEqual(new Date('2025-01-10T12:00:00'));
85+
expect(result?.getFullYear()).toEqual(2025);
86+
});
87+
88+
it('handles edge case: exactly 1 day in the future', () => {
89+
// mocked time is 2025-01-15 22:00:00
90+
// Exactly 24 hours later: 2025-01-16 22:00:00
91+
// Should be clamped since it's <= 1 day from now
92+
const result = dateParser('Jan 16 22:00:00');
93+
expect(result?.getTime()).toEqual(mockDate.getTime());
94+
});
95+
96+
it('handles edge case: just over 1 day in the future', () => {
97+
// mocked time is 2025-01-15 22:00:00
98+
// 24 hours + 1 second later should shift year
99+
const result = dateParser('Jan 16 22:00:01');
100+
expect(result).toEqual(new Date('2024-01-16T22:00:01'));
101+
});
47102
});
48103

49104
describe('parseTimeRangeInput', () => {

packages/app/src/components/TimePicker/utils.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ function normalizeParsedDate(parsed?: chrono.ParsedComponents): Date | null {
66
return null;
77
}
88

9-
if (parsed.isCertain('year')) {
10-
return parsed.date();
11-
}
12-
139
const now = new Date();
10+
const parsedDate = parsed.date();
11+
12+
// If all of the time components have been inferred, set the time components of now
13+
// to match the parsed time components. This ensures that the comparison later on uses
14+
// the same point in time when only worrying about dates.
1415
if (
1516
!(
1617
parsed.isCertain('hour') ||
@@ -19,19 +20,27 @@ function normalizeParsedDate(parsed?: chrono.ParsedComponents): Date | null {
1920
parsed.isCertain('millisecond')
2021
)
2122
) {
22-
// If all of the time components have been inferred, set the time components of now
23-
// to match the parsed time components. This ensures that the comparison later on uses
24-
// the same point in time when only worrying about dates.
2523
now.setHours(parsed.get('hour') || 0);
2624
now.setMinutes(parsed.get('minute') || 0);
2725
now.setSeconds(parsed.get('second') || 0);
2826
now.setMilliseconds(parsed.get('millisecond') || 0);
2927
}
3028

31-
const parsedDate = parsed.date();
29+
// Handle future dates:
30+
// - If slightly in the future (within 1 day), clamp to now
31+
// - If significantly in the future (>1 day) AND year was inferred, shift year back
32+
// (e.g., "Dec 25" typed in June defaults to Dec 25 this year, but user likely meant last Dec 25)
3233
if (parsedDate > now) {
33-
parsedDate.setFullYear(parsedDate.getFullYear() - 1);
34+
const oneDayFromNow = now.getTime() + ms('1d');
35+
if (parsedDate.getTime() <= oneDayFromNow) {
36+
// Slightly in the future: clamp to now
37+
return now;
38+
} else if (!parsed.isCertain('year')) {
39+
// Significantly in the future with inferred year: shift year back
40+
parsedDate.setFullYear(parsedDate.getFullYear() - 1);
41+
}
3442
}
43+
3544
return parsedDate;
3645
}
3746

0 commit comments

Comments
 (0)