| name | stacks-datetime |
| description | Use when working with dates and times in Stacks — the DateTime class with Carbon-like API (add/sub, comparison, formatting, start/end of day/month/year), date parsing, format tokens, or timezone handling. Covers @stacksjs/datetime. |
| license | MIT |
| compatibility | Bun >= 1.3.0, TypeScript |
| allowed-tools | Read Edit Write Bash Grep Glob |
Stacks DateTime
Carbon-inspired DateTime class with immutable operations and zero external dependencies.
Key Paths
- Core package:
storage/framework/core/datetime/src/
- Package:
@stacksjs/datetime
Architecture
The index.ts exports:
export { DateTime, now } from './now'
export { format } from './format'
export { parse } from './parse'
export { format as dateFormat } from './format'
Three source files:
now.ts — DateTime class and now() helper
format.ts — standalone format() function with token-based formatting and timezone support
parse.ts — standalone parse() function with token-based parsing
DateTime Class (now.ts)
Construction
import { DateTime, now } from '@stacksjs/datetime'
const dt = new DateTime()
const dt = new DateTime(new Date())
const dt = new DateTime('2024-06-15')
const dt = new DateTime('15/06/2024', 'DD/MM/YYYY')
const dt = DateTime.now()
const dt = DateTime.create(2024, 6, 15, 10, 30, 0)
const dt = DateTime.fromDate(new Date())
const dt = DateTime.parse('2024-06-15')
dt = .(, )
Note: DateTime.create() uses 1-based months (1=January), converting internally with month - 1.
Getters (read-only properties)
dt.year
dt.month
dt.day
dt.hour
dt.minute
dt.second
dt.dayOfWeek
dt.timestamp
Formatting
dt.format('YYYY-MM-DD HH:mm:ss')
dt.format('MMMM D, YYYY', 'en')
dt.format('HH:mm', { locale: 'de', tz: 'Europe/Berlin' })
dt.toDateString()
dt.toTimeString()
dt.toDateTimeString()
dt.toISOString()
dt.toString()
dt.toJSON()
Format Tokens
Used by both DateTime.format() and the standalone format() function:
| Token | Output | Description |
|---|
| YYYY | 2024 | 4-digit year |
| YY | 24 | 2-digit year |
| MMMM | January | Full month name (via Intl.DateTimeFormat) |
| MMM | Jan | Short month name (via Intl.DateTimeFormat) |
| MM | 01 | 2-digit month (01-12) |
| M | 1 | Month (1-12) |
| DD | 01 | 2-digit day (01-31) |
| D | 1 | Day (1-31) |
| dddd | Wednesday | Full weekday name (via Intl.DateTimeFormat) |
| ddd | Wed | Short weekday name (via Intl.DateTimeFormat) |
| d | W | Narrow weekday (via Intl.DateTimeFormat) |
| HH | 00 | 24-hour padded (00-23) |
| H | 0 | 24-hour (0-23) |
| hh | 12 | 12-hour padded (01-12) |
| h | 12 | 12-hour (1-12) |
| mm | 05 | Minutes padded (00-59) |
| m | 5 | Minutes (0-59) |
| ss | 09 | Seconds padded (00-59) |
| s | 9 | Seconds (0-59) |
| A | AM/PM | Uppercase AM/PM |
| a | am/pm | Lowercase am/pm |
| Z | +0530 | Timezone offset (no colon, e.g. +0800, -0500) |
Tokens are matched longest-first to avoid partial matching.
Arithmetic (all return NEW DateTime instances -- immutable)
dt.addSeconds(30) dt.subSeconds(30)
dt.addMinutes(15) dt.subMinutes(15)
dt.addHours(2) dt.subHours(2)
dt.addDays(7) dt.subDays(7)
dt.addWeeks(1) dt.subWeeks(1)
dt.addMonths(3) dt.subMonths(3)
dt.addYears(1) dt.subYears(1)
Implementation details:
addSeconds/Minutes/Hours use millisecond arithmetic on timestamp
addDays uses Date.setDate() (handles month boundaries correctly)
addMonths uses Date.setMonth() (handles year boundaries)
addYears uses Date.setFullYear()
- All
sub* methods delegate to add*(-n)
Period Boundaries (return NEW DateTime instances)
dt.startOfDay()
dt.endOfDay()
dt.startOfMonth()
dt.endOfMonth()
dt.startOfYear()
dt.endOfYear()
Setters (return NEW DateTime instances)
dt.setYear(2025)
dt.setMonth(6)
dt.setDay(15)
dt.setHour(10)
dt.setMinute(30)
dt.setSecond(0)
Comparison
dt.isBefore(other)
dt.isAfter(other)
dt.isSame(other)
dt.isSameDay(other)
dt.isBetween(start, end)
dt.isPast()
dt.isFuture()
dt.isToday()
dt.isLeapYear()
All comparison methods accept DateTime | Date as the other parameter.
Difference
dt.diffInSeconds(other)
dt.diffInMinutes(other)
dt.diffInHours(other)
dt.diffInDays(other)
Returns signed values: positive if this is after other, negative if before.
Conversion
dt.toNativeDate()
dt.valueOf()
dt.toJSON()
Standalone format() Function (format.ts)
import { format } from '@stacksjs/datetime'
format(new Date(), 'YYYY-MM-DD')
format('2024-06-15', 'MMMM D, YYYY')
format(new Date(), 'dddd, MMMM D, YYYY h:mm A')
format(new Date(), 'dddd, MMMM D', 'de')
format(new Date(), 'YYYY-MM-DD HH:mm Z', { tz: 'America/New_York' })
format(new Date(), 'HH:mm', { locale: 'en', tz: 'Asia/Tokyo' })
Signature: format(inputDate: Date | string, formatStr?: string, localeOrOptions?: string | { locale?: string, tz?: string })
Default format string: 'YYYY-MM-DD'
Default locale: 'en'
Timezone Support
When a tz option is provided, the function uses Intl.DateTimeFormat with timeZone to extract date parts in the target timezone. The timezone offset (Z token) is computed by comparing UTC wall-clock time to the timezone's wall-clock time.
Without tz, uses local time via native Date getters (faster path, no Intl overhead for numeric parts). Named parts (month names, weekday names) always use Intl for locale support.
Standalone parse() Function (parse.ts)
import { parse } from '@stacksjs/datetime'
parse('2024-06-15')
parse('2024-06-15T10:30:00Z')
parse('15/06/2024', 'DD/MM/YYYY')
parse('June 15, 2024', 'MMMM DD, YYYY')
parse('2024-06-15 10:30:00', 'YYYY-MM-DD HH:mm:ss')
parse('03:30 PM', 'hh:mm A')
parse('2024-06-15 10:30 +0530', 'YYYY-MM-DD HH:mm Z')
Parse Behavior
Without format string:
- Date-only ISO strings (YYYY-MM-DD) are parsed as LOCAL time, not UTC -- this is a deliberate fix for the native Date behavior where
new Date('2024-06-15') treats it as UTC causing day shifts
- All other strings use native
new Date() parsing
- Throws on invalid dates
With format string:
- Builds a regex from the format tokens and extracts named groups
- Supports tokens: YYYY, YY, MMMM, MMM, MM, M, DD, D, HH, H, hh, h, mm, m, ss, s, A, a, Z
- YY: years 70-99 become 1900s, 00-69 become 2000s
- AM/PM: adjusts hours for 12-hour format (12 AM = 0, 12 PM = 12)
- Month names: handles full ("January") and short ("Jan") names, case-insensitive
- Z token: parses +HHMM offset, constructs UTC time and adjusts
- Throws if the format doesn't match the input string
now() Helper
import { now } from '@stacksjs/datetime'
now()
now().toDateString()
now().format('MMMM D, YYYY')
now().addDays(7).toDateString()
now().startOfMonth().toDateString()
Gotchas
- All DateTime arithmetic operations return NEW instances -- the original is never mutated
DateTime.month returns 1-12 (1-based), NOT 0-11 like native Date
DateTime.create(year, month) takes 1-based month -- internally converts with month - 1
dayOfWeek returns 0 (Sunday) through 6 (Saturday) -- matches native Date.getDay()
timestamp returns MILLISECONDS since epoch, not seconds
diffIn* methods return signed values -- positive when this is after other
isBetween(start, end) is EXCLUSIVE on both ends (strictly between)
parse('2024-06-15') without format is treated as LOCAL time (intentional deviation from spec)
- The
Z timezone offset token format is +HHMM (no colon) -- e.g. +0530, -0800
format() with tz option uses Intl.DateTimeFormat which requires valid IANA timezone names
- The
d token in formatting returns the NARROW weekday (single letter like "W"), not a day number
endOfMonth() uses the new Date(year, month + 1, 0) trick to find the last day
- Application timezone should be configured in
config/app.ts
- Prefer
@stacksjs/datetime over raw Date for framework consistency