ClockTime
Never use JavaScript's native Date class in Psychic applications. For time-only values without timezone, use Dream's ClockTime. JavaScript's Date always includes date and timezone, causing confusion. Dream's classes provide precise control over what you're representing.
// ❌ DON'T DO THIS
const badTime = new Date('1970-01-01T14:30:45')
// ✅ DO THIS INSTEAD
import { ClockTime } from '@rvoh/dream'
const goodTime = ClockTime.fromObject({ hour: 14, minute: 30, second: 45 })
const formatted = goodTime.toLocaleString({ hour: '2-digit', minute: '2-digit' })
Overview
ClockTime represents a time of day without timezone information. It's useful for:
- Business hours (e.g., "Open 9:00 AM - 5:00 PM")
- Schedules and recurring events (e.g., "Meeting every day at 2:30 PM")
- Time comparisons independent of date or timezone
- Representing Postgres
TIME WITHOUT TIME ZONEfields
Key characteristics:
- No timezone information (all timezone data is stripped)
- Microsecond precision (6 decimal places)
- No date component
- Suitable for abstract "time of day" concepts
Postgres Mapping
Database time without time zone columns are automatically converted to ClockTime objects when Dream models are loaded from the database.
Import
import { ClockTime } from '@rvoh/dream'
Creating ClockTime Instances
Current Time
// Current time (timezone info stripped)
ClockTime.now()
From Components
// From object
ClockTime.fromObject({ hour: 14, minute: 30 })
ClockTime.fromObject({ hour: 14, minute: 30, second: 45 })
ClockTime.fromObject({
hour: 14,
minute: 30,
second: 45,
millisecond: 123,
microsecond: 456,
})
// With options
ClockTime.fromObject({ hour: 14, minute: 30 }, { locale: 'fr-FR' })
From Strings
All parsing methods strip timezone information from the input string, preserving only the time values.
// From ISO 8601 time string (timezone ignored)
ClockTime.fromISO('14:30:45')
ClockTime.fromISO('14:30:45.123456')
ClockTime.fromISO('14:30:45-05:00') // stores 14:30:45, ignores -05:00
ClockTime.fromISO('14:30:45.123456+02:00') // stores 14:30:45.123456, ignores +02:00
// From SQL time string (timezone ignored)
ClockTime.fromSQL('14:30:45')
ClockTime.fromSQL('14:30:45.123456')
ClockTime.fromSQL('14:30:45+05:30') // stores 14:30:45, ignores +05:30
// From custom format
ClockTime.fromFormat('14:30:45', 'HH:mm:ss')
ClockTime.fromFormat('2:30 PM', 'h:mm a')
ClockTime.fromFormat('02:30:45', 'hh:mm:ss')
From DateTime
import { DateTime } from '@rvoh/dream'
const dt = DateTime.now()
ClockTime.fromDateTime(dt) // extracts time portion, strips timezone
From JavaScript Date
ClockTime.fromJSDate(new Date()) // extracts time, strips timezone
Formatting and Output
All output methods omit timezone offset by default.
ISO Format
const time = ClockTime.fromObject({ hour: 14, minute: 30, second: 45, millisecond: 123, microsecond: 456 })
// ISO time without timezone offset
time.toISO() // '14:30:45.123456'
time.toISOTime() // '14:30:45.123456' (alias)
// With options
time.toISO({ suppressMilliseconds: true }) // '14:30:45' (if ms/µs are zero)
time.toISO({ suppressSeconds: true }) // '14:30' (if seconds are zero)
time.toISO({ format: 'basic' }) // '143045.123456' (compact)
SQL Format
const time = ClockTime.fromISO('14:30:45.123456')
// SQL time without timezone offset
time.toSQL() // '14:30:45.123456'
time.toSQLTime() // '14:30:45.123456' (alias)
Localized Format
const time = ClockTime.fromObject({ hour: 14, minute: 30 })
// Default locale
time.toLocaleString() // '2:30 PM' (12-hour in US locale)
// Custom format options
time.toLocaleString({
hour: '2-digit',
minute: '2-digit',
}) // '02:30 PM'
time.toLocaleString({
hour: '2-digit',
minute: '2-digit',
hour12: false,
}) // '14:30' (24-hour)
time.toLocaleString({
hour: 'numeric',
minute: '2-digit',
second: '2-digit',
}) // '2:30:00 PM'
For Serialization
const time = ClockTime.now()
// JSON serialization (ISO format without timezone)
time.toJSON() // '14:30:45.123456'
JSON.stringify({ time }) // '{"time":"14:30:45.123456"}'
// String conversion
time.toString() // '14:30:45.123456'
String(time)
// valueOf
time.valueOf() // '14:30:45.123456'
Getters and Properties
const time = ClockTime.fromISO('14:30:45.123456')
time.hour // 14
time.minute // 30
time.second // 45
time.millisecond // 123
time.microsecond // 456
Conversions
const time = ClockTime.fromISO('14:30:45')
// To DateTime (with base date of 2000-01-01 in UTC)
const dt = time.toDateTime()
Math Operations
Adding and Subtracting
const time = ClockTime.fromObject({ hour: 14, minute: 30 })
// Add duration
time.plus({ hours: 2 }) // 16:30:00
time.plus({ hours: 2, minutes: 15 }) // 16:45:00
time.plus({ minutes: 30 }) // 15:00:00
time.plus({ seconds: 45 }) // 14:30:45
time.plus({ milliseconds: 500, microseconds: 250 })
// Wraps around midnight
time.plus({ hours: 10 }) // 00:30:00 (next day, but date ignored)
// Subtract duration
time.minus({ hours: 2 }) // 12:30:00
time.minus({ minutes: 45 }) // 13:45:00
// Wraps around midnight
time.minus({ hours: 15 }) // 23:30:00 (previous day, but date ignored)
Setting Components
const time = ClockTime.fromObject({ hour: 14, minute: 30, second: 45 })
// Set specific components
time.set({ hour: 9 }) // 09:30:45
time.set({ minute: 0 }) // 14:00:45
time.set({ hour: 16, minute: 45 }) // 16:45:45
time.set({ microsecond: 500 }) // 14:30:45.000500
Difference
The diff() method returns an object with the requested time units.
const t1 = ClockTime.fromObject({ hour: 16, minute: 45 })
const t2 = ClockTime.fromObject({ hour: 14, minute: 30 })
// Single unit
t1.diff(t2, 'hours') // { hours: 2.25 }
t1.diff(t2, 'minutes') // { minutes: 135 }
t1.diff(t2, 'seconds') // { seconds: 8100 }
// Multiple units
t1.diff(t2, ['hours', 'minutes'])
// { hours: 2, minutes: 15 }
t1.diff(t2, ['hours', 'minutes', 'seconds'])
// { hours: 2, minutes: 15, seconds: 0 }
// With microsecond precision
const t3 = ClockTime.fromISO('16:45:30.123456')
const t4 = ClockTime.fromISO('14:30:15.100200')
t3.diff(t4, ['hours', 'minutes', 'seconds', 'milliseconds', 'microseconds'])
// { hours: 2, minutes: 15, seconds: 15, milliseconds: 23, microseconds: 256 }
// All units (default: hours, minutes, seconds, milliseconds, microseconds)
t1.diff(t2)
// { hours: 2, minutes: 15, seconds: 0, milliseconds: 0, microseconds: 0 }
Boundaries
const time = ClockTime.fromObject({ hour: 14, minute: 30, second: 45 })
// Start of period
time.startOf('hour') // 14:00:00.000000
time.startOf('minute') // 14:30:00.000000
time.startOf('second') // 14:30:45.000000
// End of period
time.endOf('hour') // 14:59:59.999999
time.endOf('minute') // 14:30:59.999999
time.endOf('second') // 14:30:45.999999
Comparisons
Equality
const t1 = ClockTime.fromObject({ hour: 14, minute: 30 })
const t2 = ClockTime.fromObject({ hour: 14, minute: 30 })
const t3 = ClockTime.fromObject({ hour: 14, minute: 31 })
t1.equals(t2) // true
t1.equals(t3) // false
Same Period
const t1 = ClockTime.fromObject({ hour: 14, minute: 30, second: 45 })
const t2 = ClockTime.fromObject({ hour: 14, minute: 45, second: 30 })
t1.hasSame(t2, 'hour') // true
t1.hasSame(t2, 'minute') // false
t1.hasSame(t2, 'second') // false
Min and Max
const t1 = ClockTime.fromObject({ hour: 14, minute: 30 })
const t2 = ClockTime.fromObject({ hour: 16, minute: 45 })
const t3 = ClockTime.fromObject({ hour: 9, minute: 15 })
ClockTime.min(t1, t2, t3) // t3 (09:15:00)
ClockTime.max(t1, t2, t3) // t2 (16:45:00)
Usage with Models
ClockTime instances are automatically created when loading time without time zone columns from the database:
import { Model } from '@rvoh/psychic'
import { ClockTime } from '@rvoh/dream'
class BusinessHours extends Model {
id!: number
dayOfWeek!: string
openTime!: ClockTime // time without time zone
closeTime!: ClockTime // time without time zone
}
// Load from database
const hours = await BusinessHours.findBy({ dayOfWeek: 'Monday' })
// All time fields are ClockTime instances
console.log(hours.openTime.toISO()) // '09:00:00.000000'
console.log(hours.closeTime.toISO()) // '17:00:00.000000'
// Check if currently open
const now = ClockTime.now()
const isOpen = now >= hours.openTime && now <= hours.closeTime
// Modify and save
hours.openTime = ClockTime.fromObject({ hour: 8, minute: 30 })
await hours.save()
Common Patterns
Time Ranges
const openTime = ClockTime.fromObject({ hour: 9, minute: 0 })
const closeTime = ClockTime.fromObject({ hour: 17, minute: 0 })
const now = ClockTime.now()
// Check if within range
const isOpen = now >= openTime && now <= closeTime
Duration Between Times
const start = ClockTime.fromObject({ hour: 9, minute: 0 })
const end = ClockTime.fromObject({ hour: 17, minute: 30 })
const duration = end.diff(start, ['hours', 'minutes'])
// { hours: 8, minutes: 30 }
const totalHours = end.diff(start, 'hours').hours
// 8.5
Recurring Events
// Store meeting time without timezone
const meetingTime = ClockTime.fromObject({ hour: 14, minute: 30 })
// Meeting happens at this time every day, regardless of timezone
// Each timezone interprets it in their local time
Business Hours Calculation
const businessStart = ClockTime.fromObject({ hour: 9, minute: 0 })
const businessEnd = ClockTime.fromObject({ hour: 17, minute: 0 })
const lunchStart = ClockTime.fromObject({ hour: 12, minute: 0 })
const lunchEnd = ClockTime.fromObject({ hour: 13, minute: 0 })
// Total business hours
const totalHours = businessEnd.diff(businessStart, 'hours').hours // 8
// Working hours (excluding lunch)
const workingHours = totalHours - lunchEnd.diff(lunchStart, 'hours').hours // 7
Error Handling
ClockTime throws InvalidClockTime errors for invalid operations:
import { ClockTime, InvalidClockTime } from '@rvoh/dream'
try {
const time = ClockTime.fromISO('25:00:00') // invalid hour
} catch (e) {
if (e instanceof InvalidClockTime) {
console.error(e.message)
}
}
// Invalid time values
try {
ClockTime.fromObject({ hour: 25, minute: 0 }) // hour out of range
} catch (e) {
// InvalidClockTime
}
try {
ClockTime.fromISO('invalid-time')
} catch (e) {
// InvalidClockTime
}