Skip to content

Commit 9c1a06f

Browse files
committed
refactor(table): move local-ISO date formatters to utils/dates
dateToISODate/Time/DateTime were generic Date-to-string helpers stranded in filters/serialize.ts after the folder split. Move them to utils/dates alongside the existing date formatters, rename to make the local-time semantics explicit (dateToLocalISO*), and add docstrings clarifying when to use them versus Date.toISOString().
1 parent f0f3981 commit 9c1a06f

5 files changed

Lines changed: 67 additions & 40 deletions

File tree

frontend/src/components/data-table/__tests__/date-filter-inputs.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* Copyright 2026 Marimo. All rights reserved. */
22
import { describe, expect, it } from "vitest";
3+
import { dateToLocalISODate } from "@/utils/dates";
34
import { parsePastedRange } from "../date-filter-inputs";
4-
import { dateToISODate } from "../filters";
55

66
describe("parsePastedRange", () => {
77
it.each([
@@ -15,15 +15,15 @@ describe("parsePastedRange", () => {
1515
])("splits a date range pasted with %s separator", (_, text) => {
1616
const result = parsePastedRange("date", text);
1717
expect(result).toBeDefined();
18-
expect(result && dateToISODate(result.min)).toBe("2026-02-01");
19-
expect(result && dateToISODate(result.max)).toBe("2026-04-01");
18+
expect(result && dateToLocalISODate(result.min)).toBe("2026-02-01");
19+
expect(result && dateToLocalISODate(result.max)).toBe("2026-04-01");
2020
});
2121

2222
it("returns a degenerate range for a single pasted date", () => {
2323
const result = parsePastedRange("date", "2026-03-15");
2424
expect(result).toBeDefined();
25-
expect(result && dateToISODate(result.min)).toBe("2026-03-15");
26-
expect(result && dateToISODate(result.max)).toBe("2026-03-15");
25+
expect(result && dateToLocalISODate(result.min)).toBe("2026-03-15");
26+
expect(result && dateToLocalISODate(result.max)).toBe("2026-03-15");
2727
});
2828

2929
it("returns undefined for unparsable input", () => {

frontend/src/components/data-table/date-filter-inputs.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ import type { DateValue, TimeValue } from "react-aria-components";
1111
import { TimeField } from "@/components/ui/date-input";
1212
import { DatePicker, DateRangePicker } from "@/components/ui/date-picker";
1313
import {
14-
dateToISODate,
15-
dateToISODateTime,
16-
dateToISOTime,
17-
type FilterType,
18-
} from "./filters";
14+
dateToLocalISODate,
15+
dateToLocalISODateTime,
16+
dateToLocalISOTime,
17+
} from "@/utils/dates";
18+
import type { FilterType } from "./filters";
1919

2020
export type DateLikeFilterType = Extract<
2121
FilterType,
@@ -28,11 +28,11 @@ function dateToAria(
2828
): DateValue | TimeValue {
2929
switch (filterType) {
3030
case "date":
31-
return parseDate(dateToISODate(d));
31+
return parseDate(dateToLocalISODate(d));
3232
case "datetime":
33-
return parseDateTime(dateToISODateTime(d));
33+
return parseDateTime(dateToLocalISODateTime(d));
3434
case "time":
35-
return parseTime(dateToISOTime(d));
35+
return parseTime(dateToLocalISOTime(d));
3636
}
3737
}
3838

frontend/src/components/data-table/filters/format.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
/* Copyright 2026 Marimo. All rights reserved. */
22

33
import { logNever } from "@/utils/assertNever";
4-
import { exactDateTime } from "@/utils/dates";
4+
import {
5+
dateToLocalISODate,
6+
dateToLocalISOTime,
7+
exactDateTime,
8+
} from "@/utils/dates";
59
import { OPERATOR_LABELS } from "../operator-labels";
610
import { stringifyUnknownValue } from "../utils";
711
import type { ColumnFilterValue } from "./builders";
8-
import { dateToISODate, dateToISOTime } from "./serialize";
912
import type { FormattedFilter } from "./types";
1013

1114
interface FormatContext {
@@ -91,9 +94,9 @@ export function formatValue(
9194
) {
9295
const format =
9396
value.type === "date"
94-
? dateToISODate
97+
? dateToLocalISODate
9598
: value.type === "time"
96-
? dateToISOTime
99+
? dateToLocalISOTime
97100
: (d: Date) => exactDateTime(d, ctx.timezone, ctx.locale);
98101
switch (value.operator) {
99102
case "between":

frontend/src/components/data-table/filters/serialize.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,14 @@ import type {
77
} from "@/plugins/impl/data-frames/schema";
88
import type { ColumnId } from "@/plugins/impl/data-frames/types";
99
import { assertNever } from "@/utils/assertNever";
10+
import {
11+
dateToLocalISODate,
12+
dateToLocalISODateTime,
13+
dateToLocalISOTime,
14+
} from "@/utils/dates";
1015
import type { ColumnFilterValue } from "./builders";
1116
import { isNullishFilter } from "./guards";
1217

13-
function pad2(n: number): string {
14-
return n.toString().padStart(2, "0");
15-
}
16-
17-
function pad4(n: number): string {
18-
return n.toString().padStart(4, "0");
19-
}
20-
21-
export function dateToISODate(d: Date): string {
22-
return `${pad4(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
23-
}
24-
25-
export function dateToISOTime(d: Date): string {
26-
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
27-
}
28-
29-
export function dateToISODateTime(d: Date): string {
30-
return `${dateToISODate(d)}T${dateToISOTime(d)}`;
31-
}
32-
3318
export function filterToFilterCondition(
3419
columnIdString: string,
3520
filter: ColumnFilterValue | undefined,
@@ -137,10 +122,10 @@ export function filterToFilterCondition(
137122
case "time": {
138123
const encode =
139124
filter.type === "date"
140-
? dateToISODate
125+
? dateToLocalISODate
141126
: filter.type === "time"
142-
? dateToISOTime
143-
: dateToISODateTime;
127+
? dateToLocalISOTime
128+
: dateToLocalISODateTime;
144129
switch (filter.operator) {
145130
case "between":
146131
return [

frontend/src/utils/dates.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,45 @@ export function timeAgo(
134134
return value.toString();
135135
}
136136

137+
function pad2(n: number): string {
138+
return n.toString().padStart(2, "0");
139+
}
140+
141+
function pad4(n: number): string {
142+
return n.toString().padStart(4, "0");
143+
}
144+
145+
/**
146+
* Format a Date as `YYYY-MM-DD` using the date's local-time fields.
147+
*
148+
* The output reflects what the user sees in their own timezone (the calendar
149+
* day on their clock), not the UTC day. Use this when round-tripping values
150+
* that originated from local-time inputs — date pickers, "filter on this
151+
* day", calendar UI — so the displayed and serialized days agree.
152+
*
153+
* Not suitable for cross-timezone storage; use `Date.toISOString()` for that.
154+
*/
155+
export function dateToLocalISODate(d: Date): string {
156+
return `${pad4(d.getFullYear())}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}`;
157+
}
158+
159+
/**
160+
* Format a Date as `HH:MM:SS` using the date's local-time fields.
161+
*
162+
* See `dateToLocalISODate` for the rationale on local vs UTC.
163+
*/
164+
export function dateToLocalISOTime(d: Date): string {
165+
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
166+
}
167+
168+
/**
169+
* Format a Date as `YYYY-MM-DDTHH:MM:SS` (no timezone suffix) using local
170+
* fields. See `dateToLocalISODate` for the rationale on local vs UTC.
171+
*/
172+
export function dateToLocalISODateTime(d: Date): string {
173+
return `${dateToLocalISODate(d)}T${dateToLocalISOTime(d)}`;
174+
}
175+
137176
export const supportedDateFormats = ["yyyy", "yyyy-MM", "yyyy-MM-dd"] as const;
138177
export type DateFormat = (typeof supportedDateFormats)[number];
139178

0 commit comments

Comments
 (0)