-
-
Notifications
You must be signed in to change notification settings - Fork 751
fix: use UTC getters in formatDate/formatDateTime to prevent timezone drift #3782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
c2c9d64
00a1a92
1ce1618
3bce5ac
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,27 +5,39 @@ export const defineTransformer = (transformer: ContentTransformer) => { | |
| } | ||
|
|
||
| export const formatDateTime = (datetime: string): string => { | ||
| const d = new Date(datetime) | ||
| if (typeof datetime === 'string' && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(datetime)) { | ||
| return datetime | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't let the canonical fast path skip validity checks. The new passthrough branches return any Also applies to: 27-29 🤖 Prompt for AI Agents |
||
| const normalized = typeof datetime === 'string' | ||
| ? datetime.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)?$/, '$1T$2$3Z') | ||
| : datetime | ||
| const d = new Date(normalized) | ||
| if (Number.isNaN(d.getTime())) { | ||
| throw new TypeError(`Invalid datetime value: "${datetime}"`) | ||
| } | ||
|
|
||
| const hours = d.getHours() | ||
| const minutes = d.getMinutes() | ||
| const seconds = d.getSeconds() | ||
| const hours = d.getUTCHours() | ||
| const minutes = d.getUTCMinutes() | ||
| const seconds = d.getUTCSeconds() | ||
|
|
||
| return `${formatDate(datetime)} ${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` | ||
| } | ||
|
|
||
| export const formatDate = (date: string): string => { | ||
| const d = new Date(date) | ||
| if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(date)) { | ||
| return date | ||
| } | ||
| const normalized = typeof date === 'string' | ||
| ? date.replace(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})(\.\d+)?$/, '$1T$2$3Z') | ||
| : date | ||
| const d = new Date(normalized) | ||
| if (Number.isNaN(d.getTime())) { | ||
| throw new TypeError(`Invalid date value: "${date}"`) | ||
| } | ||
|
|
||
| const year = d.getFullYear() | ||
| const month = d.getMonth() + 1 | ||
| const day = d.getDate() | ||
| const year = d.getUTCFullYear() | ||
| const month = d.getUTCMonth() + 1 | ||
| const day = d.getUTCDate() | ||
|
|
||
| return `${year.toString().padStart(4, '0')}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}` | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,27 +2,31 @@ import { describe, expect, it } from 'vitest' | |
| import { formatDate, formatDateTime } from '../../src/runtime/internal/preview/utils' | ||
|
|
||
| describe('formatDate', () => { | ||
| it('formats a date string as YYYY-MM-DD', () => { | ||
| // formatDate uses local time (getFullYear/getMonth/getDate), so we | ||
| // construct expected values the same way to stay timezone-agnostic. | ||
| const input = '2022-06-15T12:00:00.000Z' | ||
| const d = new Date(input) | ||
| const expected = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}` | ||
| expect(formatDate(input)).toBe(expected) | ||
| it('formats an ISO date string as YYYY-MM-DD using UTC', () => { | ||
| expect(formatDate('2022-06-15T12:00:00.000Z')).toBe('2022-06-15') | ||
| }) | ||
|
|
||
| it('returns canonical date strings unchanged', () => { | ||
| expect(formatDate('2022-06-15')).toBe('2022-06-15') | ||
| expect(formatDate('2024-01-01')).toBe('2024-01-01') | ||
| }) | ||
|
|
||
| it('pads single-digit month and day', () => { | ||
| const input = '2022-01-05T12:00:00.000Z' | ||
| const result = formatDate(input) | ||
| // Format is always YYYY-MM-DD with zero-padded segments | ||
| expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/) | ||
| expect(result).toContain('-05') | ||
| expect(formatDate('2022-01-05T00:00:00.000Z')).toBe('2022-01-05') | ||
| }) | ||
|
|
||
| it('handles end-of-year dates', () => { | ||
| const input = '2022-12-31T12:00:00.000Z' | ||
| const result = formatDate(input) | ||
| expect(result).toMatch(/^\d{4}-12-31$/) | ||
| it('handles end-of-year dates consistently in UTC', () => { | ||
| // 2022-12-31T23:00:00Z is still Dec 31 in UTC even if it's Jan 1 locally | ||
| expect(formatDate('2022-12-31T23:00:00.000Z')).toBe('2022-12-31') | ||
| }) | ||
|
|
||
| it('handles dates near midnight boundary in UTC', () => { | ||
| // This is Jan 1 00:30 UTC — should be 2023-01-01, not 2022-12-31 | ||
| expect(formatDate('2023-01-01T00:30:00.000Z')).toBe('2023-01-01') | ||
| }) | ||
|
|
||
| it('parses space-separated datetime as UTC', () => { | ||
| expect(formatDate('2022-06-15 14:30:00')).toBe('2022-06-15') | ||
| }) | ||
|
|
||
| it('throws on invalid date', () => { | ||
|
|
@@ -31,7 +35,6 @@ describe('formatDate', () => { | |
| }) | ||
|
|
||
| it('produces same output as the build-time copy', async () => { | ||
| // Guard against the two copies drifting apart. | ||
| const buildTime = await import('../../src/utils/content/transformers/utils') | ||
| const inputs = ['2022-06-15T12:00:00.000Z', '2023-01-01T00:00:00.000Z', '2024-12-31T23:59:59.000Z'] | ||
| for (const input of inputs) { | ||
|
|
@@ -41,17 +44,29 @@ describe('formatDate', () => { | |
| }) | ||
|
|
||
| describe('formatDateTime', () => { | ||
| it('formats a datetime string as YYYY-MM-DD HH:mm:ss', () => { | ||
| const input = '2022-06-15T14:30:45.000Z' | ||
| const result = formatDateTime(input) | ||
| expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) | ||
| // The date portion must match formatDate | ||
| expect(result.split(' ')[0]).toBe(formatDate(input)) | ||
| it('formats an ISO datetime string as YYYY-MM-DD HH:mm:ss using UTC', () => { | ||
| expect(formatDateTime('2022-06-15T14:30:45.000Z')).toBe('2022-06-15 14:30:45') | ||
| }) | ||
|
|
||
| it('returns canonical datetime strings unchanged', () => { | ||
| expect(formatDateTime('2022-06-15 14:30:45')).toBe('2022-06-15 14:30:45') | ||
| }) | ||
|
Comment on lines
50
to
57
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add a Date-object test for
Suggested test addition describe('formatDateTime', () => {
it('formats an ISO datetime string as YYYY-MM-DD HH:mm:ss using UTC', () => {
expect(formatDateTime('2022-06-15T14:30:45.000Z')).toBe('2022-06-15 14:30:45')
})
+ it('handles Date object input', () => {
+ expect(formatDateTime(new Date('2022-06-15T14:30:45.000Z'))).toBe('2022-06-15 14:30:45')
+ })
+
it('returns canonical datetime strings unchanged', () => {
expect(formatDateTime('2022-06-15 14:30:45')).toBe('2022-06-15 14:30:45')
})🤖 Prompt for AI Agents |
||
|
|
||
| it('pads single-digit hours, minutes, and seconds', () => { | ||
| const result = formatDateTime('2022-01-01T01:02:03.000Z') | ||
| expect(result).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/) | ||
| expect(formatDateTime('2022-01-01T01:02:03.000Z')).toBe('2022-01-01 01:02:03') | ||
| }) | ||
|
|
||
| it('uses UTC time components regardless of system timezone', () => { | ||
| // Midnight UTC should always produce 00:00:00 | ||
| expect(formatDateTime('2022-06-15T00:00:00.000Z')).toBe('2022-06-15 00:00:00') | ||
| // 23:59:59 UTC should always produce that time, not shift to next day | ||
| expect(formatDateTime('2022-12-31T23:59:59.000Z')).toBe('2022-12-31 23:59:59') | ||
| }) | ||
|
|
||
| it('the date portion matches formatDate output', () => { | ||
| const input = '2022-06-15T14:30:45.000Z' | ||
| const result = formatDateTime(input) | ||
| expect(result.split(' ')[0]).toBe(formatDate(input)) | ||
| }) | ||
|
|
||
| it('throws on invalid datetime', () => { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validate canonical-looking inputs before the passthrough return.
These fast paths now accept malformed-but-shaped values like
2024-02-31and2024-02-31 12:00:00unchanged, so they never reach theTypeErrorpath and can serialize invalid data into the SQL columns. Please keep the short-circuit, but only after verifying the matched components represent a real UTC date/datetime.Also applies to: 113-115
🤖 Prompt for AI Agents