Skip to content

Commit e54bd84

Browse files
committed
feat: support date without time (+ refactoring)
1 parent f30a71f commit e54bd84

File tree

3 files changed

+193
-79
lines changed

3 files changed

+193
-79
lines changed

ui/src/common/RelativeDateTimeSelector.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import {TextField} from '@material-ui/core';
3-
import {normalizeDate, parseRelativeTime, userFriendlyDate} from '../utils/time';
3+
import {parseRelativeTime} from '../utils/time';
44
import Typography from '@material-ui/core/Typography';
55
import useTimeout from '@rooks/use-timeout';
66

@@ -29,18 +29,16 @@ export const RelativeDateTimeSelector: React.FC<RelativeDateTimeSelectorProps> =
2929
const [error, setError] = React.useState('');
3030
const {start, stop} = useTimeout(() => setErrVisible(true), 200);
3131
const parsed = parseRelativeTime(apiValue, type);
32-
const value = userFriendlyDate(apiValue);
3332

3433
return (
3534
<TextField
3635
fullWidth
3736
style={style}
38-
value={value}
37+
value={parsed.success ? parsed.localized : apiValue}
3938
disabled={disabled}
4039
InputProps={{disableUnderline}}
4140
onChange={(e) => {
42-
const newValue = normalizeDate(e.target.value);
43-
const result = parseRelativeTime(newValue, type);
41+
const result = parseRelativeTime(e.target.value, type);
4442
setErrVisible(false);
4543
stop();
4644
if (!result.success) {
@@ -49,18 +47,16 @@ export const RelativeDateTimeSelector: React.FC<RelativeDateTimeSelectorProps> =
4947
} else {
5048
setError('');
5149
}
52-
setValue(newValue, result.success);
50+
setValue(result.success ? result.normalized : e.target.value, result.success);
5351
}}
5452
error={error !== ''}
5553
helperText={
56-
small ? (
57-
undefined
58-
) : errVisible ? (
54+
small ? undefined : errVisible ? (
5955
<Typography color={'secondary'} variant={'caption'}>
6056
{error}
6157
</Typography>
6258
) : (
63-
<Typography variant={'caption'}>{!parsed.success ? '...' : parsed.value.format('llll')}</Typography>
59+
<Typography variant={'caption'}>{!parsed.success ? '...' : parsed.preview.format('llll')}</Typography>
6460
)
6561
}
6662
label={label}

ui/src/utils/time.test.ts

Lines changed: 157 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {isValidDate, normalizeDate, parseRelativeTime, userFriendlyDate} from './time';
1+
import {isValidDate, parseRelativeTime} from './time';
22
import moment from 'moment';
33

44
moment.updateLocale('en', {
@@ -24,43 +24,160 @@ it('should test for valid date', () => {
2424
// 2019-10-14 Monday
2525
// 2019-10-21 Monday
2626

27-
it('should parse', () => {
28-
expectSuccess(parseRelativeTime('now-1d', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-19 15:55:00');
29-
expectSuccess(parseRelativeTime('now-120s', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-20 15:53:15');
30-
expectSuccess(parseRelativeTime('now-1d-1h', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-19 14:55:00');
31-
expectSuccess(parseRelativeTime('now/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-14 00:00:00');
32-
expectSuccess(parseRelativeTime('now/w', 'endOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-20 23:59:59');
33-
expectSuccess(parseRelativeTime('now-1w/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-10-07 00:00:00');
34-
expectSuccess(parseRelativeTime('now-1y+1w/w', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2018-10-22 00:00:00');
35-
expectSuccess(parseRelativeTime('now/d+5h', 'startOf', moment('2019-10-20T15:55:00'))).toEqual('2019-10-20 05:00:00');
36-
expectSuccess(parseRelativeTime('now/y', 'startOf', moment('2019-10-20T15:55:15'))).toEqual('2019-01-01 00:00:00');
37-
});
38-
39-
it('should convert to RFC3339 and back', () => {
40-
// can't put exact dates since moment doesn't allow overriding `.local()`'s timezone for unit tests
41-
const userDate = '2025-01-01 10:10';
42-
const rfcDate = moment(userDate).utc().format();
43-
44-
expect(normalizeDate(userDate)).toBe(rfcDate);
45-
expect(userFriendlyDate(rfcDate)).toBe(userDate);
46-
});
47-
48-
it('should not modify relative ranges', () => {
49-
expect(normalizeDate('now-1d')).toBe('now-1d');
50-
expect(normalizeDate('now-120s')).toBe('now-120s');
51-
expect(normalizeDate('now-1d-1h')).toBe('now-1d-1h');
52-
expect(normalizeDate('now/w')).toBe('now/w');
53-
expect(normalizeDate('now/w')).toBe('now/w');
54-
expect(normalizeDate('now-1w/w')).toBe('now-1w/w');
55-
expect(normalizeDate('now-1y+1w/w')).toBe('now-1y+1w/w');
56-
expect(normalizeDate('now/d+5h')).toBe('now/d+5h');
57-
expect(normalizeDate('now/y')).toBe('now/y');
58-
});
59-
60-
const expectSuccess = (value: ReturnType<typeof parseRelativeTime>) => {
61-
if (value.success) {
62-
return expect(value.value.format('YYYY-MM-DD HH:mm:ss'));
27+
it.each([
28+
{
29+
value: 'now-1d',
30+
divide: 'startOf',
31+
now: moment('2019-10-20T15:55:00'),
32+
expected: '2019-10-19 15:55:00',
33+
localized: 'now-1d',
34+
normalized: 'now-1d',
35+
shouldParse: true,
36+
},
37+
{
38+
value: 'now-120s',
39+
divide: 'startOf',
40+
now: moment('2019-10-20T15:55:15'),
41+
expected: '2019-10-20 15:53:15',
42+
localized: 'now-120s',
43+
normalized: 'now-120s',
44+
shouldParse: true,
45+
},
46+
{
47+
value: 'now-1d-1h',
48+
divide: 'startOf',
49+
now: moment('2019-10-20T15:55:00'),
50+
expected: '2019-10-19 14:55:00',
51+
localized: 'now-1d-1h',
52+
normalized: 'now-1d-1h',
53+
shouldParse: true,
54+
},
55+
{
56+
value: 'now/w',
57+
divide: 'startOf',
58+
now: moment('2019-10-20T15:55:15'),
59+
expected: '2019-10-14 00:00:00',
60+
localized: 'now/w',
61+
normalized: 'now/w',
62+
shouldParse: true,
63+
},
64+
{
65+
value: 'now/w',
66+
divide: 'endOf',
67+
now: moment('2019-10-20T15:55:15'),
68+
expected: '2019-10-20 23:59:59',
69+
localized: 'now/w',
70+
normalized: 'now/w',
71+
shouldParse: true,
72+
},
73+
{
74+
value: 'now-1w/w',
75+
divide: 'startOf',
76+
now: moment('2019-10-20T15:55:15'),
77+
expected: '2019-10-07 00:00:00',
78+
localized: 'now-1w/w',
79+
normalized: 'now-1w/w',
80+
shouldParse: true,
81+
},
82+
{
83+
value: 'now-1y+1w/w',
84+
divide: 'startOf',
85+
now: moment('2019-10-20T15:55:15'),
86+
expected: '2018-10-22 00:00:00',
87+
localized: 'now-1y+1w/w',
88+
normalized: 'now-1y+1w/w',
89+
shouldParse: true,
90+
},
91+
{
92+
value: 'now/d+5h',
93+
divide: 'startOf',
94+
now: moment('2019-10-20T15:55:00'),
95+
expected: '2019-10-20 05:00:00',
96+
localized: 'now/d+5h',
97+
normalized: 'now/d+5h',
98+
shouldParse: true,
99+
},
100+
{
101+
value: 'now/y',
102+
divide: 'startOf',
103+
now: moment('2019-10-20T15:55:15'),
104+
expected: '2019-01-01 00:00:00',
105+
localized: 'now/y',
106+
normalized: 'now/y',
107+
shouldParse: true,
108+
},
109+
{
110+
value: '2025-01-01 10:10',
111+
divide: 'startOf',
112+
expected: '2025-01-01 10:10:00',
113+
localized: '2025-01-01 10:10',
114+
normalized: moment('2025-01-01 10:10').utc().format(),
115+
shouldParse: true,
116+
},
117+
{
118+
value: '2025-01-01 10:10',
119+
divide: 'endOf',
120+
expected: '2025-01-01 10:10:00',
121+
localized: '2025-01-01 10:10',
122+
normalized: moment('2025-01-01 10:10').utc().format(),
123+
shouldParse: true,
124+
},
125+
{
126+
value: '2025-01-02',
127+
divide: 'startOf',
128+
expected: '2025-01-02 00:00:00',
129+
localized: '2025-01-02',
130+
normalized: moment('2025-01-02 00:00').utc().format(),
131+
shouldParse: true,
132+
},
133+
{
134+
value: '2025-01-02',
135+
divide: 'endOf',
136+
expected: '2025-01-02 23:59:59',
137+
localized: '2025-01-02',
138+
normalized: moment('2025-01-02 23:59:59').utc().format(),
139+
shouldParse: true,
140+
},
141+
{
142+
value: moment('2025-01-02 10:00:00').format(),
143+
divide: 'startOf',
144+
expected: '2025-01-02 10:00:00',
145+
localized: '2025-01-02 10:00',
146+
normalized: moment('2025-01-02 10:00:00').utc().format(),
147+
shouldParse: true,
148+
},
149+
{
150+
value: moment('2025-01-02 00:00:00').format(),
151+
divide: 'startOf',
152+
expected: '2025-01-02 00:00:00',
153+
localized: '2025-01-02',
154+
normalized: moment('2025-01-02 00:00:00').utc().format(),
155+
shouldParse: true,
156+
},
157+
{
158+
value: moment('2025-01-02 23:59:59').utc().format(),
159+
divide: 'endOf',
160+
expected: '2025-01-02 23:59:59',
161+
localized: '2025-01-02',
162+
normalized: moment('2025-01-02 23:59:59').utc().format(),
163+
shouldParse: true,
164+
},
165+
{
166+
value: 'invalid',
167+
divide: 'endOf',
168+
expected: "Expected valid date (e.g. 2020-01-01 16:30) or 'now' at index 0",
169+
localized: 'invalid',
170+
normalized: undefined,
171+
shouldParse: false,
172+
},
173+
])('should parse', ({value, divide, now, expected, normalized, localized, shouldParse}) => {
174+
const result = parseRelativeTime(value, divide as 'startOf' | 'endOf', now);
175+
expect(result.success).toBe(shouldParse);
176+
if (result.success) {
177+
expect(result.preview.format('YYYY-MM-DD HH:mm:ss')).toEqual(expected);
178+
expect(result.normalized).toEqual(normalized);
179+
expect(result.localized).toEqual(localized);
180+
}else{
181+
expect(result.error).toEqual(expected);
63182
}
64-
expect(value.error).toEqual('no error');
65-
return expect('');
66-
};
183+
});

ui/src/utils/time.ts

Lines changed: 30 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import moment from 'moment-timezone';
22

33
interface Success {
44
success: true;
5-
value: moment.Moment;
5+
preview: moment.Moment;
6+
localized: string;
7+
normalized: string;
68
}
79

810
interface Failure {
@@ -32,9 +34,28 @@ enum Unit {
3234
Second = 's',
3335
}
3436

37+
// tslint:disable-next-line:cyclomatic-complexity mccabe-complexity
3538
export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', nowDate = moment()): Success | Failure => {
36-
if (isValidDate(value)) {
37-
return success(asDate(value));
39+
for (const format of ['YYYY-MM-DD HH:mm', 'YYYY-MM-DD', 'YYYY-MM-DD[T]HH:mm:ssZ']) {
40+
if (isValidDate(value, format)) {
41+
const parsed = asDate(value, format);
42+
if (divide === 'endOf' && format === 'YYYY-MM-DD') {
43+
parsed.endOf('day');
44+
}
45+
46+
if (format === 'YYYY-MM-DD[T]HH:mm:ssZ') {
47+
const localDate = parsed.clone().local();
48+
if (
49+
(divide === 'startOf' && parsed.isSame(localDate.startOf('day'), 'second')) ||
50+
(divide === 'endOf' && parsed.isSame(localDate.endOf('day'), 'second'))
51+
) {
52+
value = asDate(value).format('YYYY-MM-DD');
53+
} else {
54+
value = asDate(value).format('YYYY-MM-DD HH:mm');
55+
}
56+
}
57+
return success(parsed, value, parsed.clone().utc().format());
58+
}
3859
}
3960

4061
if (value.substr(0, 3) === 'now') {
@@ -71,7 +92,7 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no
7192
case Type.Unit:
7293
if (!isUnit(currentChar)) {
7394
return failure(
74-
'Expected unit (' + Object.values(Unit) + ') at index ' + currentIndex + ' but was ' + currentChar
95+
'Expected unit (' + Object.values(Unit) + ') at index ' + currentIndex + ' but was ' + currentChar,
7596
);
7697
}
7798

@@ -105,18 +126,18 @@ export const parseRelativeTime = (value: string, divide: 'endOf' | 'startOf', no
105126
if (expectNext === Type.Value) {
106127
return failure('Expected number at the end but got nothing');
107128
}
108-
return success(time);
129+
return success(time, value, value);
109130
}
110131

111132
if (value.indexOf('now') !== -1) {
112-
return failure("'now' must be at the start");
133+
return failure('\'now\' must be at the start');
113134
}
114135

115-
return failure("Expected valid date (e.g. 2020-01-01 16:30) or 'now' at index 0");
136+
return failure('Expected valid date (e.g. 2020-01-01 16:30) or \'now\' at index 0');
116137
};
117138

118-
export const success = (value: moment.Moment): Success => {
119-
return {success: true, value};
139+
export const success = (value: moment.Moment, localized: string, normalized: string): Success => {
140+
return {success: true, preview: value, normalized, localized};
120141
};
121142
export const failure = (error: string): Failure => {
122143
return {success: false, error};
@@ -140,23 +161,3 @@ export const isSameDate = (from: moment.Moment, to?: moment.Moment): boolean =>
140161
const fromString = from.format('YYYYMMDD');
141162
return to === undefined || fromString === to.format('YYYYMMDD');
142163
};
143-
144-
export function normalizeDate(date: string): string {
145-
if (isValidDate(date, 'YYYY-MM-DD HH:mm')) {
146-
return moment(date)
147-
.utc()
148-
.format();
149-
} else {
150-
return date;
151-
}
152-
}
153-
154-
export function userFriendlyDate(date: string): string {
155-
if (isValidDate(date)) {
156-
return moment(date)
157-
.local()
158-
.format('YYYY-MM-DD HH:mm');
159-
} else {
160-
return date;
161-
}
162-
}

0 commit comments

Comments
 (0)