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
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,10 @@
"namespace": "@tanstack",
"devDependencies": {
"@solidjs/testing-library": "^0.8.6",
"@tanstack/config": "^0.6.0",
"@tanstack/config": "^0.18.0",
"@tanstack/eslint-config": "^0.1.0",
"@tanstack/publish-config": "^0.1.0",
"@tanstack/typedoc-config": "^0.2.0",
"@tanstack/vite-config": "^0.2.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular-time/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import { tanstackViteConfig } from '@tanstack/config/vite'

const config = defineConfig({
test: {
Expand Down
2 changes: 1 addition & 1 deletion packages/react-time/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,6 @@
},
"devDependencies": {
"@types/use-sync-external-store": "^0.0.3",
"@vitejs/plugin-react": "^4.2.1"
"@vitejs/plugin-react": "^4.4.1"
}
}
2 changes: 1 addition & 1 deletion packages/react-time/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import { tanstackViteConfig } from '@tanstack/config/vite'
import react from '@vitejs/plugin-react'

const config = defineConfig({
Expand Down
3 changes: 2 additions & 1 deletion packages/solid-time/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import { tanstackViteConfig } from '@tanstack/vite-config'
import { tanstackViteConfig } from '@tanstack/config/vite'

import solid from 'vite-plugin-solid'

const config = defineConfig({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Locale } from '../formatter/shared'

export interface IDateDefaults {
export interface DateDefaults {
calendar: string
locale: string
timeZone: string
Expand All @@ -14,9 +14,9 @@ const {

/**
* getDateDefaults
* @returns IDateDefaults
* @returns DateDefaults
*/
export function getDateDefaults(): IDateDefaults {
export function getDateDefaults(): DateDefaults {
return {
calendar: defaultCalendar,
locale: defaultLocale,
Expand Down
63 changes: 63 additions & 0 deletions packages/time/src/date/fromZonedDateTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import './setupTemporal'
import { Temporal } from '@js-temporal/polyfill'

export type ReturnFormat = 'standard' | 'long' | 'epoch' | 'Date' | 'ZonedDateTime'

export interface FromZonedDateTimeResult<T extends ReturnFormat> {
value: T extends 'ZonedDateTime'
? Temporal.ZonedDateTime
: T extends 'epoch'
? number
: T extends 'Date'
? Date
: string
options: {
timeZone: string
calendar: string
}
}

/**
* Converts a Temporal.ZonedDateTime to the requested return format
* @param zdt - The ZonedDateTime to convert
* @param returnFormat - The desired return format
* @returns The converted value and options
*/
export function fromZonedDateTime<T extends ReturnFormat>(
zdt: Temporal.ZonedDateTime,
returnFormat: T = 'standard' as T,
): FromZonedDateTimeResult<T> {
const timeZone = zdt.timeZoneId
const calendar = zdt.calendarId

let value: Temporal.ZonedDateTime | number | Date | string

switch (returnFormat) {
case 'ZonedDateTime':
value = zdt
break
case 'epoch':
value = zdt.epochMilliseconds
break
case 'Date':
value = new Date(zdt.epochMilliseconds)
break
case 'long':
// ISO 8601 extended format with timezone and calendar
value = `${zdt.toInstant().toString()}[${timeZone}][u-ca=${calendar}]`
break
case 'standard':
default:
// Standard ISO 8601 / RFC 3339 format
value = zdt.toInstant().toString()
break
}

return {
value: value as FromZonedDateTimeResult<T>['value'],
options: {
timeZone,
calendar,
},
}
}
1 change: 1 addition & 0 deletions packages/time/src/date/isValidDate/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './isValidDate'
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
* @param date Date
* @returns boolean
*/
export function isValidDate(date: any): boolean {
export function isValidDate(date: unknown): date is Date {
if (Object.prototype.toString.call(date) !== '[object Date]') {
return false
}
return date.getTime() === date.getTime()
const dateObj = date as Date
return dateObj.getTime() === dateObj.getTime()
}
16 changes: 16 additions & 0 deletions packages/time/src/date/isValidDate/tests/isValidDate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {describe, expect, test} from 'vitest';
import {isValidDate} from '../isValidDate';

describe('isValidDate', () => {
test('should return true for a valid date', () => {
expect(isValidDate(new Date())).toBe(true);
})

test('should return false for an invalid date', () => {
expect(isValidDate(new Date("invalid"))).toBe(false);
});

test("should return false for null", () => {
expect(isValidDate(null)).toBe(false);
});
});
1 change: 1 addition & 0 deletions packages/time/src/date/parse/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './parse'
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ declare global {

if (!('Temporal' in globalThis)) {
// Attach Temporal to the global object if it doesn't exist
;(globalThis as any).Temporal = Temporal
;(globalThis as Record<string, unknown>).Temporal = Temporal
}
98 changes: 98 additions & 0 deletions packages/time/src/date/startOf.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import './setupTemporal'
import { Temporal } from '@js-temporal/polyfill'
import { getDefaultCalendar, getDefaultLocale, getDefaultTimeZone, normalizeLocale } from './dateDefaults'
import { toZonedDateTime, type ToZonedDateTimeOptions } from './toZonedDateTime'
import {
fromZonedDateTime,
type ReturnFormat,
type FromZonedDateTimeResult,
} from './fromZonedDateTime'

export type StartOfUnit =
| 'year'
| 'month'
| 'week'
| 'day'
| 'hour'
| 'minute'
| 'second'
| 'millisecond'

export interface StartOfOptions extends ToZonedDateTimeOptions {
returnFormat?: ReturnFormat
}

export interface StartOfParams {
date: string | number | Date | Temporal.ZonedDateTime
unit: StartOfUnit
returnFormat?: ReturnFormat
options?: ToZonedDateTimeOptions
}

/**
* Returns the start of a given unit of time for a date
* @param params - Parameters object containing date, unit, returnFormat, and options
* @returns Object containing the value in the requested format and options (timeZone, calendar)
*/
export function startOf<T extends ReturnFormat = 'standard'>({
date,
unit,
returnFormat = 'standard' as T,
options = {},
}: StartOfParams): FromZonedDateTimeResult<T> {
const mergedOptions: ToZonedDateTimeOptions = {
timeZone: options.timeZone ?? getDefaultTimeZone(),
calendar: options.calendar ?? getDefaultCalendar(),
}

// Convert input to ZonedDateTime
let zdt = toZonedDateTime(date, mergedOptions)

// Apply startOf logic based on unit
switch (unit) {
case 'year':
zdt = zdt.with({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 })
break
case 'month':
zdt = zdt.with({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 })
break
case 'week': {
// Use native getWeekInfo API to get the first day of the week based on locale
const defaultLocale = getDefaultLocale()
const normalizedLocale = normalizeLocale(defaultLocale)
const localeString = Array.isArray(normalizedLocale)
? normalizedLocale[0] ?? 'en-US'
: normalizedLocale
const locale = new Intl.Locale(localeString)
const weekInfo = locale.getWeekInfo()
const firstDay = weekInfo.firstDay
const dayOfWeek = zdt.dayOfWeek
const daysInWeek = zdt.daysInWeek

const daysToSubtract = (dayOfWeek - firstDay + daysInWeek) % daysInWeek

zdt = zdt
.subtract({ days: daysToSubtract })
.with({ hour: 0, minute: 0, second: 0, millisecond: 0 })
break
}
case 'day':
zdt = zdt.with({ hour: 0, minute: 0, second: 0, millisecond: 0 })
break
case 'hour':
zdt = zdt.with({ minute: 0, second: 0, millisecond: 0 })
break
case 'minute':
zdt = zdt.with({ second: 0, millisecond: 0 })
break
case 'second':
zdt = zdt.with({ millisecond: 0 })
break
case 'millisecond':
break
default:
throw new Error(`Invalid unit: "${unit}". Must be one of: year, month, week, day, hour, minute, second, millisecond`)
}

return fromZonedDateTime(zdt, returnFormat)
}
83 changes: 83 additions & 0 deletions packages/time/src/date/toZonedDateTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import './setupTemporal'
import { Temporal } from '@js-temporal/polyfill'
import { getDefaultCalendar, getDefaultTimeZone } from './dateDefaults'
import { validateDate } from './validateDate'

export interface ToZonedDateTimeOptions {
timeZone?: string
calendar?: string
}

/**
* Checks if a value is already a Temporal.ZonedDateTime
*/
function isZonedDateTime(
value: unknown,
): value is Temporal.ZonedDateTime {
if (
typeof value !== 'object' ||
value === null ||
!('timeZone' in value) ||
!('calendar' in value)
) {
return false
}

const candidate = value as Record<string, unknown>
return (
typeof candidate.timeZone === 'string' &&
typeof candidate.calendar === 'string'
)
}

/**
* Converts a date input (string, number, Date, or ZonedDateTime) to a Temporal.ZonedDateTime
* @param date - The date input (RFC 3339 string, epoch number, Date object, or ZonedDateTime)
* @param options - Options containing timeZone and calendar
* @returns Temporal.ZonedDateTime
*/
export function toZonedDateTime(
date: string | number | Date | Temporal.ZonedDateTime,
options: ToZonedDateTimeOptions = {},
): Temporal.ZonedDateTime {
const timeZone = options.timeZone ?? getDefaultTimeZone()
const calendar = options.calendar ?? getDefaultCalendar()

// If already a ZonedDateTime, return as-is
if (isZonedDateTime(date)) {
return date
}

let dateString: string

if (typeof date === 'string') {
// Check if string already includes timezone and calendar
if (date.includes('[') && date.includes(']')) {
// String already has timezone/calendar info, parse directly
return Temporal.ZonedDateTime.from(date)
}
// Validate and parse the string
const parsedDate = validateDate({
date,
errorMessage: `Invalid date string: "${date}"`,
})
dateString = parsedDate.toISOString()
} else if (typeof date === 'number') {
// Epoch time - convert to Date first, then to ISO string
const parsedDate = validateDate({
date,
errorMessage: `Invalid epoch time: "${date}"`,
})
dateString = parsedDate.toISOString()
} else if (date instanceof Date) {
// Date object - convert to ISO string
dateString = date.toISOString()
} else {
throw new Error(`Invalid date input type: ${typeof date}`)
}

// Convert to ZonedDateTime with timezone and calendar
return Temporal.ZonedDateTime.from(
`${dateString}[${timeZone}][u-ca=${calendar}]`,
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { getDefaultLocale, normalizeLocale } from '../utils/dateDefaults'
import { extractLocaleOptions } from './extractLocaleOptions'
import type { IDateFormatterBuildParams, IDateFormatterOptions } from './shared'
import { getDefaultLocale, normalizeLocale } from '../../date/dateDefaults'
import { extractLocaleOptions } from '../extractLocaleOptions'
import type { DateFormatterBuildParams, DateFormatterOptions } from '../shared'

/**
* Function: buildDateFormatter
Expand All @@ -20,18 +20,18 @@ import type { IDateFormatterBuildParams, IDateFormatterOptions } from './shared'
* When using UTC date strings, it is suggested that you use the 'options' object
* to set the 'timeZone' when building the formatter. The 'timeZone' is defaulted
* to the user's browser timezone.
* @param {IDateFormatterBuildParams} [param0]
* @param {DateFormatterBuildParams} [param0]
* @returns Intl.DateTimeFormat
*/
export function buildDateFormatter({
locale = getDefaultLocale(),
options,
}: IDateFormatterBuildParams = {}): Intl.DateTimeFormat {
}: DateFormatterBuildParams): Intl.DateTimeFormat {
const normalizedLocale = normalizeLocale(locale)
const opts =
typeof options === 'string' ? { dateStyle: options } : (options ?? {})
const { formatOptions = {}, ...localeOptions } = extractLocaleOptions(opts)
const { dateStyle, ...rest } = formatOptions as IDateFormatterOptions
const { dateStyle, ...rest } = formatOptions as DateFormatterOptions
const newOptions = {
...localeOptions,
...(dateStyle ? { dateStyle } : rest),
Expand Down
1 change: 1 addition & 0 deletions packages/time/src/formatter/buildDateFormatter/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './buildDateFormatter'
Loading
Loading