diff --git a/.changeset/cyan-guests-sip.md b/.changeset/cyan-guests-sip.md new file mode 100644 index 0000000..8940248 --- /dev/null +++ b/.changeset/cyan-guests-sip.md @@ -0,0 +1,10 @@ +--- +'@layerstack/svelte-actions': patch +'@layerstack/svelte-stores': patch +'@layerstack/svelte-state': patch +'@layerstack/svelte-table': patch +'@layerstack/tailwind': patch +'@layerstack/utils': patch +--- + +refactor: Replace `date-fns` usage with new date utils (based on d3-time) to reduce bundle size diff --git a/.changeset/odd-hands-tie.md b/.changeset/odd-hands-tie.md new file mode 100644 index 0000000..ca48826 --- /dev/null +++ b/.changeset/odd-hands-tie.md @@ -0,0 +1,10 @@ +--- +'@layerstack/svelte-actions': patch +'@layerstack/svelte-stores': patch +'@layerstack/svelte-state': patch +'@layerstack/svelte-table': patch +'@layerstack/tailwind': patch +'@layerstack/utils': patch +--- + +feat: Add new date utils including `parseDate`, `timeInterval`, `startOfInterval`, `endOfInterval`, `intervalOffset`, and more diff --git a/packages/svelte-actions/package.json b/packages/svelte-actions/package.json index 4a7a174..4ecef5b 100644 --- a/packages/svelte-actions/package.json +++ b/packages/svelte-actions/package.json @@ -41,7 +41,6 @@ "@layerstack/utils": "workspace:*", "d3-array": "^3.2.4", "d3-scale": "^4.0.2", - "date-fns": "^4.1.0", "lodash-es": "^4.17.21" }, "main": "./dist/index.js", diff --git a/packages/svelte-stores/package.json b/packages/svelte-stores/package.json index fb23ccf..5ace2e6 100644 --- a/packages/svelte-stores/package.json +++ b/packages/svelte-stores/package.json @@ -39,7 +39,6 @@ "dependencies": { "@layerstack/utils": "workspace:*", "d3-array": "^3.2.4", - "date-fns": "^4.1.0", "immer": "^10.1.1", "lodash-es": "^4.17.21", "zod": "^3.24.3" diff --git a/packages/svelte-table/package.json b/packages/svelte-table/package.json index a4907b2..7211f87 100644 --- a/packages/svelte-table/package.json +++ b/packages/svelte-table/package.json @@ -39,7 +39,6 @@ "@layerstack/svelte-actions": "workspace:*", "@layerstack/utils": "workspace:*", "d3-array": "^3.2.4", - "date-fns": "^4.1.0", "lodash-es": "^4.17.21" }, "main": "./dist/index.js", diff --git a/packages/svelte-table/src/lib/utils.ts b/packages/svelte-table/src/lib/utils.ts index 57cf341..3739c6a 100644 --- a/packages/svelte-table/src/lib/utils.ts +++ b/packages/svelte-table/src/lib/utils.ts @@ -1,7 +1,6 @@ import { isFunction, get } from 'lodash-es'; -import { parseISO } from 'date-fns'; -import { PeriodType } from '@layerstack/utils'; +import { PeriodType, parseDate } from '@layerstack/utils'; import type { ColumnDef } from './types.js'; @@ -118,7 +117,7 @@ export function getCellValue(column: ColumnDef, rowData: any, rowIndex?: number) // TODO: Should we handle date-only strings different? // value = new Date(value); // console.log({ column: column.name, value }); - value = parseISO(value); + value = parseDate(value); } return value; diff --git a/packages/tailwind/package.json b/packages/tailwind/package.json index 2a8974a..9d2b04f 100644 --- a/packages/tailwind/package.json +++ b/packages/tailwind/package.json @@ -39,7 +39,6 @@ "clsx": "^2.1.1", "culori": "^4.0.1", "d3-array": "^3.2.4", - "date-fns": "^4.1.0", "lodash-es": "^4.17.21", "tailwind-merge": "^3.2.0", "tailwindcss": "^4.1.5" diff --git a/packages/utils/package.json b/packages/utils/package.json index b5fedec..6a4667e 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -21,6 +21,7 @@ "@sveltejs/package": "^2.3.11", "@sveltejs/vite-plugin-svelte": "^5.0.3", "@types/d3-array": "^3.2.1", + "@types/d3-time": "^3.0.4", "@types/lodash-es": "^4.17.12", "@vitest/coverage-v8": "^3.1.2", "prettier": "^3.5.3", @@ -36,7 +37,7 @@ "type": "module", "dependencies": { "d3-array": "^3.2.4", - "date-fns": "^4.1.0", + "d3-time": "^3.1.0", "lodash-es": "^4.17.21" }, "main": "./dist/index.js", diff --git a/packages/utils/src/lib/date.test.ts b/packages/utils/src/lib/date.test.ts index eb6a938..acd8a74 100644 --- a/packages/utils/src/lib/date.test.ts +++ b/packages/utils/src/lib/date.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, test } from 'vitest'; import { formatDate, getMonthDaysByWeek, @@ -12,6 +12,15 @@ import { hasDayOfWeek, replaceDayOfWeek, isStringDate, + timeInterval, + startOfInterval, + endOfInterval, + parseDate, + intervalOffset, + isSameInterval, + intervalDifference, + isLeapYear, + isDateWithin, } from './date.js'; import { createLocaleSettings, defaultLocale, LocaleSettings } from './locale.js'; import { @@ -21,12 +30,23 @@ import { type CustomIntlDateTimeFormatOptions, DateToken, PeriodTypeCode, + TimeIntervalType, } from './date_types.js'; import { getWeekStartsOnFromIntl } from './dateInternal.js'; -import { parseISO } from 'date-fns'; +import { + timeDay, + timeHour, + timeMillisecond, + timeMinute, + timeMonth, + timeYear, + timeSecond, + timeWeek, +} from 'd3-time'; export const testDateStr = '2023-11-21'; // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) -export const testDate = parseISO('2023-11-21'); // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) +export const testDate = parseDate('2023-11-21'); // "good" default date as the day (21) is bigger than 12 (number of months). And november is a good month1 (because why not?) + const dt_2M_2d = new Date(2023, 10, 21); const dt_2M_1d = new Date(2023, 10, 7); const dt_1M_1d = new Date(2023, 2, 7); @@ -1236,3 +1256,192 @@ describe('isStringDate()', () => { expect(isStringDate('1982-03-30T11:25:59.1234567Z')).true; }); }); + +describe('parseDate()', () => { + it('date only as locale date', () => { + expect(parseDate('1982-03-30')).toEqual(new Date('1982-03-30T04:00:00.000Z')); + }); + + it('date and time only (hour/minute) as locale date', () => { + expect(parseDate('1982-03-30T04:00')).toEqual(new Date('1982-03-30T08:00:00.000Z')); + }); + + it('date and time only (hour/minute/second) as locale date', () => { + expect(parseDate('1982-03-30T04:00:00')).toEqual(new Date('1982-03-30T08:00:00.000Z')); + }); + + it('should not equal UTC date', () => { + // Just an extra check + expect(parseDate('1982-03-30')).not.toEqual(new Date('1982-03-30')); + }); + + it('date with timezome (UTC)', () => { + expect(parseDate('1982-03-30T11:25:59Z')).toEqual(new Date('1982-03-30T11:25:59Z')); + }); + + it('date with time (offset)', () => { + expect(parseDate('1982-03-30T00:00:00-01:00')).toEqual(new Date('1982-03-30T00:00:00-01:00')); + }); + + it('date with time and 3 digit milliseconds (UTC)', () => { + expect(parseDate('1982-03-30T11:25:59.123Z')).toEqual(new Date('1982-03-30T11:25:59.123Z')); + }); + + it('date with time with 7 digit milliseconds (UTC)', () => { + expect(parseDate('1982-03-30T11:25:59.1234567Z')).toEqual( + new Date('1982-03-30T11:25:59.1234567Z') + ); + }); + + it('invalid date string', () => { + expect(parseDate('some_string')).toEqual(new Date('Invalid Date')); + }); +}); + +describe('timeInterval()', () => { + test.each([ + ['millisecond', timeMillisecond], + ['second', timeSecond], + ['minute', timeMinute], + ['hour', timeHour], + ['day', timeDay], + ['week', timeWeek], + ['month', timeMonth], + // ['quarter', timeMonth.every(3)], // TODO: how to verify this? + ['year', timeYear], + ])('timeInterval(%s) => %s', (interval, expected) => { + expect(timeInterval(interval as TimeIntervalType)).toEqual(expected); + }); +}); + +describe('startOfInterval()', () => { + test.each([ + ['millisecond', '2023-03-07T14:02:03.004'], + ['second', '2023-03-07T14:02:03'], + ['minute', '2023-03-07T14:02'], + ['hour', '2023-03-07T14:00:00.000'], + ['day', '2023-03-07'], + ['week', '2023-03-05'], + ['month', '2023-03-01'], + ['quarter', '2023-01-01'], + ['year', '2023-01-01'], + ])('startOfInterval(%s, %s) => %s', (interval, expected) => { + expect(startOfInterval(interval as TimeIntervalType, dt_1M_1d_time_pm)).toEqual( + parseDate(expected) + ); + }); +}); + +describe('endOfInterval()', () => { + test.each([ + ['millisecond', '2023-03-07T14:02:03.004'], + ['second', '2023-03-07T14:02:03.999'], + ['minute', '2023-03-07T14:02:59.999'], + ['hour', '2023-03-07T14:59:59.999'], + ['day', '2023-03-07T23:59:59.999'], + ['week', '2023-03-11T23:59:59.999'], + ['month', '2023-03-31T23:59:59.999'], + ['quarter', '2023-03-31T23:59:59.999'], + ['year', '2023-12-31T23:59:59.999'], + ])('endOfInterval(%s, %s) => %s', (interval, expected) => { + expect(endOfInterval(interval as TimeIntervalType, dt_1M_1d_time_pm)).toEqual( + parseDate(expected) + ); + }); +}); + +describe('intervalOffset()', () => { + test.each([ + ['millisecond', 1, '2023-11-21T04:00:00.001Z'], + ['millisecond', -1, '2023-11-21T03:59:59.999Z'], + ['second', 1, '2023-11-21T04:00:01Z'], + ['second', -1, '2023-11-21T03:59:59Z'], + ['minute', 1, '2023-11-21T04:01:00Z'], + ['minute', -1, '2023-11-21T03:59:00Z'], + ['hour', 1, '2023-11-21T05:00:00Z'], + ['hour', -1, '2023-11-21T03:00:00Z'], + ['day', 1, '2023-11-22'], + ['day', -1, '2023-11-20'], + ['week', 1, '2023-11-28'], + ['week', -1, '2023-11-14'], + ['month', 1, '2023-12-21'], + ['month', -1, '2023-10-21'], + ['quarter', 1, '2024-02-01'], + ['quarter', -1, '2023-08-01'], + ['year', 1, '2024-11-21'], + ['year', -1, '2022-11-21'], + ])('intervalOffset(%s, %s, %s) => %s', (interval, offset, expected) => { + expect(intervalOffset(interval as TimeIntervalType, testDate, offset)).toEqual( + parseDate(expected) + ); + }); +}); + +describe('isSameInterval()', () => { + test.each([ + ['day', '2023-03-07T00:00', '2023-03-07T23:59', true], + ['day', '2023-03-07T11:00', '2023-03-08T12:00', false], + ['week', '2023-03-07', '2023-03-08', true], + ['week', '2023-03-07', '2023-03-14', false], + ['month', '2023-03-07', '2023-03-30', true], + ['month', '2023-03-07', '2023-04-07', false], + ['quarter', '2023-03-07', '2023-01-07', true], + ['quarter', '2023-03-07', '2023-04-07', false], + ['year', '2023-03-07', '2023-11-21', true], + ['year', '2023-03-07', '2024-03-07', false], + ])('isSameInterval(%s, %s, %s) => %s', (interval, date1, date2, expected) => { + expect(isSameInterval(interval as TimeIntervalType, parseDate(date1), parseDate(date2))).toBe( + expected + ); + }); +}); + +describe('intervalDifference()', () => { + test.each([ + ['day', '2023-03-07T00:00', '2023-03-07T23:59', 0], // Same day + ['day', '2023-03-07', '2023-03-08', 1], // Next day + ['day', '2023-01-01', '2023-12-31', 364], // Full year + ['day', '2023-01-01', '2024-01-01', 365], // Next year + ['week', '2023-03-05', '2023-03-11', 0], // Same week + ['week', '2023-03-07', '2023-03-14', 1], // Next week + ['month', '2023-01-01', '2023-01-31', 0], // Same month + ['month', '2023-01-01', '2023-02-01', 1], // Next month + ['quarter', '2023-01-01', '2023-03-31', 0], // Same quarter + ['quarter', '2023-01-01', '2023-04-01', 1], // Next quarter + ['year', '2023-01-01', '2023-12-31', 0], // Same year + ['year', '2023-01-01', '2024-01-01', 1], // Next year + ])('intervalDifference(%s, %s, %s) => %s', (interval, date1, date2, expected) => { + expect( + intervalDifference(interval as TimeIntervalType, parseDate(date1), parseDate(date2)) + ).toEqual(expected); + }); +}); + +describe('isLeapYear()', () => { + test.each([ + ['2020-01-01', true], + ['2021-01-01', false], + ['2022-01-01', false], + ['2023-01-01', false], + ['2024-01-01', true], + ])('isLeapYear(%s) => %s', (date, expected) => { + expect(isLeapYear(parseDate(date))).toBe(expected); + }); +}); + +describe('isDateWithin()', () => { + test.each([ + ['2023-03-07', '2023-03-06', '2023-03-08', true], // between + ['2023-03-07', '2023-03-07', '2023-03-08', true], // match start + ['2023-03-07', '2023-03-06', '2023-03-07', true], // match end + ['2023-03-07', '2023-03-08', '2023-03-09', false], // outside start + ['2023-03-07', '2023-03-05', '2023-03-06', false], // outside end + ])('isDateWithin(%s, %s) => %s', (date, start, end, expected) => { + expect( + isDateWithin(parseDate(date), { + start: parseDate(start), + end: parseDate(end), + }) + ).toBe(expected); + }); +}); diff --git a/packages/utils/src/lib/date.ts b/packages/utils/src/lib/date.ts index 40586e4..62fd28d 100644 --- a/packages/utils/src/lib/date.ts +++ b/packages/utils/src/lib/date.ts @@ -1,34 +1,23 @@ import { - startOfDay, - endOfDay, - startOfWeek, - endOfWeek, - startOfMonth, - endOfMonth, - startOfQuarter, - endOfQuarter, - startOfYear, - endOfYear, - min, - max, - addMonths, - addDays, - differenceInDays, - differenceInWeeks, - differenceInMonths, - differenceInQuarters, - differenceInYears, - addWeeks, - addQuarters, - addYears, - isSameDay, - isSameWeek, - isSameMonth, - isSameQuarter, - isSameYear, - parseISO, - formatISO, -} from 'date-fns'; + CountableTimeInterval, + timeDay, + timeHour, + timeMillisecond, + timeMinute, + timeMonth, + timeSecond, + timeWeek, + timeYear, + timeInterval as d3TimeInterval, + type TimeInterval, + timeMonday, + timeTuesday, + timeWednesday, + timeThursday, + timeFriday, + timeSaturday, +} from 'd3-time'; +import { min, max } from 'd3-array'; import { hasKeyOf } from './typeGuards.js'; import { assertNever, entries } from './typeHelpers.js'; @@ -43,6 +32,7 @@ import { type DateFormatVariantPreset, periodTypeMappings, PeriodTypeCode, + type TimeIntervalType, } from './date_types.js'; import { defaultLocale, type LocaleSettings } from './locale.js'; @@ -204,15 +194,15 @@ export function getMonthDaysByWeek( dateInTheMonth: Date, weekStartsOn: DayOfWeek = DayOfWeek.Sunday ): Date[][] { - const startOfFirstWeek = startOfWeek(startOfMonth(dateInTheMonth), { weekStartsOn }); - const endOfLastWeek = endOfWeek(endOfMonth(dateInTheMonth), { weekStartsOn }); + const startOfFirstWeek = startOfWeek(startOfInterval('month', dateInTheMonth), weekStartsOn); + const endOfLastWeek = endOfWeek(endOfInterval('month', dateInTheMonth), weekStartsOn); const list = []; let valueToAdd = startOfFirstWeek; while (valueToAdd <= endOfLastWeek) { list.push(valueToAdd); - valueToAdd = addDays(valueToAdd, 1); + valueToAdd = intervalOffset('day', valueToAdd, 1); } return chunk(list, 7) as Date[][]; @@ -265,7 +255,7 @@ export function getFiscalYearRange( const numberOfMonths = (options && options.numberOfMonths) || 12; const startDate = new Date((fiscalYear || 0) - 1, startMonth - 1, 1); - const endDate = endOfMonth(addMonths(startDate, numberOfMonths - 1)); + const endDate = endOfInterval('month', intervalOffset('month', startDate, numberOfMonths - 1)); return { startDate, endDate }; } @@ -290,13 +280,51 @@ const biweekBaseDates = [new Date('1799-12-22T00:00'), new Date('1799-12-15T00:0 export function startOfBiWeek(date: Date, week: number, startOfWeek: DayOfWeek) { var weekBaseDate = biweekBaseDates[week - 1]; - var baseDate = addDays(weekBaseDate, startOfWeek); - var periodsSince = Math.floor(differenceInDays(date, baseDate) / 14); - return addDays(baseDate, periodsSince * 14); + var baseDate = intervalOffset('day', weekBaseDate, startOfWeek); + var periodsSince = Math.floor(intervalDifference('day', baseDate, date) / 14); + return intervalOffset('day', baseDate, periodsSince * 14); } export function endOfBiWeek(date: Date, week: number, startOfWeek: DayOfWeek) { - return addDays(startOfBiWeek(date, week, startOfWeek), 13); + return intervalOffset('day', startOfBiWeek(date, week, startOfWeek), 13); +} + +function startOfWeek(date: Date, weekStartsOn: DayOfWeek) { + switch (weekStartsOn) { + case DayOfWeek.Sunday: + return startOfInterval(timeWeek, date); + case DayOfWeek.Monday: + return startOfInterval(timeMonday, date); + case DayOfWeek.Tuesday: + return startOfInterval(timeTuesday, date); + case DayOfWeek.Wednesday: + return startOfInterval(timeWednesday, date); + case DayOfWeek.Thursday: + return startOfInterval(timeThursday, date); + case DayOfWeek.Friday: + return startOfInterval(timeFriday, date); + case DayOfWeek.Saturday: + return startOfInterval(timeSaturday, date); + } +} + +function endOfWeek(date: Date, weekStartsOn: DayOfWeek) { + switch (weekStartsOn) { + case DayOfWeek.Sunday: + return endOfInterval(timeWeek, date); + case DayOfWeek.Monday: + return endOfInterval(timeMonday, date); + case DayOfWeek.Tuesday: + return endOfInterval(timeTuesday, date); + case DayOfWeek.Wednesday: + return endOfInterval(timeWednesday, date); + case DayOfWeek.Thursday: + return endOfInterval(timeThursday, date); + case DayOfWeek.Friday: + return endOfInterval(timeFriday, date); + case DayOfWeek.Saturday: + return endOfInterval(timeSaturday, date); + } } export function getDateFuncsByPeriodType( @@ -310,107 +338,101 @@ export function getDateFuncsByPeriodType( switch (periodType) { case PeriodType.Day: return { - start: startOfDay, - end: endOfDay, - add: addDays, - difference: differenceInDays, - isSame: isSameDay, + start: startOfInterval('day'), + end: endOfInterval('day'), + add: (date: Date, amount: number) => intervalOffset('day', date, amount), + difference: intervalDifference('day'), + isSame: isSameInterval('day'), }; case PeriodType.Week: case PeriodType.WeekSun: return { - start: startOfWeek, - end: endOfWeek, - add: addWeeks, - difference: differenceInWeeks, - isSame: isSameWeek, + start: startOfInterval(timeWeek), + end: endOfInterval(timeWeek), + add: (date: Date, amount: number) => intervalOffset('week', date, amount), + difference: intervalDifference(timeWeek), + isSame: isSameInterval(timeWeek), }; case PeriodType.WeekMon: return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 1 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 1 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 1 }), + start: startOfInterval(timeMonday), + end: endOfInterval(timeMonday), + add: (date: Date, amount: number) => intervalOffset('week', date, amount), + difference: intervalDifference(timeMonday), + isSame: isSameInterval(timeMonday), }; case PeriodType.WeekTue: return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 2 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 2 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 2 }), + start: startOfInterval(timeTuesday), + end: endOfInterval(timeTuesday), + add: (date: Date, amount: number) => intervalOffset('week', date, amount), + difference: intervalDifference(timeTuesday), + isSame: isSameInterval(timeTuesday), }; case PeriodType.WeekWed: return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 3 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 3 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 3 }), + start: startOfInterval(timeWednesday), + end: endOfInterval(timeWednesday), + add: (date: Date, amount: number) => intervalOffset('week', date, amount), + difference: intervalDifference(timeWednesday), + isSame: isSameInterval(timeWednesday), }; case PeriodType.WeekThu: return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 4 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 4 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 4 }), + start: startOfInterval(timeThursday), + end: endOfInterval(timeThursday), + add: (date: Date, amount: number) => intervalOffset('week', date, amount), + difference: intervalDifference(timeThursday), + isSame: isSameInterval(timeThursday), }; case PeriodType.WeekFri: return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 5 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 5 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 5 }), + start: startOfInterval(timeFriday), + end: endOfInterval(timeFriday), + add: (date: Date, amount: number) => intervalOffset('week', date, amount), + difference: intervalDifference(timeFriday), + isSame: isSameInterval(timeFriday), }; case PeriodType.WeekSat: return { - start: (date: Date) => startOfWeek(date, { weekStartsOn: 6 }), - end: (date: Date) => endOfWeek(date, { weekStartsOn: 6 }), - add: addWeeks, - difference: differenceInWeeks, - isSame: (dateLeft: Date, dateRight: Date) => - isSameWeek(dateLeft, dateRight, { weekStartsOn: 6 }), + start: startOfInterval(timeSaturday), + end: endOfInterval(timeSaturday), + add: (date: Date, amount: number) => intervalOffset('week', date, amount), + difference: intervalDifference(timeSaturday), + isSame: isSameInterval(timeSaturday), }; case PeriodType.Month: return { - start: startOfMonth, - end: endOfMonth, - add: addMonths, - difference: differenceInMonths, - isSame: isSameMonth, + start: startOfInterval('month'), + end: endOfInterval('month'), + add: (date: Date, amount: number) => intervalOffset('month', date, amount), + difference: intervalDifference('month'), + isSame: isSameInterval('month'), }; case PeriodType.Quarter: return { - start: startOfQuarter, - end: endOfQuarter, - add: addQuarters, - difference: differenceInQuarters, - isSame: isSameQuarter, + start: startOfInterval('quarter'), + end: endOfInterval('quarter'), + add: (date: Date, amount: number) => intervalOffset('quarter', date, amount), + difference: intervalDifference('quarter'), + isSame: isSameInterval('quarter'), }; case PeriodType.CalendarYear: return { - start: startOfYear, - end: endOfYear, - add: addYears, - difference: differenceInYears, - isSame: isSameYear, + start: startOfInterval('year'), + end: endOfInterval('year'), + add: (date: Date, amount: number) => intervalOffset('year', date, amount), + difference: intervalDifference('year'), + isSame: isSameInterval('year'), }; case PeriodType.FiscalYearOctober: return { start: startOfFiscalYear, end: endOfFiscalYear, - add: addYears, - difference: differenceInYears, + add: (date: Date, amount: number) => intervalOffset('year', date, amount), + difference: intervalDifference('year'), isSame: isSameFiscalYear, }; @@ -437,12 +459,14 @@ export function getDateFuncsByPeriodType( return { start: (date: Date) => startOfBiWeek(date, week, dayOfWeek), end: (date: Date) => endOfBiWeek(date, week, dayOfWeek), - add: (date: Date, amount: number) => addWeeks(date, amount * 2), + add: (date: Date, amount: number) => intervalOffset('week', date, amount * 2), difference: (dateLeft: Date, dateRight: Date) => { - return differenceInWeeks(dateLeft, dateRight) / 2; + // TODO: Use interval based on start of bi-week (sunday, monday, etc) + return intervalDifference('week', dateLeft, dateRight) / 2; }, isSame: (dateLeft: Date, dateRight: Date) => { - return isSameDay( + return isSameInterval( + 'day', startOfBiWeek(dateLeft, week, dayOfWeek), startOfBiWeek(dateRight, week, dayOfWeek) ); @@ -460,11 +484,11 @@ export function getDateFuncsByPeriodType( case undefined: // Default to end of day if periodType == null, etc return { - start: startOfDay, - end: endOfDay, - add: addDays, - difference: differenceInDays, - isSame: isSameDay, + start: startOfInterval('day'), + end: endOfInterval('day'), + add: (date: Date, amount: number) => intervalOffset('day', date, amount), + difference: intervalDifference('day'), + isSame: isSameInterval('day'), }; default: @@ -472,21 +496,6 @@ export function getDateFuncsByPeriodType( } } -export function formatISODate( - date: Date | string | null | undefined, - representation: 'complete' | 'date' | 'time' = 'complete' -) { - if (date == null) { - return ''; - } - - if (typeof date === 'string') { - date = parseISO(date); - } - - return formatISO(date, { representation }); -} - export function formatIntl( settings: LocaleSettings, dt: Date, @@ -600,12 +609,10 @@ function range( ) { const start = biWeek === undefined - ? startOfWeek(date, { weekStartsOn }) + ? startOfWeek(date, weekStartsOn) : startOfBiWeek(date, biWeek, weekStartsOn); const end = - biWeek === undefined - ? endOfWeek(date, { weekStartsOn }) - : endOfBiWeek(date, biWeek, weekStartsOn); + biWeek === undefined ? endOfWeek(date, weekStartsOn) : endOfBiWeek(date, biWeek, weekStartsOn); return formatIntl(settings, start, formatToUse) + ' - ' + formatIntl(settings, end, formatToUse); } @@ -664,7 +671,7 @@ export function formatDateWithLocale( options: FormatDateOptions = {} ): string { if (typeof date === 'string') { - date = parseISO(date); + date = parseDate(date); } // Handle 'Invalid Date' @@ -732,8 +739,8 @@ export function formatDateWithLocale( case PeriodType.Quarter: return [ - formatIntl(settings, startOfQuarter(date), rv(month!)!), - formatIntl(settings, endOfQuarter(date), rv(monthsYear!)!), + formatIntl(settings, startOfInterval('quarter', date), rv(month!)!), + formatIntl(settings, endOfInterval('quarter', date), rv(monthsYear!)!), ].join(' - '); case PeriodType.CalendarYear: @@ -776,7 +783,7 @@ export function formatDateWithLocale( return range(settings, date, 6, rv(week!)!, 2); default: - return formatISO(date); + return date.toISOString(); // default: // assertNever(periodType); // This will now report unhandled cases } @@ -833,15 +840,222 @@ export function randomDate(from: Date, to: Date) { } // '1982-03-30' +// '1982-03-30T04:00' +// '1982-03-30T04:00:00' // '1982-03-30T11:25:59Z' // '1982-03-30T11:25:59-04:00' // '1982-03-30T11:25:59.123Z' // '1982-03-30T11:25:59.1234567Z' -const DATE_FORMAT = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(.\d+|)(Z|(-|\+)\d{2}:\d{2}))?$/; +const DATE_FORMAT = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}(:\d{2}(\.\d+|)?(Z|(-|\+)\d{2}:\d{2}?)?)?)?$/; /** - * Determine if string is UTC (yyyy-mm-ddThh:mm:ssZ) or Offset (yyyy-mm-ddThh:mm:ss-ZZ:ZZ) or Date-only (yyyy-mm-dd) date string + * Determine if string is valid date string + * - Date-only (yyyy-mm-dd) + * - Date with time (yyyy-mm-ddThh:mm:ss) + * - Date with time and timezone (yyyy-mm-ddThh:mm:ssZ) + * - Date with time and offset (yyyy-mm-ddThh:mm:ss-ZZ:ZZ) + * - Date with time and 3 digit milliseconds (yyyy-mm-ddThh:mm:ss.sss) with or without timezone / offset + * - Date with time and 7 digit milliseconds (yyyy-mm-ddThh:mm:ss.sssssss) with or without timezone / offset */ export function isStringDate(value: string) { return DATE_FORMAT.test(value); } + +/** + * Determine if string is a date string with time (yyyy-mm-ddThh:mm:ss) + */ +export function isStringDateWithTime(value: string) { + return isStringDate(value) && value.includes('T'); +} + +/** + * Determine if string is a date string with time and timezone (yyyy-mm-ddThh:mm:ssZ) or Offset (yyyy-mm-ddThh:mm:ss-ZZ:ZZ) + */ +export function isStringDateWithTimezone(value: string) { + return isStringDateWithTime(value) && /Z$|[+-]\d{2}:\d{2}$/.test(value); +} + +/** Parse a date string as a local Date if no timezone is specified */ +export function parseDate(datestr: string) { + if (!isStringDate(datestr)) return new Date('Invalid Date'); + + if (isStringDateWithTime(datestr)) { + // Respect timezone. Also parses unqualified strings like '1982-03-30T04:00' as local date + return new Date(datestr); + } + + const [date, time] = datestr.split('T'); + const [year, month, day] = date.split('-').map(Number); + + if (time) { + const [hour, minute, second] = time.split(':').map(Number); + return new Date(year, month - 1, day, hour, minute, second); + } else { + return new Date(year, month - 1, day); + } +} + +/** Custom time interval for quarters */ +export const timeQuarter = d3TimeInterval( + // floor + (date) => { + date.setMonth(date.getMonth() - (date.getMonth() % 3), 1); + date.setHours(0, 0, 0, 0); + }, + // offset + (date, step) => date.setMonth(date.getMonth() + step * 3, 1), + // count + (start, end) => (end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24 * 30 * 3), + // field + (date) => date.getMonth() // TODO: what should this be? +); + +/** Get a time interval function by name */ +export function timeInterval(name: TimeIntervalType) { + switch (name) { + case 'millisecond': + return timeMillisecond; + case 'second': + return timeSecond; + case 'minute': + return timeMinute; + case 'hour': + return timeHour; + case 'day': + return timeDay; + case 'week': + return timeWeek; + case 'month': + return timeMonth; + case 'quarter': + return timeQuarter; + case 'year': + return timeYear; + } +} + +/** + * Get the date at the start of the interval + * @param interval The time interval to use + * @param date Optional date to get start of interval for. If not provided, returns a function that takes a date. + * @returns Either a Date or a function that takes a date and returns a Date + */ +export function startOfInterval(interval: TimeInterval | TimeIntervalType, date: Date): Date; +export function startOfInterval(interval: TimeInterval | TimeIntervalType): (date: Date) => Date; +export function startOfInterval( + interval: TimeInterval | TimeIntervalType, + date?: Date +): Date | ((date: Date) => Date) { + interval = typeof interval === 'string' ? timeInterval(interval) : interval; + + if (date === undefined) { + return (date: Date) => new Date(interval.floor(date)); + } + + return new Date(interval.floor(date)); +} + +/** + * Get the date at the end of the interval + * Similar to `interval.ceil(date)` except: + * - returns end of day instead of start of next day + * - properly handles start of day (i.e. not return same date) + * @param interval The time interval to use + * @param date Optional date to get end of interval for. If not provided, returns a function that takes a date. + * @returns Either a Date or a function that takes a date and returns a Date + */ +export function endOfInterval(interval: TimeInterval | TimeIntervalType, date: Date): Date; +export function endOfInterval(interval: TimeInterval | TimeIntervalType): (date: Date) => Date; +export function endOfInterval( + interval: TimeInterval | TimeIntervalType, + date?: Date +): Date | ((date: Date) => Date) { + interval = typeof interval === 'string' ? timeInterval(interval) : interval; + + if (date === undefined) { + return (date: Date) => new Date(interval.offset(interval.floor(date), 1).getTime() - 1); + } + + // Can not use `new Date(+interval.ceil(date) - 1)`; as `.ceil()` will return same date when start of the day (matching `.floor()`) + return new Date(interval.offset(interval.floor(date), 1).getTime() - 1); +} + +/** Add or subtract an interval from a date */ +export function intervalOffset( + interval: TimeInterval | TimeIntervalType, + date: Date, + offset: number +) { + interval = typeof interval === 'string' ? timeInterval(interval) : interval; + return interval.offset(date, offset); +} + +/** Check if two dates are in the same interval (such as same day or month) */ +export function isSameInterval( + interval: TimeInterval | TimeIntervalType, + date1: Date, + date2: Date +): boolean; +export function isSameInterval( + interval: TimeInterval | TimeIntervalType +): (date1: Date, date2: Date) => boolean; +export function isSameInterval( + interval: TimeInterval | TimeIntervalType, + date1?: Date, + date2?: Date +) { + interval = typeof interval === 'string' ? timeInterval(interval) : interval; + + if (date1 === undefined || date2 === undefined) { + return (date1: Date, date2: Date) => + interval.floor(date1).getTime() === interval.floor(date2).getTime(); + } + + return interval.floor(date1).getTime() === interval.floor(date2).getTime(); +} + +/** Get the number of intervals between two dates (based on boundaries crossed) */ +export function intervalDifference( + interval: CountableTimeInterval | TimeIntervalType, + date1: Date, + date2: Date +): number; +export function intervalDifference( + interval: CountableTimeInterval | TimeIntervalType +): (date1: Date, date2: Date) => number; +export function intervalDifference( + interval: CountableTimeInterval | TimeIntervalType, + date1?: Date, + date2?: Date +) { + interval = typeof interval === 'string' ? timeInterval(interval) : interval; + + if (date1 === undefined || date2 === undefined) { + return (date1: Date, date2: Date) => interval.count(date1, date2); + } + + return interval.count(date1, date2); +} + +/** Check if date is a leap year */ +export function isLeapYear(date: Date) { + return ( + date.getFullYear() % 400 === 0 || + (date.getFullYear() % 4 === 0 && date.getFullYear() % 100 !== 0) + ); +} + +/** Check if first date is before second date */ +export function isDateBefore(date1: Date, date2: Date) { + return date1.getTime() < date2.getTime(); +} + +/** Check if first date is after second date */ +export function isDateAfter(date1: Date, date2: Date) { + return date1.getTime() > date2.getTime(); +} + +/** Check if date is within interval */ +export function isDateWithin(date: Date, range: { start: Date; end: Date }) { + return date.getTime() >= range.start.getTime() && date.getTime() <= range.end.getTime(); +} diff --git a/packages/utils/src/lib/dateRange.ts b/packages/utils/src/lib/dateRange.ts index 8ef3fb6..ba89262 100644 --- a/packages/utils/src/lib/dateRange.ts +++ b/packages/utils/src/lib/dateRange.ts @@ -1,6 +1,12 @@ -import { startOfDay, isLeapYear, isAfter, isBefore, subYears } from 'date-fns'; - -import { getDateFuncsByPeriodType, updatePeriodTypeWithWeekStartsOn } from './date.js'; +import { + getDateFuncsByPeriodType, + intervalOffset, + isLeapYear, + isDateAfter, + isDateBefore, + startOfInterval, + updatePeriodTypeWithWeekStartsOn, +} from './date.js'; import { PeriodType } from './date_types.js'; import type { LocaleSettings } from './locale.js'; @@ -34,7 +40,7 @@ export function getDateRangePresets( periodType: PeriodType ): { label: string; value: DateRange }[] { let now = new Date(); - const today = startOfDay(now); + const today = startOfInterval('day', now); if (settings) { periodType = @@ -201,12 +207,12 @@ export function getPreviousYearPeriodOffset( // if year before reference date is a leap year and is before 2/29 const adjustForLeapYear = options?.referenceDate ? (isLeapYear(options?.referenceDate) && - isAfter( + isDateAfter( options?.referenceDate, new Date(options?.referenceDate.getFullYear(), /*Feb*/ 1, 28) )) || - (isLeapYear(subYears(options?.referenceDate, 1)) && - isBefore( + (isLeapYear(intervalOffset('year', options?.referenceDate, -1)) && + isDateBefore( options?.referenceDate, new Date(options?.referenceDate.getFullYear(), /*Feb*/ 1, 29) )) @@ -269,7 +275,8 @@ export function getPeriodComparisonOffset( switch (view) { case 'prevPeriod': const dateFuncs = getDateFuncsByPeriodType(settings, period.periodType); - return dateFuncs.difference(period.from, period.to) - 1; // Difference counts full days, need additoinal offset + // return dateFuncs.difference(period.from, period.to) - 1; // Difference counts full days, need additional offset + return dateFuncs.difference(period.to, period.from); // Difference counts full days, need additional offset case 'prevYear': return getPreviousYearPeriodOffset(period.periodType, { diff --git a/packages/utils/src/lib/date_types.ts b/packages/utils/src/lib/date_types.ts index 6dd1a15..6d29b04 100644 --- a/packages/utils/src/lib/date_types.ts +++ b/packages/utils/src/lib/date_types.ts @@ -94,6 +94,17 @@ export const periodTypeMappings = { export type PeriodTypeCode = ValueOf; +export type TimeIntervalType = + | 'millisecond' + | 'second' + | 'minute' + | 'hour' + | 'day' + | 'week' + | 'month' + | 'quarter' + | 'year'; + export enum DayOfWeek { Sunday = 0, Monday = 1, diff --git a/packages/utils/src/lib/duration.test.ts b/packages/utils/src/lib/duration.test.ts index 529218d..64e9566 100644 --- a/packages/utils/src/lib/duration.test.ts +++ b/packages/utils/src/lib/duration.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { Duration, DurationUnits } from './duration.js'; -import { subDays } from 'date-fns'; +import { intervalOffset } from './date.js'; describe('Duration', () => { it('default', () => { @@ -35,7 +35,7 @@ describe('Duration', () => { }); it('start-only should use `now` for end', () => { - const start = subDays(new Date(), 10); + const start = intervalOffset('day', new Date(), -10); const actual = new Duration({ start }); expect(actual.years).equal(0); expect(actual.days).equal(10); diff --git a/packages/utils/src/lib/duration.ts b/packages/utils/src/lib/duration.ts index 0fcc8a2..f1fe25d 100644 --- a/packages/utils/src/lib/duration.ts +++ b/packages/utils/src/lib/duration.ts @@ -1,4 +1,4 @@ -import { parseISO } from 'date-fns'; +import { parseDate } from './date.js'; export type DurationOption = { milliseconds?: number; @@ -33,8 +33,8 @@ export class Duration { duration?: DurationOption; } = {} ) { - const startDate = typeof options.start === 'string' ? parseISO(options.start) : options.start; - const endDate = typeof options.end === 'string' ? parseISO(options.end) : options.end; + const startDate = typeof options.start === 'string' ? parseDate(options.start) : options.start; + const endDate = typeof options.end === 'string' ? parseDate(options.end) : options.end; const differenceInMs = startDate ? Math.abs(Number(endDate || new Date()) - Number(startDate)) diff --git a/packages/utils/src/lib/format.test.ts b/packages/utils/src/lib/format.test.ts index a38eb60..54efea4 100644 --- a/packages/utils/src/lib/format.test.ts +++ b/packages/utils/src/lib/format.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { format } from './format.js'; +import { parseDate } from './date.js'; import { PeriodType } from './date_types.js'; import { testDate, testDateStr } from './date.test.js'; -import { parseISO } from 'date-fns'; describe('format()', () => { it('returns empty string for null', () => { @@ -120,7 +120,7 @@ describe('format()', () => { expect(actual).equal('11/21/2023'); }); it('format based on value type (date)', () => { - const actual = format(parseISO(testDateStr)); + const actual = format(parseDate(testDateStr)); expect(actual).equal('11/21/2023'); }); it('format based on value type (string)', () => { diff --git a/packages/utils/src/lib/index.ts b/packages/utils/src/lib/index.ts index 88e07c8..e0bcd80 100644 --- a/packages/utils/src/lib/index.ts +++ b/packages/utils/src/lib/index.ts @@ -1,6 +1,13 @@ // top-level exports export { flatten, unique, greatestAbs } from './array.js'; -export { formatDate, getDateFuncsByPeriodType } from './date.js'; +export { + formatDate, + parseDate, + timeInterval, + startOfInterval, + endOfInterval, + getDateFuncsByPeriodType, +} from './date.js'; export { PeriodType, DayOfWeek, DateToken } from './date_types.js'; export * from './date_types.js'; export * from './dom.js'; diff --git a/packages/utils/src/lib/json.ts b/packages/utils/src/lib/json.ts index e65abd1..56d446d 100644 --- a/packages/utils/src/lib/json.ts +++ b/packages/utils/src/lib/json.ts @@ -1,5 +1,4 @@ -import { parseISO } from 'date-fns'; -import { isStringDate } from './date.js'; +import { isStringDate, parseDate } from './date.js'; /** * JSON.stringify() with custom handling for `Map` and `Set`. To be used with `parse()` @@ -43,7 +42,7 @@ export function parse(value: string): T { */ export function reviver(key: string, value: any) { if (typeof value === 'string' && isStringDate(value)) { - return parseISO(value); + return parseDate(value); } else if (typeof value === 'object' && value !== null) { if (value._type === 'Map') { return new Map(value.value); diff --git a/packages/utils/src/lib/object.test.ts b/packages/utils/src/lib/object.test.ts index c4e0ac7..e479640 100644 --- a/packages/utils/src/lib/object.test.ts +++ b/packages/utils/src/lib/object.test.ts @@ -1,12 +1,12 @@ import { describe, it, expect } from 'vitest'; -import { addHours, subHours } from 'date-fns'; +import { intervalOffset } from './date.js'; import { expireObject, omit, omitNil, pick } from './object.js'; describe('expireObject', () => { it('simple value not expired', () => { const original = 123; - const expiry = addHours(new Date(), 1); + const expiry = intervalOffset('hour', new Date(), 1); const actual = expireObject(original, expiry); expect(actual).equal(original); @@ -14,7 +14,7 @@ describe('expireObject', () => { it('simple value expired', () => { const original = 123; - const expiry = subHours(new Date(), 1); + const expiry = intervalOffset('hour', new Date(), -1); const actual = expireObject(original, expiry); expect(actual).toBeNull(); @@ -22,7 +22,7 @@ describe('expireObject', () => { it('Date not expired', () => { const original = new Date(); - const expiry = addHours(new Date(), 1); + const expiry = intervalOffset('hour', new Date(), 1); const actual = expireObject(original, expiry); expect(actual).equal(original); @@ -30,7 +30,7 @@ describe('expireObject', () => { it('Date expired', () => { const original = new Date(); - const expiry = subHours(new Date(), 1); + const expiry = intervalOffset('hour', new Date(), -1); const actual = expireObject(original, expiry); expect(actual).toBeNull(); @@ -42,7 +42,7 @@ describe('expireObject', () => { two: 2, three: 3, }; - const expiry = addHours(new Date(), 1); + const expiry = intervalOffset('hour', new Date(), 1); const actual = expireObject(original, expiry); expect(actual).equal(original); @@ -54,7 +54,7 @@ describe('expireObject', () => { two: 2, three: 3, }; - const expiry = subHours(new Date(), 1); + const expiry = intervalOffset('hour', new Date(), -1); const actual = expireObject(original, expiry); expect(actual).toBeNull(); @@ -67,7 +67,7 @@ describe('expireObject', () => { three: 3, }; const expiry = { - two: subHours(new Date(), 1), + two: intervalOffset('hour', new Date(), -1), }; const actual = expireObject(original, expiry); @@ -89,9 +89,9 @@ describe('expireObject', () => { three: 3, }; const expiry = { - one: subHours(new Date(), 3), - two: addHours(new Date(), 1), - $default: subHours(new Date(), 1), + one: intervalOffset('hour', new Date(), -3), + two: intervalOffset('hour', new Date(), 1), + $default: intervalOffset('hour', new Date(), -1), }; const actual = expireObject(original, expiry); @@ -112,9 +112,9 @@ describe('expireObject', () => { three: 3, }; const expiry = { - one: subHours(new Date(), 3), - two: addHours(new Date(), 1), - four: subHours(new Date(), 1), + one: intervalOffset('hour', new Date(), -3), + two: intervalOffset('hour', new Date(), 1), + four: intervalOffset('hour', new Date(), -1), }; const actual = expireObject(original, expiry); @@ -139,8 +139,8 @@ describe('expireObject', () => { three: 3, }; const expiry = { - one: subHours(new Date(), 3), - two: addHours(new Date(), 1), + one: intervalOffset('hour', new Date(), -3), + two: intervalOffset('hour', new Date(), 1), }; const actual = expireObject(original, expiry); @@ -166,9 +166,9 @@ describe('expireObject', () => { }; const expiry = { one: { - foo: subHours(new Date(), 3), + foo: intervalOffset('hour', new Date(), -3), }, - two: addHours(new Date(), 1), + two: intervalOffset('hour', new Date(), 1), }; const actual = expireObject(original, expiry); @@ -188,8 +188,8 @@ describe('expireObject', () => { it('removes $default expiry if expired', () => { const expiry = { - one: addHours(new Date(), 1), - $default: subHours(new Date(), 1), + one: intervalOffset('hour', new Date(), 1), + $default: intervalOffset('hour', new Date(), -1), }; // Test cleaning up expiry diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5e47fd..736273e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,6 @@ importers: d3-scale: specifier: ^4.0.2 version: 4.0.2 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -148,9 +145,6 @@ importers: d3-array: specifier: ^3.2.4 version: 3.2.4 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 immer: specifier: ^10.1.1 version: 10.1.1 @@ -221,9 +215,6 @@ importers: d3-array: specifier: ^3.2.4 version: 3.2.4 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -288,9 +279,6 @@ importers: d3-array: specifier: ^3.2.4 version: 3.2.4 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -346,9 +334,9 @@ importers: d3-array: specifier: ^3.2.4 version: 3.2.4 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 + d3-time: + specifier: ^3.1.0 + version: 3.1.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -362,6 +350,9 @@ importers: '@types/d3-array': specifier: ^3.2.1 version: 3.2.1 + '@types/d3-time': + specifier: ^3.0.4 + version: 3.0.4 '@types/lodash-es': specifier: ^4.17.12 version: 4.17.12 @@ -428,9 +419,6 @@ importers: d3-array: specifier: ^3.2.4 version: 3.2.4 - date-fns: - specifier: ^4.1.0 - version: 4.1.0 lodash-es: specifier: ^4.17.21 version: 4.17.21 diff --git a/sites/docs/package.json b/sites/docs/package.json index b2a1de7..ec18191 100644 --- a/sites/docs/package.json +++ b/sites/docs/package.json @@ -58,7 +58,6 @@ "@mdi/js": "^7.4.47", "clsx": "^2.1.1", "d3-array": "^3.2.4", - "date-fns": "^4.1.0", "lodash-es": "^4.17.21", "prism-svelte": "^0.5.0", "prism-themes": "^1.9.0", diff --git a/sites/docs/src/routes/docs/svelte-stores/localStore/+page.md b/sites/docs/src/routes/docs/svelte-stores/localStore/+page.md index 7f79df9..81973d9 100644 --- a/sites/docs/src/routes/docs/svelte-stores/localStore/+page.md +++ b/sites/docs/src/routes/docs/svelte-stores/localStore/+page.md @@ -11,9 +11,11 @@ ```svelte ``` diff --git a/sites/docs/src/routes/docs/svelte-stores/timerStore/+page.md b/sites/docs/src/routes/docs/svelte-stores/timerStore/+page.md index 1572f1a..772b44c 100644 --- a/sites/docs/src/routes/docs/svelte-stores/timerStore/+page.md +++ b/sites/docs/src/routes/docs/svelte-stores/timerStore/+page.md @@ -1,6 +1,4 @@

Examples

@@ -13,7 +12,9 @@

Date range

-
{JSON.stringify(new Duration({ start: subDays(new Date(), 3) }), null, 2)}
+
+ {JSON.stringify(new Duration({ start: intervalOffset('day', new Date(), 3) }), null, 2)} +

Date range

@@ -27,9 +28,11 @@

Date range

-
{new Duration({ start: subDays(new Date(), 3) }).format()}
-
{new Duration({ start: subMonths(new Date(), 3) }).format()}
-
{new Duration({ start: subMonths(new Date(), 3) }).format({ variant: 'long' })}
+
{new Duration({ start: intervalOffset('day', new Date(), 3) }).format()}
+
{new Duration({ start: intervalOffset('month', new Date(), 3) }).format()}
+
+ {new Duration({ start: intervalOffset('month', new Date(), 3) }).format({ variant: 'long' })} +

string range