svelte-date-utils 0.2.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,66 @@
1
+ import dayjs from 'dayjs';
2
+ import type { Dayjs } from 'dayjs';
3
+ import type { DateAdapter } from './types.js';
4
+ export interface AdapterDayjsOptions {
5
+ /** Locale such as `de` or `en-gb`. Import the matching Day.js locale before using it. */
6
+ locale?: string;
7
+ /** First day of the week, 0 (Sunday) to 6 (Saturday). Defaults to the locale's first day. */
8
+ weekStartsOn?: number;
9
+ }
10
+ /** Adapter for Day.js. Values are immutable Day.js `Dayjs` objects. */
11
+ export declare class AdapterDayjs implements DateAdapter<Dayjs> {
12
+ readonly lib = "dayjs";
13
+ readonly locale: string;
14
+ private readonly weekStartsOn?;
15
+ constructor({ locale, weekStartsOn }?: AdapterDayjsOptions);
16
+ private withLocale;
17
+ private getWeekStart;
18
+ private formatForDayjs;
19
+ date: (value?: string) => dayjs.Dayjs;
20
+ getInvalidDate: () => dayjs.Dayjs;
21
+ isValid: (value: Dayjs | null | undefined) => value is Dayjs;
22
+ toJsDate: (value: Dayjs) => Date;
23
+ formatByString: (value: Dayjs, format: string) => string;
24
+ parse: (value: string, format: string) => dayjs.Dayjs | null;
25
+ isEqual: (value: Dayjs | null, comparing: Dayjs | null) => boolean;
26
+ isSameYear: (value: Dayjs, comparing: Dayjs) => boolean;
27
+ isSameMonth: (value: Dayjs, comparing: Dayjs) => boolean;
28
+ isSameDay: (value: Dayjs, comparing: Dayjs) => boolean;
29
+ isAfter: (value: Dayjs, comparing: Dayjs) => boolean;
30
+ isBefore: (value: Dayjs, comparing: Dayjs) => boolean;
31
+ isWithinRange: (value: Dayjs, [start, end]: [Dayjs, Dayjs]) => boolean;
32
+ startOfYear: (value: Dayjs) => dayjs.Dayjs;
33
+ startOfMonth: (value: Dayjs) => dayjs.Dayjs;
34
+ startOfWeek: (value: Dayjs) => dayjs.Dayjs;
35
+ startOfDay: (value: Dayjs) => dayjs.Dayjs;
36
+ endOfYear: (value: Dayjs) => dayjs.Dayjs;
37
+ endOfMonth: (value: Dayjs) => dayjs.Dayjs;
38
+ endOfWeek: (value: Dayjs) => dayjs.Dayjs;
39
+ endOfDay: (value: Dayjs) => dayjs.Dayjs;
40
+ addYears: (value: Dayjs, amount: number) => dayjs.Dayjs;
41
+ addMonths: (value: Dayjs, amount: number) => dayjs.Dayjs;
42
+ addWeeks: (value: Dayjs, amount: number) => dayjs.Dayjs;
43
+ addDays: (value: Dayjs, amount: number) => dayjs.Dayjs;
44
+ addHours: (value: Dayjs, amount: number) => dayjs.Dayjs;
45
+ addMinutes: (value: Dayjs, amount: number) => dayjs.Dayjs;
46
+ addSeconds: (value: Dayjs, amount: number) => dayjs.Dayjs;
47
+ getYear: (value: Dayjs) => number;
48
+ getMonth: (value: Dayjs) => number;
49
+ getDate: (value: Dayjs) => number;
50
+ getHours: (value: Dayjs) => number;
51
+ getMinutes: (value: Dayjs) => number;
52
+ getSeconds: (value: Dayjs) => number;
53
+ getMilliseconds: (value: Dayjs) => number;
54
+ setYear: (value: Dayjs, year: number) => dayjs.Dayjs;
55
+ setMonth: (value: Dayjs, month: number) => dayjs.Dayjs;
56
+ setDate: (value: Dayjs, date: number) => dayjs.Dayjs;
57
+ setHours: (value: Dayjs, hours: number) => dayjs.Dayjs;
58
+ setMinutes: (value: Dayjs, minutes: number) => dayjs.Dayjs;
59
+ setSeconds: (value: Dayjs, seconds: number) => dayjs.Dayjs;
60
+ setMilliseconds: (value: Dayjs, milliseconds: number) => dayjs.Dayjs;
61
+ getDaysInMonth: (value: Dayjs) => number;
62
+ getWeekArray: (value: Dayjs) => dayjs.Dayjs[][];
63
+ /** Week number where week 1 contains January 1, matching the other adapters. */
64
+ getWeekNumber: (value: Dayjs) => number;
65
+ getDayOfWeek: (value: Dayjs) => number;
66
+ }
@@ -0,0 +1,124 @@
1
+ import dayjs from 'dayjs';
2
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js';
3
+ import localeData from 'dayjs/plugin/localeData.js';
4
+ dayjs.extend(customParseFormat);
5
+ dayjs.extend(localeData);
6
+ const FORMAT_TOKENS = {
7
+ yyyy: 'YYYY',
8
+ yy: 'YY',
9
+ y: 'YYYY',
10
+ MMMM: 'MMMM',
11
+ MMM: 'MMM',
12
+ MM: 'MM',
13
+ M: 'M',
14
+ dd: 'DD',
15
+ d: 'D',
16
+ HH: 'HH',
17
+ H: 'H',
18
+ hh: 'hh',
19
+ h: 'h',
20
+ mm: 'mm',
21
+ m: 'm',
22
+ ss: 'ss',
23
+ s: 's',
24
+ SSS: 'SSS',
25
+ a: 'a',
26
+ };
27
+ const FORMAT_TOKEN_PATTERN = /yyyy|MMMM|MMM|yy|y|MM|M|dd|d|HH|H|hh|h|mm|m|ss|s|SSS|a/g;
28
+ /** Adapter for Day.js. Values are immutable Day.js `Dayjs` objects. */
29
+ export class AdapterDayjs {
30
+ lib = 'dayjs';
31
+ locale;
32
+ weekStartsOn;
33
+ constructor({ locale = 'en', weekStartsOn } = {}) {
34
+ if (weekStartsOn !== undefined &&
35
+ (!Number.isInteger(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6)) {
36
+ throw new RangeError('weekStartsOn must be an integer from 0 to 6.');
37
+ }
38
+ this.locale = locale.toLowerCase();
39
+ this.weekStartsOn = weekStartsOn;
40
+ }
41
+ withLocale = (value) => value.locale(this.locale);
42
+ getWeekStart = (value) => this.weekStartsOn ?? this.withLocale(value).localeData().firstDayOfWeek();
43
+ formatForDayjs = (format) => format.replace(FORMAT_TOKEN_PATTERN, (token) => FORMAT_TOKENS[token] ?? token);
44
+ date = (value) => value === undefined ? this.withLocale(dayjs()) : this.withLocale(dayjs(value));
45
+ getInvalidDate = () => this.withLocale(dayjs(Number.NaN));
46
+ isValid = (value) => value != null && value.isValid();
47
+ toJsDate = (value) => value.toDate();
48
+ formatByString = (value, format) => this.withLocale(value).format(this.formatForDayjs(format));
49
+ parse = (value, format) => value === ''
50
+ ? null
51
+ : this.withLocale(dayjs(value, this.formatForDayjs(format), this.locale, true));
52
+ isEqual = (value, comparing) => value === null || comparing === null
53
+ ? value === comparing
54
+ : value.valueOf() === comparing.valueOf();
55
+ isSameYear = (value, comparing) => value.isSame(comparing, 'year');
56
+ isSameMonth = (value, comparing) => value.isSame(comparing, 'month');
57
+ isSameDay = (value, comparing) => value.isSame(comparing, 'day');
58
+ isAfter = (value, comparing) => value.isAfter(comparing);
59
+ isBefore = (value, comparing) => value.isBefore(comparing);
60
+ isWithinRange = (value, [start, end]) => !value.isBefore(start) && !value.isAfter(end);
61
+ startOfYear = (value) => value.startOf('year');
62
+ startOfMonth = (value) => value.startOf('month');
63
+ startOfWeek = (value) => value.startOf('day').subtract((value.day() - this.getWeekStart(value) + 7) % 7, 'day');
64
+ startOfDay = (value) => value.startOf('day');
65
+ endOfYear = (value) => value.endOf('year');
66
+ endOfMonth = (value) => value.endOf('month');
67
+ endOfWeek = (value) => this.startOfWeek(value).add(6, 'day').endOf('day');
68
+ endOfDay = (value) => value.endOf('day');
69
+ addYears = (value, amount) => this.addMonths(value, amount * 12);
70
+ addMonths = (value, amount) => {
71
+ const day = value.date();
72
+ const result = value.date(1).add(amount, 'month');
73
+ return result.date(Math.min(day, result.daysInMonth()));
74
+ };
75
+ addWeeks = (value, amount) => value.add(amount, 'week');
76
+ addDays = (value, amount) => value.add(amount, 'day');
77
+ addHours = (value, amount) => value.add(amount, 'hour');
78
+ addMinutes = (value, amount) => value.add(amount, 'minute');
79
+ addSeconds = (value, amount) => value.add(amount, 'second');
80
+ getYear = (value) => value.year();
81
+ getMonth = (value) => value.month();
82
+ getDate = (value) => value.date();
83
+ getHours = (value) => value.hour();
84
+ getMinutes = (value) => value.minute();
85
+ getSeconds = (value) => value.second();
86
+ getMilliseconds = (value) => value.millisecond();
87
+ setYear = (value, year) => {
88
+ const day = value.date();
89
+ const result = value.date(1).year(year);
90
+ return result.date(Math.min(day, result.daysInMonth()));
91
+ };
92
+ setMonth = (value, month) => {
93
+ const day = value.date();
94
+ const result = value.date(1).month(month);
95
+ return result.date(Math.min(day, result.daysInMonth()));
96
+ };
97
+ setDate = (value, date) => value.date(date);
98
+ setHours = (value, hours) => value.hour(hours);
99
+ setMinutes = (value, minutes) => value.minute(minutes);
100
+ setSeconds = (value, seconds) => value.second(seconds);
101
+ setMilliseconds = (value, milliseconds) => value.millisecond(milliseconds);
102
+ getDaysInMonth = (value) => value.daysInMonth();
103
+ getWeekArray = (value) => {
104
+ const start = this.startOfWeek(this.startOfMonth(value));
105
+ const end = this.endOfWeek(this.endOfMonth(value));
106
+ const weeks = [];
107
+ let current = start;
108
+ for (let count = 0; !current.isAfter(end); count += 1) {
109
+ (weeks[Math.floor(count / 7)] ??= []).push(current);
110
+ current = this.addDays(current, 1);
111
+ }
112
+ return weeks;
113
+ };
114
+ /** Week number where week 1 contains January 1, matching the other adapters. */
115
+ getWeekNumber = (value) => {
116
+ const yearStart = this.startOfWeek(this.startOfYear(value));
117
+ const weekStart = this.startOfWeek(value);
118
+ const nextYearStart = this.startOfWeek(this.startOfYear(this.addYears(value, 1)));
119
+ if (!weekStart.isBefore(nextYearStart))
120
+ return 1;
121
+ return Math.round(weekStart.diff(yearStart, 'day') / 7) + 1;
122
+ };
123
+ getDayOfWeek = (value) => ((value.day() - this.getWeekStart(value) + 7) % 7) + 1;
124
+ }
@@ -0,0 +1,66 @@
1
+ import { CalendarDateTime, type DateValue } from '@internationalized/date';
2
+ import type { DateAdapter } from './types.js';
3
+ export interface AdapterInternationalizedDateOptions {
4
+ /** Locale used for week rules and formatting. Defaults to the user's locale. */
5
+ locale?: string;
6
+ /** First day of the week, 0 (Sunday) to 6 (Saturday). Defaults to the locale's first day. */
7
+ weekStartsOn?: number;
8
+ /** Time zone used to create and convert dates. Defaults to the system time zone. */
9
+ timeZone?: string;
10
+ }
11
+ /** Adapter for `@internationalized/date`. Values are its immutable CalendarDate types. */
12
+ export declare class AdapterInternationalizedDate implements DateAdapter<DateValue> {
13
+ readonly lib = "@internationalized/date";
14
+ readonly locale: string;
15
+ readonly timeZone: string;
16
+ private readonly weekStartsOn?;
17
+ constructor({ locale, weekStartsOn, timeZone }?: AdapterInternationalizedDateOptions);
18
+ date: (value?: string) => DateValue;
19
+ getInvalidDate: () => DateValue;
20
+ isValid: (value: DateValue | null | undefined) => value is DateValue;
21
+ toJsDate: (value: DateValue) => Date;
22
+ private asDateTime;
23
+ private startDay;
24
+ formatByString: (value: DateValue, format: string) => string;
25
+ parse: (value: string, format: string) => DateValue | null;
26
+ isEqual: (value: DateValue | null, comparing: DateValue | null) => boolean;
27
+ isSameYear: (value: DateValue, comparing: DateValue) => boolean;
28
+ isSameMonth: (value: DateValue, comparing: DateValue) => boolean;
29
+ isSameDay: (value: DateValue, comparing: DateValue) => boolean;
30
+ isAfter: (value: DateValue, comparing: DateValue) => boolean;
31
+ isBefore: (value: DateValue, comparing: DateValue) => boolean;
32
+ isWithinRange: (value: DateValue, [start, end]: [DateValue, DateValue]) => boolean;
33
+ startOfYear: (value: DateValue) => DateValue;
34
+ startOfMonth: (value: DateValue) => DateValue;
35
+ startOfWeek: (value: DateValue) => DateValue;
36
+ startOfDay: (value: DateValue) => DateValue;
37
+ endOfYear: (value: DateValue) => DateValue;
38
+ endOfMonth: (value: DateValue) => DateValue;
39
+ endOfWeek: (value: DateValue) => DateValue;
40
+ endOfDay: (value: DateValue) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
41
+ addYears: (value: DateValue, amount: number) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
42
+ addMonths: (value: DateValue, amount: number) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
43
+ addWeeks: (value: DateValue, amount: number) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
44
+ addDays: (value: DateValue, amount: number) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
45
+ addHours: (value: DateValue, amount: number) => CalendarDateTime | import("@internationalized/date").ZonedDateTime;
46
+ addMinutes: (value: DateValue, amount: number) => CalendarDateTime | import("@internationalized/date").ZonedDateTime;
47
+ addSeconds: (value: DateValue, amount: number) => CalendarDateTime | import("@internationalized/date").ZonedDateTime;
48
+ getYear: (value: DateValue) => number;
49
+ getMonth: (value: DateValue) => number;
50
+ getDate: (value: DateValue) => number;
51
+ getHours: (value: DateValue) => number;
52
+ getMinutes: (value: DateValue) => number;
53
+ getSeconds: (value: DateValue) => number;
54
+ getMilliseconds: (value: DateValue) => number;
55
+ setYear: (value: DateValue, year: number) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
56
+ setMonth: (value: DateValue, month: number) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
57
+ setDate: (value: DateValue, day: number) => import("@internationalized/date").CalendarDate | CalendarDateTime | import("@internationalized/date").ZonedDateTime;
58
+ setHours: (value: DateValue, hour: number) => CalendarDateTime | import("@internationalized/date").ZonedDateTime;
59
+ setMinutes: (value: DateValue, minute: number) => CalendarDateTime | import("@internationalized/date").ZonedDateTime;
60
+ setSeconds: (value: DateValue, second: number) => CalendarDateTime | import("@internationalized/date").ZonedDateTime;
61
+ setMilliseconds: (value: DateValue, millisecond: number) => CalendarDateTime | import("@internationalized/date").ZonedDateTime;
62
+ getDaysInMonth: (value: DateValue) => number;
63
+ getWeekArray: (value: DateValue) => DateValue[][];
64
+ getWeekNumber: (value: DateValue) => number;
65
+ getDayOfWeek: (value: DateValue) => number;
66
+ }
@@ -0,0 +1,144 @@
1
+ import { CalendarDateTime, endOfMonth, endOfWeek, endOfYear, getLocalTimeZone, getDayOfWeek, isSameDay, isSameMonth, isSameYear, now, parseDate, parseDateTime, parseZonedDateTime, startOfMonth, startOfWeek, startOfYear, toCalendarDateTime, } from '@internationalized/date';
2
+ const WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
3
+ const invalidValues = new WeakSet();
4
+ /** Adapter for `@internationalized/date`. Values are its immutable CalendarDate types. */
5
+ export class AdapterInternationalizedDate {
6
+ lib = '@internationalized/date';
7
+ locale;
8
+ timeZone;
9
+ weekStartsOn;
10
+ constructor({ locale, weekStartsOn, timeZone } = {}) {
11
+ this.locale = locale ?? (typeof navigator === 'undefined' ? 'en-US' : navigator.language);
12
+ this.timeZone = timeZone ?? getLocalTimeZone();
13
+ if (weekStartsOn !== undefined) {
14
+ if (!Number.isInteger(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6) {
15
+ throw new RangeError('weekStartsOn must be an integer from 0 to 6.');
16
+ }
17
+ this.weekStartsOn = WEEKDAYS[weekStartsOn];
18
+ }
19
+ }
20
+ date = (value) => {
21
+ if (value === undefined)
22
+ return toCalendarDateTime(now(this.timeZone));
23
+ try {
24
+ if (/T.*(?:Z|[+-]\d{2}:?\d{2})$/.test(value))
25
+ return parseZonedDateTime(value);
26
+ return value.includes('T') ? parseDateTime(value) : parseDate(value);
27
+ }
28
+ catch {
29
+ return this.getInvalidDate();
30
+ }
31
+ };
32
+ getInvalidDate = () => {
33
+ const value = new CalendarDateTime(1, 1, 1);
34
+ invalidValues.add(value);
35
+ return value;
36
+ };
37
+ isValid = (value) => value != null && !invalidValues.has(value);
38
+ toJsDate = (value) => 'timeZone' in value ? value.toDate() : value.toDate(this.timeZone);
39
+ asDateTime = (value) => toCalendarDateTime(value);
40
+ startDay = (value) => 'hour' in value ? value.set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) : value;
41
+ formatByString = (value, format) => {
42
+ const pad = (number, size = 2) => String(number).padStart(size, '0');
43
+ const tokens = {
44
+ yyyy: pad(value.year, 4),
45
+ MM: pad(value.month),
46
+ dd: pad(value.day),
47
+ HH: pad(this.getHours(value)),
48
+ mm: pad(this.getMinutes(value)),
49
+ ss: pad(this.getSeconds(value)),
50
+ SSS: pad(this.getMilliseconds(value), 3),
51
+ };
52
+ return format.replace(/yyyy|SSS|MM|dd|HH|mm|ss/g, (token) => tokens[token] ?? token);
53
+ };
54
+ parse = (value, format) => {
55
+ if (value === '')
56
+ return null;
57
+ const order = [];
58
+ const source = format
59
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
60
+ .replace(/yyyy|SSS|MM|dd|HH|mm|ss/g, (token) => {
61
+ order.push(token);
62
+ return `(\\d{${token.length}})`;
63
+ });
64
+ const match = new RegExp(`^${source}$`).exec(value);
65
+ if (!match)
66
+ return this.getInvalidDate();
67
+ const part = (token, fallback) => {
68
+ const index = order.indexOf(token);
69
+ return index === -1 ? fallback : Number(match[index + 1]);
70
+ };
71
+ try {
72
+ const year = part('yyyy', 1970);
73
+ const month = part('MM', 1);
74
+ const day = part('dd', 1);
75
+ return order.some((token) => ['HH', 'mm', 'ss', 'SSS'].includes(token))
76
+ ? new CalendarDateTime(year, month, day, part('HH', 0), part('mm', 0), part('ss', 0), part('SSS', 0))
77
+ : parseDate(`${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`);
78
+ }
79
+ catch {
80
+ return this.getInvalidDate();
81
+ }
82
+ };
83
+ isEqual = (value, comparing) => value === null || comparing === null ? value === comparing : value.compare(comparing) === 0;
84
+ isSameYear = (value, comparing) => isSameYear(value, comparing);
85
+ isSameMonth = (value, comparing) => isSameMonth(value, comparing);
86
+ isSameDay = (value, comparing) => isSameDay(value, comparing);
87
+ isAfter = (value, comparing) => value.compare(comparing) > 0;
88
+ isBefore = (value, comparing) => value.compare(comparing) < 0;
89
+ isWithinRange = (value, [start, end]) => value.compare(start) >= 0 && value.compare(end) <= 0;
90
+ startOfYear = (value) => startOfYear(value);
91
+ startOfMonth = (value) => startOfMonth(value);
92
+ startOfWeek = (value) => startOfWeek(value, this.locale, this.weekStartsOn);
93
+ startOfDay = (value) => this.startDay(value);
94
+ endOfYear = (value) => endOfYear(value);
95
+ endOfMonth = (value) => endOfMonth(value);
96
+ endOfWeek = (value) => endOfWeek(value, this.locale, this.weekStartsOn);
97
+ endOfDay = (value) => 'hour' in value ? this.startDay(value).add({ days: 1 }).subtract({ milliseconds: 1 }) : value;
98
+ addYears = (value, amount) => value.add({ years: amount });
99
+ addMonths = (value, amount) => value.add({ months: amount });
100
+ addWeeks = (value, amount) => value.add({ weeks: amount });
101
+ addDays = (value, amount) => value.add({ days: amount });
102
+ addHours = (value, amount) => 'hour' in value ? value.add({ hours: amount }) : this.asDateTime(value).add({ hours: amount });
103
+ addMinutes = (value, amount) => 'hour' in value
104
+ ? value.add({ minutes: amount })
105
+ : this.asDateTime(value).add({ minutes: amount });
106
+ addSeconds = (value, amount) => 'hour' in value
107
+ ? value.add({ seconds: amount })
108
+ : this.asDateTime(value).add({ seconds: amount });
109
+ getYear = (value) => value.year;
110
+ getMonth = (value) => value.month - 1;
111
+ getDate = (value) => value.day;
112
+ getHours = (value) => ('hour' in value ? value.hour : 0);
113
+ getMinutes = (value) => ('minute' in value ? value.minute : 0);
114
+ getSeconds = (value) => ('second' in value ? value.second : 0);
115
+ getMilliseconds = (value) => ('millisecond' in value ? value.millisecond : 0);
116
+ setYear = (value, year) => value.set({ year });
117
+ setMonth = (value, month) => value.set({ month: month + 1 });
118
+ setDate = (value, day) => value.set({ day });
119
+ setHours = (value, hour) => 'hour' in value ? value.set({ hour }) : this.asDateTime(value).set({ hour });
120
+ setMinutes = (value, minute) => 'hour' in value ? value.set({ minute }) : this.asDateTime(value).set({ minute });
121
+ setSeconds = (value, second) => 'hour' in value ? value.set({ second }) : this.asDateTime(value).set({ second });
122
+ setMilliseconds = (value, millisecond) => 'hour' in value ? value.set({ millisecond }) : this.asDateTime(value).set({ millisecond });
123
+ getDaysInMonth = (value) => value.calendar.getDaysInMonth(value);
124
+ getWeekArray = (value) => {
125
+ const start = this.startOfWeek(this.startOfMonth(value));
126
+ const end = this.endOfWeek(this.endOfMonth(value));
127
+ const weeks = [];
128
+ let current = start;
129
+ for (let count = 0; current.compare(end) <= 0; count += 1) {
130
+ (weeks[Math.floor(count / 7)] ??= []).push(current);
131
+ current = current.add({ days: 1 });
132
+ }
133
+ return weeks;
134
+ };
135
+ getWeekNumber = (value) => {
136
+ const yearStart = this.startOfWeek(this.startOfYear(value));
137
+ const weekStart = this.startOfWeek(value);
138
+ const nextYearStart = this.startOfWeek(this.startOfYear(value.add({ years: 1 })));
139
+ if (weekStart.compare(nextYearStart) >= 0)
140
+ return 1;
141
+ return Math.round((+this.toJsDate(weekStart) - +this.toJsDate(yearStart)) / 604_800_000) + 1;
142
+ };
143
+ getDayOfWeek = (value) => getDayOfWeek(value, this.locale, this.weekStartsOn) + 1;
144
+ }
@@ -0,0 +1,5 @@
1
+ import type { DateAdapter } from './types.js';
2
+ /** Makes an adapter available to date components rendered below the current Svelte component. */
3
+ export declare function provideDateAdapter<T>(adapter: DateAdapter<T>): void;
4
+ /** Reads the adapter from the current component tree, throwing when no provider is present. */
5
+ export declare function getDateAdapter<T>(componentName: string): DateAdapter<T>;
@@ -0,0 +1,14 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ const dateAdapterContextKey = Symbol.for('svelte-date-utils.date-adapter');
3
+ /** Makes an adapter available to date components rendered below the current Svelte component. */
4
+ export function provideDateAdapter(adapter) {
5
+ setContext(dateAdapterContextKey, adapter);
6
+ }
7
+ /** Reads the adapter from the current component tree, throwing when no provider is present. */
8
+ export function getDateAdapter(componentName) {
9
+ const adapter = getContext(dateAdapterContextKey);
10
+ if (!adapter) {
11
+ throw new Error(`${componentName} requires a date adapter context. Call provideDateAdapter(adapter) in a parent component.`);
12
+ }
13
+ return adapter;
14
+ }
@@ -0,0 +1,2 @@
1
+ export { AdapterDayjs } from './AdapterDayjs.js';
2
+ export type { AdapterDayjsOptions } from './AdapterDayjs.js';
package/dist/dayjs.js ADDED
@@ -0,0 +1 @@
1
+ export { AdapterDayjs } from './AdapterDayjs.js';
@@ -0,0 +1,13 @@
1
+ export interface UserLocaleOptions {
2
+ /** Add `fallbackLocale` at the end of the list. Defaults to true. */
3
+ useFallbackLocale?: boolean;
4
+ /** Defaults to `en-US`. */
5
+ fallbackLocale?: string;
6
+ }
7
+ /** The locales of the browser, most preferred first, without duplicates. */
8
+ export declare function getUserLocales({ useFallbackLocale, fallbackLocale, }?: UserLocaleOptions): string[];
9
+ /** The most preferred locale of the browser. With `useFallbackLocale: false` there may be none. */
10
+ export declare function getUserLocale(options?: UserLocaleOptions & {
11
+ useFallbackLocale?: true;
12
+ }): string;
13
+ export declare function getUserLocale(options: UserLocaleOptions): string | undefined;
@@ -0,0 +1,39 @@
1
+ // A dependency-free replacement for `get-user-locale` (and its `memoize` dependency).
2
+ const splitCommas = (locale) => (locale.includes(',') ? locale.split(',') : [locale]);
3
+ /** Turns `en-us` into `en-US`, `en-US.UTF-8` and `en-US@posix` into `en-US`, and `C` into `en-US`. */
4
+ function normalizeLocale(locale) {
5
+ if (!locale)
6
+ return locale;
7
+ if (locale === 'C' || locale === 'posix' || locale === 'POSIX')
8
+ return 'en-US';
9
+ // "en-US.UTF-8": keep the part before the dot
10
+ if (locale.includes('.'))
11
+ return normalizeLocale(locale.split('.')[0] ?? '');
12
+ // "en-US@posix": keep the part before the at sign
13
+ if (locale.includes('@'))
14
+ return normalizeLocale(locale.split('@')[0] ?? '');
15
+ // already "en-US", or has no region
16
+ if (!locale.includes('-') || locale.toLowerCase() !== locale)
17
+ return locale;
18
+ const [language, region = ''] = locale.split('-');
19
+ return `${language}-${region.toUpperCase()}`;
20
+ }
21
+ /** The locales of the browser, most preferred first, without duplicates. */
22
+ export function getUserLocales({ useFallbackLocale = true, fallbackLocale = 'en-US', } = {}) {
23
+ const list = [];
24
+ if (typeof navigator !== 'undefined') {
25
+ for (const language of navigator.languages ?? [])
26
+ list.push(...splitCommas(language));
27
+ if (navigator.language)
28
+ list.push(...splitCommas(navigator.language));
29
+ }
30
+ if (useFallbackLocale)
31
+ list.push(fallbackLocale);
32
+ return list
33
+ .filter((locale) => typeof locale === 'string')
34
+ .map(normalizeLocale)
35
+ .filter((locale, index, all) => all.indexOf(locale) === index);
36
+ }
37
+ export function getUserLocale(options) {
38
+ return getUserLocales(options)[0];
39
+ }
package/dist/index.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  export { AdapterDate } from './AdapterDate.js';
2
2
  export type { AdapterDateOptions } from './AdapterDate.js';
3
3
  export { createDateUtils } from './createDateUtils.js';
4
+ export { getDateAdapter, provideDateAdapter } from './context.js';
4
5
  export type { DateUtils, TimeLike, YearLike } from './createDateUtils.js';
5
6
  export type { DateAdapter } from './types.js';
7
+ export { getUserLocale, getUserLocales } from './getUserLocale.js';
8
+ export type { UserLocaleOptions } from './getUserLocale.js';
6
9
  /**
7
10
  * The helpers on the native `Date`, with the names and behavior of `@wojtekmaj/date-utils`.
8
11
  * For date-fns or Luxon, call `createDateUtils` with `AdapterDateFns` or `AdapterLuxon`
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ import { AdapterDate } from './AdapterDate.js';
2
2
  import { createDateUtils } from './createDateUtils.js';
3
3
  export { AdapterDate } from './AdapterDate.js';
4
4
  export { createDateUtils } from './createDateUtils.js';
5
+ export { getDateAdapter, provideDateAdapter } from './context.js';
6
+ export { getUserLocale, getUserLocales } from './getUserLocale.js';
5
7
  /**
6
8
  * The helpers on the native `Date`, with the names and behavior of `@wojtekmaj/date-utils`.
7
9
  * For date-fns or Luxon, call `createDateUtils` with `AdapterDateFns` or `AdapterLuxon`
@@ -0,0 +1,2 @@
1
+ export { AdapterInternationalizedDate } from './AdapterInternationalizedDate.js';
2
+ export type { AdapterInternationalizedDateOptions } from './AdapterInternationalizedDate.js';
@@ -0,0 +1 @@
1
+ export { AdapterInternationalizedDate } from './AdapterInternationalizedDate.js';
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "svelte-date-utils",
3
- "version": "0.2.0",
4
- "description": "Date helpers for calendars and date inputs (start, end, previous, next and range of a century, decade, year, month and day) that work on native Date, date-fns or Luxon through adapters.",
3
+ "version": "0.5.0",
4
+ "description": "Date helpers for calendars and date inputs (start, end, previous, next and range of a century, decade, year, month and day) with native Date, date-fns, Day.js, Luxon and @internationalized/date adapters.",
5
5
  "keywords": [
6
6
  "date",
7
7
  "date-fns",
8
+ "dayjs",
8
9
  "luxon",
9
10
  "svelte",
10
11
  "utils"
@@ -31,9 +32,17 @@
31
32
  "types": "./dist/date-fns.d.ts",
32
33
  "default": "./dist/date-fns.js"
33
34
  },
35
+ "./dayjs": {
36
+ "types": "./dist/dayjs.d.ts",
37
+ "default": "./dist/dayjs.js"
38
+ },
34
39
  "./luxon": {
35
40
  "types": "./dist/luxon.d.ts",
36
41
  "default": "./dist/luxon.js"
42
+ },
43
+ "./internationalized": {
44
+ "types": "./dist/internationalized.d.ts",
45
+ "default": "./dist/internationalized.js"
37
46
  }
38
47
  },
39
48
  "publishConfig": {
@@ -52,26 +61,41 @@
52
61
  "watch": "tsc -p tsconfig.build.json --watch"
53
62
  },
54
63
  "devDependencies": {
64
+ "@internationalized/date": "^3.12.4",
55
65
  "@types/luxon": "^3.7.1",
56
66
  "@vitest/browser-playwright": "5.0.5",
57
67
  "date-fns": "^4.1.0",
68
+ "dayjs": "^1.11.23",
58
69
  "luxon": "^3.7.2",
59
70
  "playwright": "^1.64.1",
71
+ "svelte": "^5.0.0",
60
72
  "typescript": "^6.0.3",
61
73
  "vite": "npm:@voidzero-dev/vite-plus-core@1.4.0",
62
74
  "vite-plus": "1.4.0",
63
75
  "vitest": "5.0.5"
64
76
  },
65
77
  "peerDependencies": {
78
+ "@internationalized/date": "^3.12.4",
66
79
  "date-fns": "^3.2.0 || ^4.0.0",
67
- "luxon": "^3.0.2"
80
+ "dayjs": "^1.11.23",
81
+ "luxon": "^3.0.2",
82
+ "svelte": "^5.0.0"
68
83
  },
69
84
  "peerDependenciesMeta": {
85
+ "@internationalized/date": {
86
+ "optional": true
87
+ },
70
88
  "date-fns": {
71
89
  "optional": true
72
90
  },
73
91
  "luxon": {
74
92
  "optional": true
93
+ },
94
+ "dayjs": {
95
+ "optional": true
96
+ },
97
+ "svelte": {
98
+ "optional": true
75
99
  }
76
100
  }
77
101
  }
@@ -0,0 +1,159 @@
1
+ import dayjs from 'dayjs';
2
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js';
3
+ import localeData from 'dayjs/plugin/localeData.js';
4
+
5
+ import type { Dayjs } from 'dayjs';
6
+ import type { DateAdapter } from './types.js';
7
+
8
+ dayjs.extend(customParseFormat);
9
+ dayjs.extend(localeData);
10
+
11
+ const FORMAT_TOKENS: Record<string, string> = {
12
+ yyyy: 'YYYY',
13
+ yy: 'YY',
14
+ y: 'YYYY',
15
+ MMMM: 'MMMM',
16
+ MMM: 'MMM',
17
+ MM: 'MM',
18
+ M: 'M',
19
+ dd: 'DD',
20
+ d: 'D',
21
+ HH: 'HH',
22
+ H: 'H',
23
+ hh: 'hh',
24
+ h: 'h',
25
+ mm: 'mm',
26
+ m: 'm',
27
+ ss: 'ss',
28
+ s: 's',
29
+ SSS: 'SSS',
30
+ a: 'a',
31
+ };
32
+
33
+ const FORMAT_TOKEN_PATTERN = /yyyy|MMMM|MMM|yy|y|MM|M|dd|d|HH|H|hh|h|mm|m|ss|s|SSS|a/g;
34
+
35
+ export interface AdapterDayjsOptions {
36
+ /** Locale such as `de` or `en-gb`. Import the matching Day.js locale before using it. */
37
+ locale?: string;
38
+ /** First day of the week, 0 (Sunday) to 6 (Saturday). Defaults to the locale's first day. */
39
+ weekStartsOn?: number;
40
+ }
41
+
42
+ /** Adapter for Day.js. Values are immutable Day.js `Dayjs` objects. */
43
+ export class AdapterDayjs implements DateAdapter<Dayjs> {
44
+ public readonly lib = 'dayjs';
45
+ public readonly locale: string;
46
+ private readonly weekStartsOn?: number;
47
+
48
+ constructor({ locale = 'en', weekStartsOn }: AdapterDayjsOptions = {}) {
49
+ if (
50
+ weekStartsOn !== undefined &&
51
+ (!Number.isInteger(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6)
52
+ ) {
53
+ throw new RangeError('weekStartsOn must be an integer from 0 to 6.');
54
+ }
55
+ this.locale = locale.toLowerCase();
56
+ this.weekStartsOn = weekStartsOn;
57
+ }
58
+
59
+ private withLocale = (value: Dayjs) => value.locale(this.locale);
60
+ private getWeekStart = (value: Dayjs) =>
61
+ this.weekStartsOn ?? this.withLocale(value).localeData().firstDayOfWeek();
62
+ private formatForDayjs = (format: string) =>
63
+ format.replace(FORMAT_TOKEN_PATTERN, (token) => FORMAT_TOKENS[token] ?? token);
64
+
65
+ public date = (value?: string) =>
66
+ value === undefined ? this.withLocale(dayjs()) : this.withLocale(dayjs(value));
67
+ public getInvalidDate = () => this.withLocale(dayjs(Number.NaN));
68
+ public isValid = (value: Dayjs | null | undefined): value is Dayjs =>
69
+ value != null && value.isValid();
70
+ public toJsDate = (value: Dayjs) => value.toDate();
71
+
72
+ public formatByString = (value: Dayjs, format: string) =>
73
+ this.withLocale(value).format(this.formatForDayjs(format));
74
+ public parse = (value: string, format: string) =>
75
+ value === ''
76
+ ? null
77
+ : this.withLocale(dayjs(value, this.formatForDayjs(format), this.locale, true));
78
+
79
+ public isEqual = (value: Dayjs | null, comparing: Dayjs | null) =>
80
+ value === null || comparing === null
81
+ ? value === comparing
82
+ : value.valueOf() === comparing.valueOf();
83
+ public isSameYear = (value: Dayjs, comparing: Dayjs) => value.isSame(comparing, 'year');
84
+ public isSameMonth = (value: Dayjs, comparing: Dayjs) => value.isSame(comparing, 'month');
85
+ public isSameDay = (value: Dayjs, comparing: Dayjs) => value.isSame(comparing, 'day');
86
+ public isAfter = (value: Dayjs, comparing: Dayjs) => value.isAfter(comparing);
87
+ public isBefore = (value: Dayjs, comparing: Dayjs) => value.isBefore(comparing);
88
+ public isWithinRange = (value: Dayjs, [start, end]: [Dayjs, Dayjs]) =>
89
+ !value.isBefore(start) && !value.isAfter(end);
90
+
91
+ public startOfYear = (value: Dayjs) => value.startOf('year');
92
+ public startOfMonth = (value: Dayjs) => value.startOf('month');
93
+ public startOfWeek = (value: Dayjs) =>
94
+ value.startOf('day').subtract((value.day() - this.getWeekStart(value) + 7) % 7, 'day');
95
+ public startOfDay = (value: Dayjs) => value.startOf('day');
96
+ public endOfYear = (value: Dayjs) => value.endOf('year');
97
+ public endOfMonth = (value: Dayjs) => value.endOf('month');
98
+ public endOfWeek = (value: Dayjs) => this.startOfWeek(value).add(6, 'day').endOf('day');
99
+ public endOfDay = (value: Dayjs) => value.endOf('day');
100
+
101
+ public addYears = (value: Dayjs, amount: number) => this.addMonths(value, amount * 12);
102
+ public addMonths = (value: Dayjs, amount: number) => {
103
+ const day = value.date();
104
+ const result = value.date(1).add(amount, 'month');
105
+ return result.date(Math.min(day, result.daysInMonth()));
106
+ };
107
+ public addWeeks = (value: Dayjs, amount: number) => value.add(amount, 'week');
108
+ public addDays = (value: Dayjs, amount: number) => value.add(amount, 'day');
109
+ public addHours = (value: Dayjs, amount: number) => value.add(amount, 'hour');
110
+ public addMinutes = (value: Dayjs, amount: number) => value.add(amount, 'minute');
111
+ public addSeconds = (value: Dayjs, amount: number) => value.add(amount, 'second');
112
+
113
+ public getYear = (value: Dayjs) => value.year();
114
+ public getMonth = (value: Dayjs) => value.month();
115
+ public getDate = (value: Dayjs) => value.date();
116
+ public getHours = (value: Dayjs) => value.hour();
117
+ public getMinutes = (value: Dayjs) => value.minute();
118
+ public getSeconds = (value: Dayjs) => value.second();
119
+ public getMilliseconds = (value: Dayjs) => value.millisecond();
120
+
121
+ public setYear = (value: Dayjs, year: number) => {
122
+ const day = value.date();
123
+ const result = value.date(1).year(year);
124
+ return result.date(Math.min(day, result.daysInMonth()));
125
+ };
126
+ public setMonth = (value: Dayjs, month: number) => {
127
+ const day = value.date();
128
+ const result = value.date(1).month(month);
129
+ return result.date(Math.min(day, result.daysInMonth()));
130
+ };
131
+ public setDate = (value: Dayjs, date: number) => value.date(date);
132
+ public setHours = (value: Dayjs, hours: number) => value.hour(hours);
133
+ public setMinutes = (value: Dayjs, minutes: number) => value.minute(minutes);
134
+ public setSeconds = (value: Dayjs, seconds: number) => value.second(seconds);
135
+ public setMilliseconds = (value: Dayjs, milliseconds: number) => value.millisecond(milliseconds);
136
+
137
+ public getDaysInMonth = (value: Dayjs) => value.daysInMonth();
138
+ public getWeekArray = (value: Dayjs) => {
139
+ const start = this.startOfWeek(this.startOfMonth(value));
140
+ const end = this.endOfWeek(this.endOfMonth(value));
141
+ const weeks: Dayjs[][] = [];
142
+ let current = start;
143
+ for (let count = 0; !current.isAfter(end); count += 1) {
144
+ (weeks[Math.floor(count / 7)] ??= []).push(current);
145
+ current = this.addDays(current, 1);
146
+ }
147
+ return weeks;
148
+ };
149
+
150
+ /** Week number where week 1 contains January 1, matching the other adapters. */
151
+ public getWeekNumber = (value: Dayjs) => {
152
+ const yearStart = this.startOfWeek(this.startOfYear(value));
153
+ const weekStart = this.startOfWeek(value);
154
+ const nextYearStart = this.startOfWeek(this.startOfYear(this.addYears(value, 1)));
155
+ if (!weekStart.isBefore(nextYearStart)) return 1;
156
+ return Math.round(weekStart.diff(yearStart, 'day') / 7) + 1;
157
+ };
158
+ public getDayOfWeek = (value: Dayjs) => ((value.day() - this.getWeekStart(value) + 7) % 7) + 1;
159
+ }
@@ -0,0 +1,202 @@
1
+ import {
2
+ CalendarDateTime,
3
+ endOfMonth,
4
+ endOfWeek,
5
+ endOfYear,
6
+ getLocalTimeZone,
7
+ getDayOfWeek,
8
+ isSameDay,
9
+ isSameMonth,
10
+ isSameYear,
11
+ now,
12
+ parseDate,
13
+ parseDateTime,
14
+ parseZonedDateTime,
15
+ startOfMonth,
16
+ startOfWeek,
17
+ startOfYear,
18
+ toCalendarDateTime,
19
+ type DateValue,
20
+ } from '@internationalized/date';
21
+ import type { DateAdapter } from './types.js';
22
+
23
+ const WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] as const;
24
+ const invalidValues = new WeakSet<object>();
25
+
26
+ export interface AdapterInternationalizedDateOptions {
27
+ /** Locale used for week rules and formatting. Defaults to the user's locale. */
28
+ locale?: string;
29
+ /** First day of the week, 0 (Sunday) to 6 (Saturday). Defaults to the locale's first day. */
30
+ weekStartsOn?: number;
31
+ /** Time zone used to create and convert dates. Defaults to the system time zone. */
32
+ timeZone?: string;
33
+ }
34
+
35
+ /** Adapter for `@internationalized/date`. Values are its immutable CalendarDate types. */
36
+ export class AdapterInternationalizedDate implements DateAdapter<DateValue> {
37
+ public readonly lib = '@internationalized/date';
38
+ public readonly locale: string;
39
+ public readonly timeZone: string;
40
+ private readonly weekStartsOn?: (typeof WEEKDAYS)[number];
41
+
42
+ constructor({ locale, weekStartsOn, timeZone }: AdapterInternationalizedDateOptions = {}) {
43
+ this.locale = locale ?? (typeof navigator === 'undefined' ? 'en-US' : navigator.language);
44
+ this.timeZone = timeZone ?? getLocalTimeZone();
45
+ if (weekStartsOn !== undefined) {
46
+ if (!Number.isInteger(weekStartsOn) || weekStartsOn < 0 || weekStartsOn > 6) {
47
+ throw new RangeError('weekStartsOn must be an integer from 0 to 6.');
48
+ }
49
+ this.weekStartsOn = WEEKDAYS[weekStartsOn];
50
+ }
51
+ }
52
+
53
+ public date = (value?: string): DateValue => {
54
+ if (value === undefined) return toCalendarDateTime(now(this.timeZone));
55
+ try {
56
+ if (/T.*(?:Z|[+-]\d{2}:?\d{2})$/.test(value)) return parseZonedDateTime(value);
57
+ return value.includes('T') ? parseDateTime(value) : parseDate(value);
58
+ } catch {
59
+ return this.getInvalidDate();
60
+ }
61
+ };
62
+ public getInvalidDate = (): DateValue => {
63
+ const value = new CalendarDateTime(1, 1, 1);
64
+ invalidValues.add(value);
65
+ return value;
66
+ };
67
+ public isValid = (value: DateValue | null | undefined): value is DateValue =>
68
+ value != null && !invalidValues.has(value);
69
+ public toJsDate = (value: DateValue) =>
70
+ 'timeZone' in value ? value.toDate() : value.toDate(this.timeZone);
71
+
72
+ private asDateTime = (value: DateValue) => toCalendarDateTime(value);
73
+ private startDay = (value: DateValue): DateValue =>
74
+ 'hour' in value ? value.set({ hour: 0, minute: 0, second: 0, millisecond: 0 }) : value;
75
+ public formatByString = (value: DateValue, format: string) => {
76
+ const pad = (number: number, size = 2) => String(number).padStart(size, '0');
77
+ const tokens: Record<string, string> = {
78
+ yyyy: pad(value.year, 4),
79
+ MM: pad(value.month),
80
+ dd: pad(value.day),
81
+ HH: pad(this.getHours(value)),
82
+ mm: pad(this.getMinutes(value)),
83
+ ss: pad(this.getSeconds(value)),
84
+ SSS: pad(this.getMilliseconds(value), 3),
85
+ };
86
+ return format.replace(/yyyy|SSS|MM|dd|HH|mm|ss/g, (token) => tokens[token] ?? token);
87
+ };
88
+
89
+ public parse = (value: string, format: string): DateValue | null => {
90
+ if (value === '') return null;
91
+ const order: string[] = [];
92
+ const source = format
93
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
94
+ .replace(/yyyy|SSS|MM|dd|HH|mm|ss/g, (token) => {
95
+ order.push(token);
96
+ return `(\\d{${token.length}})`;
97
+ });
98
+ const match = new RegExp(`^${source}$`).exec(value);
99
+ if (!match) return this.getInvalidDate();
100
+ const part = (token: string, fallback: number) => {
101
+ const index = order.indexOf(token);
102
+ return index === -1 ? fallback : Number(match[index + 1]);
103
+ };
104
+ try {
105
+ const year = part('yyyy', 1970);
106
+ const month = part('MM', 1);
107
+ const day = part('dd', 1);
108
+ return order.some((token) => ['HH', 'mm', 'ss', 'SSS'].includes(token))
109
+ ? new CalendarDateTime(
110
+ year,
111
+ month,
112
+ day,
113
+ part('HH', 0),
114
+ part('mm', 0),
115
+ part('ss', 0),
116
+ part('SSS', 0),
117
+ )
118
+ : parseDate(
119
+ `${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`,
120
+ );
121
+ } catch {
122
+ return this.getInvalidDate();
123
+ }
124
+ };
125
+
126
+ public isEqual = (value: DateValue | null, comparing: DateValue | null) =>
127
+ value === null || comparing === null ? value === comparing : value.compare(comparing) === 0;
128
+ public isSameYear = (value: DateValue, comparing: DateValue) => isSameYear(value, comparing);
129
+ public isSameMonth = (value: DateValue, comparing: DateValue) => isSameMonth(value, comparing);
130
+ public isSameDay = (value: DateValue, comparing: DateValue) => isSameDay(value, comparing);
131
+ public isAfter = (value: DateValue, comparing: DateValue) => value.compare(comparing) > 0;
132
+ public isBefore = (value: DateValue, comparing: DateValue) => value.compare(comparing) < 0;
133
+ public isWithinRange = (value: DateValue, [start, end]: [DateValue, DateValue]) =>
134
+ value.compare(start) >= 0 && value.compare(end) <= 0;
135
+
136
+ public startOfYear = (value: DateValue) => startOfYear(value);
137
+ public startOfMonth = (value: DateValue) => startOfMonth(value);
138
+ public startOfWeek = (value: DateValue) => startOfWeek(value, this.locale, this.weekStartsOn);
139
+ public startOfDay = (value: DateValue) => this.startDay(value);
140
+ public endOfYear = (value: DateValue) => endOfYear(value);
141
+ public endOfMonth = (value: DateValue) => endOfMonth(value);
142
+ public endOfWeek = (value: DateValue) => endOfWeek(value, this.locale, this.weekStartsOn);
143
+ public endOfDay = (value: DateValue) =>
144
+ 'hour' in value ? this.startDay(value).add({ days: 1 }).subtract({ milliseconds: 1 }) : value;
145
+
146
+ public addYears = (value: DateValue, amount: number) => value.add({ years: amount });
147
+ public addMonths = (value: DateValue, amount: number) => value.add({ months: amount });
148
+ public addWeeks = (value: DateValue, amount: number) => value.add({ weeks: amount });
149
+ public addDays = (value: DateValue, amount: number) => value.add({ days: amount });
150
+ public addHours = (value: DateValue, amount: number) =>
151
+ 'hour' in value ? value.add({ hours: amount }) : this.asDateTime(value).add({ hours: amount });
152
+ public addMinutes = (value: DateValue, amount: number) =>
153
+ 'hour' in value
154
+ ? value.add({ minutes: amount })
155
+ : this.asDateTime(value).add({ minutes: amount });
156
+ public addSeconds = (value: DateValue, amount: number) =>
157
+ 'hour' in value
158
+ ? value.add({ seconds: amount })
159
+ : this.asDateTime(value).add({ seconds: amount });
160
+
161
+ public getYear = (value: DateValue) => value.year;
162
+ public getMonth = (value: DateValue) => value.month - 1;
163
+ public getDate = (value: DateValue) => value.day;
164
+ public getHours = (value: DateValue) => ('hour' in value ? value.hour : 0);
165
+ public getMinutes = (value: DateValue) => ('minute' in value ? value.minute : 0);
166
+ public getSeconds = (value: DateValue) => ('second' in value ? value.second : 0);
167
+ public getMilliseconds = (value: DateValue) => ('millisecond' in value ? value.millisecond : 0);
168
+
169
+ public setYear = (value: DateValue, year: number) => value.set({ year });
170
+ public setMonth = (value: DateValue, month: number) => value.set({ month: month + 1 });
171
+ public setDate = (value: DateValue, day: number) => value.set({ day });
172
+ public setHours = (value: DateValue, hour: number) =>
173
+ 'hour' in value ? value.set({ hour }) : this.asDateTime(value).set({ hour });
174
+ public setMinutes = (value: DateValue, minute: number) =>
175
+ 'hour' in value ? value.set({ minute }) : this.asDateTime(value).set({ minute });
176
+ public setSeconds = (value: DateValue, second: number) =>
177
+ 'hour' in value ? value.set({ second }) : this.asDateTime(value).set({ second });
178
+ public setMilliseconds = (value: DateValue, millisecond: number) =>
179
+ 'hour' in value ? value.set({ millisecond }) : this.asDateTime(value).set({ millisecond });
180
+
181
+ public getDaysInMonth = (value: DateValue) => value.calendar.getDaysInMonth(value);
182
+ public getWeekArray = (value: DateValue) => {
183
+ const start = this.startOfWeek(this.startOfMonth(value));
184
+ const end = this.endOfWeek(this.endOfMonth(value));
185
+ const weeks: DateValue[][] = [];
186
+ let current = start;
187
+ for (let count = 0; current.compare(end) <= 0; count += 1) {
188
+ (weeks[Math.floor(count / 7)] ??= []).push(current);
189
+ current = current.add({ days: 1 });
190
+ }
191
+ return weeks;
192
+ };
193
+ public getWeekNumber = (value: DateValue) => {
194
+ const yearStart = this.startOfWeek(this.startOfYear(value));
195
+ const weekStart = this.startOfWeek(value);
196
+ const nextYearStart = this.startOfWeek(this.startOfYear(value.add({ years: 1 })));
197
+ if (weekStart.compare(nextYearStart) >= 0) return 1;
198
+ return Math.round((+this.toJsDate(weekStart) - +this.toJsDate(yearStart)) / 604_800_000) + 1;
199
+ };
200
+ public getDayOfWeek = (value: DateValue) =>
201
+ getDayOfWeek(value, this.locale, this.weekStartsOn) + 1;
202
+ }
package/src/context.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { getContext, setContext } from 'svelte';
2
+ import type { DateAdapter } from './types.js';
3
+
4
+ const dateAdapterContextKey = Symbol.for('svelte-date-utils.date-adapter');
5
+
6
+ /** Makes an adapter available to date components rendered below the current Svelte component. */
7
+ export function provideDateAdapter<T>(adapter: DateAdapter<T>): void {
8
+ setContext(dateAdapterContextKey, adapter);
9
+ }
10
+
11
+ /** Reads the adapter from the current component tree, throwing when no provider is present. */
12
+ export function getDateAdapter<T>(componentName: string): DateAdapter<T> {
13
+ const adapter = getContext<DateAdapter<T> | undefined>(dateAdapterContextKey);
14
+ if (!adapter) {
15
+ throw new Error(
16
+ `${componentName} requires a date adapter context. Call provideDateAdapter(adapter) in a parent component.`,
17
+ );
18
+ }
19
+ return adapter;
20
+ }
package/src/dayjs.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { AdapterDayjs } from './AdapterDayjs.js';
2
+ export type { AdapterDayjsOptions } from './AdapterDayjs.js';
@@ -0,0 +1,50 @@
1
+ // A dependency-free replacement for `get-user-locale` (and its `memoize` dependency).
2
+
3
+ export interface UserLocaleOptions {
4
+ /** Add `fallbackLocale` at the end of the list. Defaults to true. */
5
+ useFallbackLocale?: boolean;
6
+ /** Defaults to `en-US`. */
7
+ fallbackLocale?: string;
8
+ }
9
+
10
+ const splitCommas = (locale: string) => (locale.includes(',') ? locale.split(',') : [locale]);
11
+
12
+ /** Turns `en-us` into `en-US`, `en-US.UTF-8` and `en-US@posix` into `en-US`, and `C` into `en-US`. */
13
+ function normalizeLocale(locale: string): string {
14
+ if (!locale) return locale;
15
+ if (locale === 'C' || locale === 'posix' || locale === 'POSIX') return 'en-US';
16
+ // "en-US.UTF-8": keep the part before the dot
17
+ if (locale.includes('.')) return normalizeLocale(locale.split('.')[0] ?? '');
18
+ // "en-US@posix": keep the part before the at sign
19
+ if (locale.includes('@')) return normalizeLocale(locale.split('@')[0] ?? '');
20
+ // already "en-US", or has no region
21
+ if (!locale.includes('-') || locale.toLowerCase() !== locale) return locale;
22
+ const [language, region = ''] = locale.split('-');
23
+ return `${language}-${region.toUpperCase()}`;
24
+ }
25
+
26
+ /** The locales of the browser, most preferred first, without duplicates. */
27
+ export function getUserLocales({
28
+ useFallbackLocale = true,
29
+ fallbackLocale = 'en-US',
30
+ }: UserLocaleOptions = {}): string[] {
31
+ const list: unknown[] = [];
32
+
33
+ if (typeof navigator !== 'undefined') {
34
+ for (const language of navigator.languages ?? []) list.push(...splitCommas(language));
35
+ if (navigator.language) list.push(...splitCommas(navigator.language));
36
+ }
37
+ if (useFallbackLocale) list.push(fallbackLocale);
38
+
39
+ return list
40
+ .filter((locale): locale is string => typeof locale === 'string')
41
+ .map(normalizeLocale)
42
+ .filter((locale, index, all) => all.indexOf(locale) === index);
43
+ }
44
+
45
+ /** The most preferred locale of the browser. With `useFallbackLocale: false` there may be none. */
46
+ export function getUserLocale(options?: UserLocaleOptions & { useFallbackLocale?: true }): string;
47
+ export function getUserLocale(options: UserLocaleOptions): string | undefined;
48
+ export function getUserLocale(options?: UserLocaleOptions): string | undefined {
49
+ return getUserLocales(options)[0];
50
+ }
package/src/index.ts CHANGED
@@ -4,8 +4,11 @@ import { createDateUtils } from './createDateUtils.js';
4
4
  export { AdapterDate } from './AdapterDate.js';
5
5
  export type { AdapterDateOptions } from './AdapterDate.js';
6
6
  export { createDateUtils } from './createDateUtils.js';
7
+ export { getDateAdapter, provideDateAdapter } from './context.js';
7
8
  export type { DateUtils, TimeLike, YearLike } from './createDateUtils.js';
8
9
  export type { DateAdapter } from './types.js';
10
+ export { getUserLocale, getUserLocales } from './getUserLocale.js';
11
+ export type { UserLocaleOptions } from './getUserLocale.js';
9
12
 
10
13
  /**
11
14
  * The helpers on the native `Date`, with the names and behavior of `@wojtekmaj/date-utils`.
@@ -0,0 +1,2 @@
1
+ export { AdapterInternationalizedDate } from './AdapterInternationalizedDate.js';
2
+ export type { AdapterInternationalizedDateOptions } from './AdapterInternationalizedDate.js';