Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions src/islands/dev/Timestamp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Alert } from '@/components/ui/Alert';
import {
describeDate,
parseTimestamp,
detectNumericUnit,
formatInTimeZone,
listTimeZones,
getLocalTimeZone,
Expand All @@ -19,6 +20,7 @@ export default function Timestamp() {
const [date, setDate] = useState<Date | null>(null);
const [timeZone, setTimeZone] = useState(() => getLocalTimeZone());
const [error, setError] = useState('');
const [detectedUnit, setDetectedUnit] = useState('');
const [pickerValue, setPickerValue] = useState('');
const [pickerZone, setPickerZone] = useState<'local' | 'utc'>('local');

Expand All @@ -27,6 +29,7 @@ export default function Timestamp() {
setPickerValue(value);
setPickerZone(zone);
setError('');
setDetectedUnit('');
if (!value) return;
const parsed = parseDateTimeLocal(value, zone);
if (parsed) setDate(parsed);
Expand All @@ -35,6 +38,7 @@ export default function Timestamp() {
const convert = () => {
setError('');
setDate(null);
setDetectedUnit('');
const trimmed = input.trim();
if (!trimmed) return;
const parsed = parseTimestamp(trimmed);
Expand All @@ -43,10 +47,13 @@ export default function Timestamp() {
return;
}
setDate(parsed);
// Surface how an all-digits value was interpreted (seconds/ms/µs/ns).
if (/^\d+$/.test(trimmed)) setDetectedUnit(detectNumericUnit(trimmed));
};

const now = () => {
setError('');
setDetectedUnit('');
setDate(new Date());
};

Expand All @@ -69,7 +76,7 @@ export default function Timestamp() {
label="Unix timestamp or date string"
value={input}
onChange={e => setInput(e.target.value)}
placeholder="1720000000 · 2026-07-12 · Jul 12 2026 10:00"
placeholder="1720000000 (s · ms · µs · ns) · 2026-07-12 · Jul 12 2026 10:00"
rows={2}
/>

Expand Down Expand Up @@ -113,13 +120,20 @@ export default function Timestamp() {
</div>
</div>

<Button variant="ghost" onClick={() => { setInput(''); setDate(null); setError(''); setPickerValue(''); }}>
<Button variant="ghost" onClick={() => { setInput(''); setDate(null); setError(''); setDetectedUnit(''); setPickerValue(''); }}>
Clear
</Button>
</div>

{error && <Alert variant="error">{error}</Alert>}

{detectedUnit && (
<p className="text-sm text-muted-foreground">
Detected numeric input as{' '}
<span className="font-bold text-foreground">Unix {detectedUnit}</span>.
</p>
)}

{date && (
<>
<label className="flex flex-wrap items-center gap-2 text-sm">
Expand Down
28 changes: 28 additions & 0 deletions src/tools/dev/timestamp.lib.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
import {
describeDate,
parseTimestamp,
detectNumericUnit,
formatInTimeZone,
listTimeZones,
getLocalTimeZone,
Expand All @@ -21,6 +22,24 @@ describe('parseTimestamp', () => {
expect(date!.getTime()).toBe(1700000000000);
});

it('parses a 16-digit numeric string as Unix microseconds', () => {
const date = parseTimestamp('1700000000000000');
expect(date).not.toBeNull();
expect(date!.getTime()).toBe(1700000000000);
});

it('parses a 19-digit numeric string as Unix nanoseconds', () => {
const date = parseTimestamp('1700000000000000000');
expect(date).not.toBeNull();
expect(date!.getTime()).toBe(1700000000000);
});

it('parses the reported 19-digit nanosecond value instead of erroring', () => {
const date = parseTimestamp('1784694237438743460');
expect(date).not.toBeNull();
expect(date!.getUTCFullYear()).toBe(2026);
});

it('parses an ISO date string', () => {
const date = parseTimestamp('2026-07-12');
expect(date).not.toBeNull();
Expand All @@ -32,6 +51,15 @@ describe('parseTimestamp', () => {
});
});

describe('detectNumericUnit', () => {
it('maps digit length to the epoch unit', () => {
expect(detectNumericUnit('1700000000')).toBe('seconds'); // 10
expect(detectNumericUnit('1700000000000')).toBe('milliseconds'); // 13
expect(detectNumericUnit('1700000000000000')).toBe('microseconds'); // 16
expect(detectNumericUnit('1784694237438743460')).toBe('nanoseconds'); // 19
});
});

describe('describeDate', () => {
it('describes the Unix epoch', () => {
const result = describeDate(new Date(0));
Expand Down
33 changes: 30 additions & 3 deletions src/tools/dev/timestamp.lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,40 @@ export function listTimeZones(): string[] {
return ['UTC', ...zones.filter(zone => zone !== 'UTC')];
}

export type NumericUnit = 'seconds' | 'milliseconds' | 'microseconds' | 'nanoseconds';

/** Convert a numeric epoch value in the given unit to milliseconds. */
const TO_MILLIS: Record<NumericUnit, (n: number) => number> = {
seconds: n => n * 1000,
milliseconds: n => n,
microseconds: n => n / 1e3,
nanoseconds: n => n / 1e6,
};

/**
* Infer the epoch unit of an all-digits timestamp from its length. Tuned so
* present-day values land on the right unit: ~10 digits = seconds,
* ~13 = milliseconds, ~16 = microseconds, ~19 = nanoseconds. The seconds/millis
* cutoffs (≤10, ≤13) match the tool's original behavior.
*/
export function detectNumericUnit(digits: string): NumericUnit {
const len = digits.length;
if (len <= 10) return 'seconds';
if (len <= 13) return 'milliseconds';
if (len <= 16) return 'microseconds';
return 'nanoseconds';
}

export function parseTimestamp(input: string): Date | null {
const trimmed = input.trim();
let date: Date;
if (/^\d+$/.test(trimmed)) {
// Numeric: treat 10-digit as seconds, 13-digit as milliseconds.
const num = Number(trimmed);
date = new Date(trimmed.length <= 10 ? num * 1000 : num);
// Numeric epoch value: infer the unit (s/ms/µs/ns) from its digit length
// and convert to milliseconds for the Date constructor. This lets us accept
// high-resolution timestamps (e.g. 19-digit nanoseconds) that would blow
// past Date's range if naively treated as milliseconds.
const millis = TO_MILLIS[detectNumericUnit(trimmed)](Number(trimmed));
date = new Date(millis);
} else {
date = new Date(trimmed);
}
Expand Down
Loading