Skip to main content

DateTime

Avoid JavaScript's Date Class

Never use JavaScript's native Date class in Psychic applications. Always use Dream's DateTime for timestamps and CalendarDate for dates. Dream's classes provide built-in math, formatting, and robust timezone handling.

// ❌ DON'T DO THIS
const badDate = new Date()
const badParsed = new Date('2024-01-15')

// ✅ DO THIS INSTEAD
import { DateTime, CalendarDate } from '@rvoh/dream'
const goodDateTime = DateTime.now()
const goodDate = CalendarDate.today()
const formatted = goodDateTime.toFormat('yyyy-MM-dd HH:mm:ss.SSSSSS')
const tomorrow = goodDate.plus({ days: 1 })

Overview​

DateTime is Dream's primary class for handling timestamps with full date and time information. It provides:

  • Microsecond precision (6 decimal places) matching Postgres capabilities
  • Automatic timezone conversion to UTC when saving to database
  • Robust timezone manipulation and formatting
  • Math operations (add, subtract, compare)
  • Wraps Luxon DateTime internally for reliability

Postgres Mapping​

Database timestamp with time zone and timestamp without time zone columns are automatically converted to DateTime objects when Dream models are loaded from the database.

Important: DateTime always converts to UTC before storing in the database, ensuring consistent timezone representation.

Import​

import { DateTime } from '@rvoh/dream'

Creating DateTime Instances​

Current Time​

// Current time in UTC (default)
DateTime.now()

// Current time in a specific timezone
DateTime.now({ zone: 'America/New_York' })
DateTime.now({ zone: 'Europe/London' })

From Components​

// Local time (in system's local zone)
DateTime.local(2024, 1, 15) // 2024-01-15T00:00:00
DateTime.local(2024, 1, 15, 14, 30) // 2024-01-15T14:30:00
DateTime.local(2024, 1, 15, 14, 30, 45) // 2024-01-15T14:30:45
DateTime.local(2024, 1, 15, 14, 30, 45, 123) // with 123 milliseconds
DateTime.local(2024, 1, 15, 14, 30, 45, 123, 456) // with 123ms + 456µs
DateTime.local(2024, 1, 15, { zone: 'America/New_York' })

// UTC time
DateTime.utc(2024, 1, 15) // 2024-01-15T00:00:00Z
DateTime.utc(2024, 1, 15, 14, 30, 45, 123, 456) // with microseconds
DateTime.utc(2024, 1, 15, { locale: 'fr' })

// From object
DateTime.fromObject({
year: 2024,
month: 1,
day: 15,
hour: 14,
minute: 30,
second: 45,
millisecond: 123,
microsecond: 456,
})

// Fractional milliseconds become microseconds
DateTime.fromObject({
year: 2024,
month: 1,
day: 15,
millisecond: 1.5, // 1ms + 500µs
})

From Strings​

// From ISO 8601 string (parses up to 6 fractional digits)
DateTime.fromISO('2024-01-15T14:30:45.123456Z')
DateTime.fromISO('2024-01-15T14:30:45.123456-05:00')
DateTime.fromISO('2024-01-15T14:30:45Z', { zone: 'America/New_York' })

// From SQL datetime string
DateTime.fromSQL('2024-01-15 14:30:45.123456')
DateTime.fromSQL('2024-01-15 14:30:45', { zone: 'America/New_York' })

// From custom format (supports 'u' or 'SSSSSS' for microseconds)
DateTime.fromFormat('01/15/2024', 'MM/dd/yyyy')
DateTime.fromFormat('01/15/2024 14:30:45', 'MM/dd/yyyy HH:mm:ss')
DateTime.fromFormat('01/15/2024 14:30:45.123456', 'MM/dd/yyyy HH:mm:ss.u')
DateTime.fromFormat('01/15/2024 14:30:45.123456', 'MM/dd/yyyy HH:mm:ss.SSSSSS')
DateTime.fromFormat('mai 25, 1982', 'MMMM dd, yyyy', { locale: 'fr' })

From Unix Timestamps​

// From milliseconds (fractional part becomes microseconds)
DateTime.fromMillis(1707234567890)
DateTime.fromMillis(1707234567890.123) // .123ms = 123µs

// From microseconds
DateTime.fromMicroseconds(1707234567890123)

// From seconds (fractional becomes ms + µs)
DateTime.fromSeconds(1707234567)
DateTime.fromSeconds(1707234567.123456) // .123456s = 123ms + 456µs

From JavaScript Date​

DateTime.fromJSDate(new Date())
DateTime.fromJSDate(new Date(), { zone: 'America/New_York' })

Formatting and Output​

ISO Format​

const dt = DateTime.fromISO('2024-01-15T14:30:45.123456-05:00')

// Full ISO datetime with 6 fractional digits (preserves timezone)
dt.toISO() // '2024-01-15T14:30:45.123456-05:00'

// ISO date only
dt.toISODate() // '2024-01-15'

// ISO time only (without timezone offset by default)
dt.toISOTime() // '14:30:45.123456'
dt.toISOTime({ includeOffset: true }) // '14:30:45.123456-05:00'

// Options
dt.toISO({ suppressMilliseconds: true }) // omit fractional when zero
dt.toISO({ suppressSeconds: true }) // omit seconds when zero
dt.toISO({ format: 'basic' }) // compact format without separators

SQL Format​

const dt = DateTime.local(2024, 1, 15, 14, 30, 45, 123, 456)

// SQL datetime (converts to UTC first)
dt.toSQL() // '2024-01-15 19:30:45.123456' (if local was EST)

// SQL date only
dt.toSQLDate() // '2024-01-15'

// SQL time (without offset by default)
dt.toSQLTime() // '14:30:45.123456'
dt.toSQLTime({ includeOffset: true }) // '14:30:45.123456 -05:00'

Custom Format​

const dt = DateTime.local(2024, 1, 15, 14, 30, 45, 123, 456)

// Uses Luxon format tokens
dt.toFormat('yyyy-MM-dd') // '2024-01-15'
dt.toFormat('yyyy-MM-dd HH:mm:ss') // '2024-01-15 14:30:45'
dt.toFormat('yyyy-MM-dd HH:mm:ss.SSS') // '2024-01-15 14:30:45.123'
dt.toFormat('yyyy-MM-dd HH:mm:ss.SSSSSS') // '2024-01-15 14:30:45.123456'
dt.toFormat('MMM dd, yyyy') // 'Jan 15, 2024'
dt.toFormat('MMMM dd, yyyy') // 'January 15, 2024'
dt.toFormat('EEE MMM dd yyyy') // 'Mon Jan 15 2024'

Localized Format​

const dt = DateTime.now()

// Using format presets
dt.toLocaleString(DateTime.DATE_SHORT) // '1/15/2024'
dt.toLocaleString(DateTime.DATE_MED) // 'Jan 15, 2024'
dt.toLocaleString(DateTime.DATE_MED_WITH_WEEKDAY) // 'Mon, Jan 15, 2024'
dt.toLocaleString(DateTime.DATE_FULL) // 'January 15, 2024'
dt.toLocaleString(DateTime.DATE_HUGE) // 'Monday, January 15, 2024'

dt.toLocaleString(DateTime.TIME_SIMPLE) // '2:30 PM'
dt.toLocaleString(DateTime.TIME_WITH_SECONDS) // '2:30:45 PM'
dt.toLocaleString(DateTime.TIME_24_SIMPLE) // '14:30'
dt.toLocaleString(DateTime.TIME_24_WITH_SECONDS) // '14:30:45'

dt.toLocaleString(DateTime.DATETIME_SHORT) // '1/15/2024, 2:30 PM'
dt.toLocaleString(DateTime.DATETIME_MED) // 'Jan 15, 2024, 2:30 PM'
dt.toLocaleString(DateTime.DATETIME_FULL) // 'January 15, 2024, 2:30 PM EST'
dt.toLocaleString(DateTime.DATETIME_HUGE) // 'Monday, January 15, 2024, 2:30 PM Eastern Standard Time'

// Custom format options
dt.toLocaleString(
{
month: 'long',
day: 'numeric',
year: 'numeric',
},
{ locale: 'fr-FR' }
)

For Serialization​

const dt = DateTime.now()

// JSON serialization (ISO format)
dt.toJSON() // '2024-01-15T14:30:45.123456Z'
JSON.stringify({ timestamp: dt })

// String conversion
dt.toString() // '2024-01-15T14:30:45.123456Z'
String(dt)

// valueOf (ISO format)
dt.valueOf() // '2024-01-15T14:30:45.123456Z'

To Unix Timestamps​

const dt = DateTime.fromISO('2024-01-15T14:30:45.123456Z')

// To milliseconds (includes fractional microseconds)
dt.toMillis() // 1705330245123.456

// To microseconds
dt.toMicroseconds() // 1705330245123456

// To seconds (includes fractional ms + µs)
dt.toSeconds() // 1705330245.123456

// Integer timestamps (truncated, not rounded)
dt.unixIntegerSeconds // 1705330245
dt.unixIntegerMilliseconds // 1705330245123
dt.unixIntegerMicroseconds // 1705330245123456

To JavaScript Date​

const dt = DateTime.now()
const jsDate = dt.toJSDate() // JavaScript Date object

To Object​

const dt = DateTime.local(2024, 1, 15, 14, 30, 45, 123, 456)

dt.toObject()
// {
// year: 2024,
// month: 1,
// day: 15,
// hour: 14,
// minute: 30,
// second: 45,
// millisecond: 123,
// microsecond: 456
// }

Getters and Properties​

Date Components​

const dt = DateTime.fromISO('2024-01-15T14:30:45.123456Z')

dt.year // 2024
dt.month // 1 (January)
dt.day // 15
dt.ordinal // 15 (day of year)
dt.weekday // 1 (Monday) to 7 (Sunday)
dt.weekdayName // 'monday', 'tuesday', etc.
dt.weekNumber // ISO week number
dt.weekYear // ISO week year
dt.quarter // 1, 2, 3, or 4

Time Components​

const dt = DateTime.fromISO('2024-01-15T14:30:45.123456Z')

dt.hour // 14
dt.minute // 30
dt.second // 45
dt.millisecond // 123
dt.microsecond // 456 (0-999, doesn't exceed to millisecond)

Timezone Information​

const dt = DateTime.now({ zone: 'America/New_York' })

dt.zoneName // 'America/New_York'
dt.offset // -300 (offset in minutes from UTC)
dt.zone // Zone object

Locale Information​

const dt = DateTime.now({ locale: 'fr-FR' })

dt.locale // 'fr-FR'

Timezone Operations​

const dt = DateTime.fromISO('2024-01-15T14:30:45.123456Z')

// Convert to different timezone
dt.setZone('America/New_York')
dt.setZone('Europe/London')
dt.setZone('Asia/Tokyo')

// Convert to UTC
dt.toUTC()

// Convert to local timezone
dt.toLocal()

// Keep local time when changing zone
dt.setZone('America/New_York', { keepLocalTime: true })

Math Operations​

Adding and Subtracting​

const dt = DateTime.now()

// Add duration
dt.plus({ hours: 2 })
dt.plus({ days: 5, hours: 3 })
dt.plus({ years: 1, months: 2, days: 3 })
dt.plus({ milliseconds: 500, microseconds: 250 })

// Fractional milliseconds become microseconds
dt.plus({ milliseconds: 1.5 }) // adds 1ms + 500µs

// Subtract duration
dt.minus({ hours: 2 })
dt.minus({ days: 5 })
dt.minus({ milliseconds: 1.5 })

// Also accepts number (interpreted as milliseconds)
dt.plus(1000) // add 1 second
dt.minus(1000) // subtract 1 second

Setting Components​

const dt = DateTime.now()

// Set specific components
dt.set({ hour: 14, minute: 30 })
dt.set({ year: 2025, month: 6, day: 15 })
dt.set({ microsecond: 500 })

Difference​

const dt1 = DateTime.fromISO('2024-01-20T14:30:45.123456Z')
const dt2 = DateTime.fromISO('2024-01-15T10:15:30.100200Z')

// Single unit
dt1.diff(dt2, 'days') // { days: 5.178... }
dt1.diff(dt2, 'hours') // { hours: 124.254... }
dt1.diff(dt2, 'minutes') // { minutes: 7455.25 }

// Multiple units
dt1.diff(dt2, ['days', 'hours'])
// { days: 5, hours: 4 }

dt1.diff(dt2, ['hours', 'minutes', 'seconds'])
// { hours: 124, minutes: 15, seconds: 15 }

// With microseconds
dt1.diff(dt2, ['milliseconds', 'microseconds'])
// { milliseconds: 123, microseconds: 256 }

// All units (default)
dt1.diff(dt2)
// { years: 0, months: 0, days: 5, hours: 4, minutes: 15,
// seconds: 15, milliseconds: 23, microseconds: 256 }

// Diff from now
dt1.diffNow('days')
dt1.diffNow(['days', 'hours'])

Boundaries​

const dt = DateTime.fromISO('2024-01-15T14:30:45.123456Z')

// Start of period
dt.startOf('year') // 2024-01-01T00:00:00.000000
dt.startOf('month') // 2024-01-01T00:00:00.000000
dt.startOf('day') // 2024-01-15T00:00:00.000000
dt.startOf('hour') // 2024-01-15T14:00:00.000000
dt.startOf('minute') // 2024-01-15T14:30:00.000000
dt.startOf('second') // 2024-01-15T14:30:45.000000

// End of period
dt.endOf('year') // 2024-12-31T23:59:59.999999
dt.endOf('month') // 2024-01-31T23:59:59.999999
dt.endOf('day') // 2024-01-15T23:59:59.999999
dt.endOf('hour') // 2024-01-15T14:59:59.999999

Comparisons​

Equality​

const dt1 = DateTime.fromISO('2024-01-15T14:30:45.123456Z')
const dt2 = DateTime.fromISO('2024-01-15T14:30:45.123456Z')
const dt3 = DateTime.fromISO('2024-01-15T14:30:45.123457Z')

dt1.equals(dt2) // true
dt1.equals(dt3) // false

Same Period​

const dt1 = DateTime.fromISO('2024-01-15T14:30:45Z')
const dt2 = DateTime.fromISO('2024-01-15T18:45:30Z')

dt1.hasSame(dt2, 'year') // true
dt1.hasSame(dt2, 'month') // true
dt1.hasSame(dt2, 'day') // true
dt1.hasSame(dt2, 'hour') // false

Min and Max​

const dt1 = DateTime.fromISO('2024-01-15T14:30:45Z')
const dt2 = DateTime.fromISO('2024-01-20T10:15:30Z')
const dt3 = DateTime.fromISO('2024-01-10T08:00:00Z')

DateTime.min(dt1, dt2, dt3) // dt3
DateTime.max(dt1, dt2, dt3) // dt2

Configuration​

Reconfigure​

const dt = DateTime.now()

// Change locale
dt.reconfigure({ locale: 'fr-FR' })

// Change locale with setLocale
dt.setLocale('fr-FR')

Global Settings​

import { Settings } from '@rvoh/dream'

// Set default zone
Settings.defaultZone = 'America/New_York'

// Set default locale
Settings.defaultLocale = 'fr-FR'

// Throw on invalid (enabled by default in Dream)
Settings.throwOnInvalid = true

Error Handling​

Every parser on DateTime — fromISO, fromSQL, fromFormat, fromObject — throws InvalidDateTime on unparsable or calendar-impossible input rather than returning a value you can probe. There is no "invalid instance" to inspect: DateTime.fromISO('garbage') throws immediately, so a guard like if (!DateTime.fromISO(v).toISODate()) { ... } never reaches its check — the throw happens first. To validate untrusted input (an API param, an LLM-emitted value), parse it and catch the specific error instead:

InvalidDateTime is exported from @rvoh/dream/errors, not @rvoh/dream:

import { DateTime } from '@rvoh/dream'
import { InvalidDateTime } from '@rvoh/dream/errors'

function parseISODateTimeOrNull(v: string): DateTime | null {
try {
return DateTime.fromISO(v)
} catch (e) {
if (e instanceof InvalidDateTime) return null
throw e
}
}

Catch only the specific parse error you expect, not every error — an unguarded catch hides bugs that have nothing to do with parsing.

Usage with Models​

DateTime instances are automatically created when loading timestamp columns from the database:

import { Model } from '@rvoh/psychic'
import { DateTime } from '@rvoh/dream'

class Event extends Model {
id!: number
createdAt!: DateTime // timestamp with time zone
updatedAt!: DateTime // timestamp with time zone
scheduledAt!: DateTime // timestamp without time zone
}

// Load from database
const event = await Event.find(1)

// All timestamp fields are DateTime instances
console.log(event.createdAt.toISO())
console.log(event.scheduledAt.plus({ days: 1 }))

// Modify and save (converts to UTC automatically)
event.scheduledAt = DateTime.fromISO('2024-06-15T14:30:00-05:00')
await event.save() // stored as UTC in database