compote-ui
Version:
An opinionated UI component library for Svelte, built on top of [Ark UI](https://ark-ui.com) with additional components and features not available in the core Ark UI library.
112 lines (111 loc) • 4.22 kB
JavaScript
import { CalendarDate, CalendarDateTime, ZonedDateTime, parseDate, parseDateTime, parseAbsolute, parseAbsoluteToLocal, fromAbsolute, fromDate, getLocalTimeZone, toTimeZone, toZoned } from '@internationalized/date';
const DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
// Trailing `Z`, or a `+HH`, `+HH:MM`, `+HHMM` offset.
const HAS_OFFSET = /(?:[zZ]|[+-]\d{2}(?::?\d{2})?)$/;
/** Normalize a DB datetime string to ISO 8601: space → `T`, bare `+HH` offset → `+HH:00`. */
function normalizeIso(value) {
return value.replace(' ', 'T').replace(/([+-]\d{2})$/, '$1:00');
}
/** Report the runtime shape of a value so a change can be emitted in the same shape. */
export function dateValueShape(value) {
if (value == null)
return null;
if (typeof value === 'string')
return 'string';
if (value instanceof Date)
return 'date';
return 'date-value';
}
/**
* Coerce a string / `Date` / `DateValue` into a `DateValue` for Ark UI.
*
* String auto-detection:
* - `"2024-01-15"` → {@link parseDate} (date-only)
* - `"2024-01-15T10:30:00"` → {@link parseDateTime} (local datetime)
* - `"2024-01-15T10:30:00Z"` / `"…+02:00"` → zoned (in `timeZone`, else local)
*/
export function toDateValue(value, timeZone) {
if (value == null)
return null;
if (value instanceof Date)
return fromDate(value, timeZone ?? getLocalTimeZone());
if (typeof value !== 'string')
return value;
const raw = value.trim();
if (!raw)
return null;
if (DATE_ONLY.test(raw))
return parseDate(raw);
const iso = normalizeIso(raw);
if (HAS_OFFSET.test(iso)) {
return timeZone ? parseAbsolute(iso, timeZone) : parseAbsoluteToLocal(iso);
}
return parseDateTime(iso);
}
/** Serialize a `DateValue` to an ISO string (absolute `…Z` for zoned values). */
export function dateValueToString(value) {
return 'timeZone' in value ? value.toAbsoluteString() : value.toString();
}
/** Convert a `DateValue` to a native `Date`. */
export function dateValueToDate(value, timeZone) {
return 'timeZone' in value
? value.toDate()
: value.toDate(timeZone ?? getLocalTimeZone());
}
/** Emit a `DateValue` back in the original `shape` it was bound as. */
export function fromDateValue(value, shape, timeZone) {
if (value == null)
return null;
if (shape === 'string')
return dateValueToString(value);
if (shape === 'date')
return dateValueToDate(value, timeZone);
return value;
}
/**
* Read any date-ish value — a `DateValue`, ISO/DB string, native `Date` or epoch-millisecond
* number — the way it should be shown in `timeZone`. Never throws: anything unparseable
* (including an out-of-range date like `2024-02-30`) is `null`.
*
* - `"2024-03-15"` and `CalendarDate` stay calendar dates. They are never turned into an
* instant, which would put them on the previous day in zones west of UTC.
* - `Date`, epoch numbers, offset strings (`Z`, `+02:00`) and `ZonedDateTime` are instants,
* converted into `timeZone`.
* - A datetime without an offset (`"2024-03-15T10:30"`, `CalendarDateTime`) is a wall-clock
* time in `timeZone`.
*/
export function readDateInZone(value, timeZone) {
try {
return readDate(value, timeZone);
}
catch {
return null;
}
}
function readDate(value, timeZone) {
if (value == null)
return null;
if (value instanceof Date) {
return Number.isNaN(value.getTime()) ? null : fromDate(value, timeZone);
}
if (typeof value === 'number') {
return Number.isFinite(value) ? fromAbsolute(value, timeZone) : null;
}
if (value instanceof ZonedDateTime)
return toTimeZone(value, timeZone);
if (value instanceof CalendarDateTime)
return toZoned(value, timeZone);
if (value instanceof CalendarDate)
return value;
if (typeof value !== 'string')
return null;
const raw = value.trim();
if (!raw)
return null;
if (DATE_ONLY.test(raw))
return parseDate(raw);
const iso = normalizeIso(raw);
return HAS_OFFSET.test(iso)
? parseAbsolute(iso, timeZone)
: toZoned(parseDateTime(iso), timeZone);
}