@samline/date 2.1.2 → 2.2.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.
package/README.md CHANGED
@@ -11,6 +11,7 @@ Day.js repository: https://github.com/iamkun/dayjs
11
11
  - format dates with configurable input and output patterns
12
12
  - default locale is English
13
13
  - load and switch supported locales on demand
14
+ - manipulate and compare dates through a chainable helper
14
15
  - create formatter instances with isolated locale state
15
16
  - enable strict parsing globally per formatter or per call
16
17
  - parse and validate dates with explicit result objects
@@ -54,7 +55,7 @@ Use the browser bundle when your project loads scripts directly in the page and
54
55
  This is useful in environments such as Shopify themes, WordPress templates, or plain HTML pages with no build step.
55
56
 
56
57
  ```html
57
- <script src="https://cdn.jsdelivr.net/npm/@samline/date@2.1.2/dist/browser/date.global.js"></script>
58
+ <script src="https://cdn.jsdelivr.net/npm/@samline/date@2.2.0/dist/browser/date.global.js"></script>
58
59
  ```
59
60
 
60
61
  Then use it from a normal script:
@@ -82,11 +83,11 @@ Use one of the package manager commands above when your project has a build step
82
83
 
83
84
  | Entrypoint | Main API | Purpose |
84
85
  | --- | --- | --- |
85
- | `@samline/date` | `createDateFormatter`, `getDate`, `parseDate`, `isValidDate` | shared core API |
86
+ | `@samline/date` | `createDateChain`, `createDateFormatter`, `getDate`, `parseDate`, `isValidDate` | shared core API |
86
87
  | `@samline/date/vanilla` | same exports as root | utility wrapper for plain TypeScript or JavaScript |
87
- | `@samline/date/react` | `useDateFormatter` | React hook with scoped formatter state |
88
- | `@samline/date/vue` | `useDateFormatter` | Vue composable with reactive locale state |
89
- | `@samline/date/svelte` | `createDateFormatterStore` | Svelte store-driven formatter API |
88
+ | `@samline/date/react` | `useDateFormatter` | React hook with scoped formatter and chain state |
89
+ | `@samline/date/vue` | `useDateFormatter` | Vue composable with reactive formatter and chain state |
90
+ | `@samline/date/svelte` | `createDateFormatterStore` | Svelte store-driven formatter and chain API |
90
91
  | `@samline/date/browser` | `DateKit` | browser global build for projects without a bundler |
91
92
 
92
93
  ## Quick Start
@@ -141,6 +142,7 @@ resolveLocale('zz-zz')
141
142
 
142
143
  The shared root entrypoint exports:
143
144
 
145
+ - `createDateChain(props?, config?)`
144
146
  - `createDateFormatter(config?)`
145
147
  - `getDate(props?, config?)`
146
148
  - `parseDate(props, config?)`
@@ -158,6 +160,7 @@ createDateFormatter(config?: {
158
160
  strict?: boolean
159
161
  invalid?: string
160
162
  }): {
163
+ createDateChain(props?: CreateDateChainOptions): DateChain
161
164
  getDate(props?: GetDateOptions): string
162
165
  parseDate(props: DateParsingOptions): ParseDateResult
163
166
  isValidDate(props: DateParsingOptions): boolean
@@ -178,6 +181,7 @@ Regional locale input falls back to the base locale when the exact variant is no
178
181
 
179
182
  The formatter instance exposes:
180
183
 
184
+ - `createDateChain(props?)`
181
185
  - `getDate(props?)`
182
186
  - `parseDate(props)`
183
187
  - `isValidDate(props)`
@@ -206,6 +210,7 @@ These helpers are also available in the browser build through `DateKit.getSuppor
206
210
  ### One-shot helpers
207
211
 
208
212
  ```ts
213
+ createDateChain(props?: CreateDateChainOptions, config?: DateFormatterConfig): DateChain
209
214
  getDate(props?: GetDateOptions, config?: DateFormatterConfig): Promise<string>
210
215
  parseDate(props: DateParsingOptions, config?: DateFormatterConfig): Promise<ParseDateResult>
211
216
  isValidDate(props: DateParsingOptions, config?: DateFormatterConfig): Promise<boolean>
@@ -217,12 +222,25 @@ All three helpers are async because they can load locale data before running the
217
222
 
218
223
  | Helper | Returns | Use it when you need |
219
224
  | --- | --- | --- |
225
+ | `createDateChain(...)` | chainable date helper | a one-shot manipulation or comparison flow with multiple date operations |
220
226
  | `getDate(...)` | formatted string | a final display value |
221
227
  | `parseDate(...)` | structured parse result | validation details, `Date`, `iso`, `timestamp`, or deferred formatting |
222
228
  | `isValidDate(...)` | boolean | only a yes or no validation check |
223
229
 
224
230
  ```ts
225
- import { getDate, isValidDate, parseDate } from '@samline/date'
231
+ import { createDateChain, getDate, isValidDate, parseDate } from '@samline/date'
232
+
233
+ const chain = createDateChain({
234
+ date: '23/03/2026',
235
+ input: 'DD/MM/YYYY'
236
+ })
237
+
238
+ await chain.ready
239
+
240
+ const moved = chain
241
+ .add(3, 'month')
242
+ .set('day', 1)
243
+ .format('YYYY-MM-DD')
226
244
 
227
245
  const value = await getDate({
228
246
  date: '23/03/2026',
@@ -241,10 +259,76 @@ const valid = await isValidDate({
241
259
  })
242
260
  ```
243
261
 
244
- They load the requested locale automatically and also use `strict: true` by default.
262
+ `getDate`, `parseDate`, and `isValidDate` are async because they can load locale data before running the operation.
263
+
264
+ `createDateChain` is also a valid one-shot helper. It returns immediately, but you must `await chain.ready` before using it when the locale may need to load.
265
+
266
+ All of these helpers use `strict: true` by default.
245
267
 
246
268
  If you call `getDate()` without props, it returns the current date formatted with the default formatter settings.
247
269
 
270
+ ### createDateChain
271
+
272
+ ```ts
273
+ const chain = createDateChain({
274
+ date: '23/03/2026',
275
+ input: 'DD/MM/YYYY',
276
+ locale: 'es',
277
+ strict: true,
278
+ invalid: 'Fecha invalida'
279
+ })
280
+
281
+ await chain.ready
282
+
283
+ chain
284
+ .add(3, 'month')
285
+ .set('day', 1)
286
+ .format('YYYY-MM-DD')
287
+ ```
288
+
289
+ Use this helper when you want to parse a date and then manipulate or compare it in the same flow.
290
+
291
+ The chainable helper exposes:
292
+
293
+ - `add(value, unit)`
294
+ - `subtract(value, unit)`
295
+ - `set(unit, value)`
296
+ - `startOf(unit)`
297
+ - `endOf(unit)`
298
+ - `format(output?)`
299
+ - `toDate()`
300
+ - `toISOString()`
301
+ - `toTimestamp()`
302
+ - `isValid()`
303
+ - `isBefore(other, unit?)`
304
+ - `isAfter(other, unit?)`
305
+ - `isSame(other, unit?)`
306
+ - `toState()`
307
+ - `ready`
308
+
309
+ `set('day', value)` is the public way to change the day of the month. Internally it maps to Day.js `set('date', value)` so the behavior stays aligned with Day.js while the public API stays clearer.
310
+
311
+ `toState()` returns the current structured state of the chain:
312
+
313
+ - valid state: `isValid`, `locale`, `date`, `iso`, `timestamp`
314
+ - invalid state: `isValid`, `locale`, `date: null`, `iso: null`, `timestamp: null`, `error`
315
+
316
+ Example:
317
+
318
+ ```ts
319
+ const chain = createDateChain({
320
+ date: '23/03/2026',
321
+ input: 'DD/MM/YYYY'
322
+ })
323
+
324
+ await chain.ready
325
+
326
+ const finalState = chain
327
+ .add(3, 'month')
328
+ .endOf('month')
329
+ .toState()
330
+ ```
331
+
248
332
  ### parseDate
249
333
 
250
334
  ```ts
@@ -655,6 +655,9 @@
655
655
  var markLocaleAsLoaded = (locale) => {
656
656
  loadedLocales.add(locale);
657
657
  };
658
+ var isLocaleLoaded = (locale) => {
659
+ return loadedLocales.has(locale);
660
+ };
658
661
  var ensureLocaleLoaded = async (locale) => {
659
662
  if (loadedLocales.has(locale)) {
660
663
  return;
@@ -694,7 +697,13 @@
694
697
  invalid: config?.invalid ?? DEFAULT_INVALID_DATE
695
698
  });
696
699
  var getInvalidDateText = (config, props) => {
697
- return props?.invalid ?? config?.invalid ?? DEFAULT_INVALID_DATE;
700
+ if (typeof props === "object" && props !== null && "invalid" in props) {
701
+ const invalid = props.invalid;
702
+ if (invalid !== void 0) {
703
+ return invalid;
704
+ }
705
+ }
706
+ return config?.invalid ?? DEFAULT_INVALID_DATE;
698
707
  };
699
708
  var getTargetLocale = (currentLocale, props) => {
700
709
  if (!props?.locale) {
@@ -720,29 +729,193 @@
720
729
  }
721
730
  return (0, import_dayjs.default)(value, [...input], locale, strict).locale(locale);
722
731
  };
732
+ var getCurrentDayjs = (locale) => {
733
+ return (0, import_dayjs.default)().locale(locale);
734
+ };
735
+ var createParseSuccess = (parsed, locale) => {
736
+ return {
737
+ isValid: true,
738
+ locale,
739
+ date: parsed.toDate(),
740
+ iso: parsed.toISOString(),
741
+ timestamp: parsed.valueOf(),
742
+ format: (output = DEFAULT_FORMAT) => parsed.format(output)
743
+ };
744
+ };
745
+ var createParseFailure = (locale, error) => {
746
+ return {
747
+ isValid: false,
748
+ locale,
749
+ date: null,
750
+ iso: null,
751
+ timestamp: null,
752
+ error
753
+ };
754
+ };
755
+ var createDateChainSuccessState = (parsed, locale) => {
756
+ return {
757
+ isValid: true,
758
+ locale,
759
+ date: parsed.toDate(),
760
+ iso: parsed.toISOString(),
761
+ timestamp: parsed.valueOf()
762
+ };
763
+ };
764
+ var createDateChainFailureState = (locale, error) => {
765
+ return {
766
+ isValid: false,
767
+ locale,
768
+ date: null,
769
+ iso: null,
770
+ timestamp: null,
771
+ error
772
+ };
773
+ };
774
+ var mapChainSetUnit = (unit) => {
775
+ if (unit === "day") {
776
+ return "date";
777
+ }
778
+ return unit;
779
+ };
780
+ var createReadyError = () => {
781
+ return new Error("Date chain is not ready. Await chain.ready before using it with locales that may need to load.");
782
+ };
783
+ var assertDateChainReady = (state) => {
784
+ if (!state.ready) {
785
+ throw createReadyError();
786
+ }
787
+ };
788
+ var getComparableDayjs = (value) => {
789
+ if (typeof value === "object" && value !== null && "toState" in value) {
790
+ const state = value.toState();
791
+ if (!state.isValid) {
792
+ return null;
793
+ }
794
+ return (0, import_dayjs.default)(state.date);
795
+ }
796
+ const comparable = (0, import_dayjs.default)(value);
797
+ if (!comparable.isValid()) {
798
+ return null;
799
+ }
800
+ return comparable;
801
+ };
802
+ var createDateChainApi = (state, ready) => {
803
+ const applyMutation = (transform) => {
804
+ assertDateChainReady(state);
805
+ if (state.current) {
806
+ state.current = transform(state.current);
807
+ }
808
+ return chain;
809
+ };
810
+ const compare = (other, comparator) => {
811
+ assertDateChainReady(state);
812
+ if (!state.current) {
813
+ return false;
814
+ }
815
+ const comparable = getComparableDayjs(other);
816
+ if (!comparable) {
817
+ return false;
818
+ }
819
+ return comparator(state.current, comparable);
820
+ };
821
+ const chain = {
822
+ ready,
823
+ add: (value, unit) => applyMutation((current) => current.add(value, unit)),
824
+ subtract: (value, unit) => applyMutation((current) => current.subtract(value, unit)),
825
+ set: (unit, value) => applyMutation((current) => current.set(mapChainSetUnit(unit), value)),
826
+ startOf: (unit) => applyMutation((current) => current.startOf(unit)),
827
+ endOf: (unit) => applyMutation((current) => current.endOf(unit)),
828
+ format: (output = DEFAULT_FORMAT) => {
829
+ assertDateChainReady(state);
830
+ if (!state.current) {
831
+ return state.invalid;
832
+ }
833
+ return state.current.format(output);
834
+ },
835
+ toDate: () => {
836
+ assertDateChainReady(state);
837
+ return state.current ? state.current.toDate() : null;
838
+ },
839
+ toISOString: () => {
840
+ assertDateChainReady(state);
841
+ return state.current ? state.current.toISOString() : null;
842
+ },
843
+ toTimestamp: () => {
844
+ assertDateChainReady(state);
845
+ return state.current ? state.current.valueOf() : null;
846
+ },
847
+ isValid: () => {
848
+ assertDateChainReady(state);
849
+ return state.current !== null;
850
+ },
851
+ isBefore: (other, unit) => compare(other, (current, comparable) => current.isBefore(comparable, unit)),
852
+ isAfter: (other, unit) => compare(other, (current, comparable) => current.isAfter(comparable, unit)),
853
+ isSame: (other, unit) => compare(other, (current, comparable) => current.isSame(comparable, unit)),
854
+ toState: () => {
855
+ assertDateChainReady(state);
856
+ if (!state.current) {
857
+ return createDateChainFailureState(state.locale, state.error ?? state.invalid);
858
+ }
859
+ return createDateChainSuccessState(state.current, state.locale);
860
+ }
861
+ };
862
+ return chain;
863
+ };
864
+ var createDateChainState = (locale, invalid, getInitialDate) => {
865
+ const state = {
866
+ locale,
867
+ invalid,
868
+ current: null,
869
+ ready: false,
870
+ error: invalid
871
+ };
872
+ const initialize = () => {
873
+ const parsed = getInitialDate();
874
+ state.ready = true;
875
+ if (!parsed.isValid()) {
876
+ state.current = null;
877
+ state.error = invalid;
878
+ return;
879
+ }
880
+ state.current = parsed;
881
+ state.error = null;
882
+ };
883
+ if (isLocaleLoaded(locale)) {
884
+ initialize();
885
+ return {
886
+ state,
887
+ ready: Promise.resolve()
888
+ };
889
+ }
890
+ return {
891
+ state,
892
+ ready: ensureLocaleLoaded(locale).then(() => {
893
+ initialize();
894
+ })
895
+ };
896
+ };
897
+ var createDateChainFromResolvedConfig = (props, config) => {
898
+ const locale = getTargetLocale(config.locale, props);
899
+ const invalid = getInvalidDateText(config, props);
900
+ const strict = props?.strict ?? config.strict;
901
+ const getInitialDate = () => {
902
+ if (props?.date === void 0) {
903
+ return getCurrentDayjs(locale);
904
+ }
905
+ return parseDateValue(props.date, props.input, locale, strict);
906
+ };
907
+ const { state, ready } = createDateChainState(locale, invalid, getInitialDate);
908
+ return createDateChainApi(state, ready);
909
+ };
723
910
  var createFormatterParseDate = (getConfig) => {
724
911
  return (props) => {
725
912
  const config = getConfig();
726
913
  const locale = getTargetLocale(config.locale, props);
727
914
  const parsed = parseDateValue(props.date, props.input, locale, props.strict ?? config.strict);
728
915
  if (!parsed.isValid()) {
729
- return {
730
- isValid: false,
731
- locale,
732
- date: null,
733
- iso: null,
734
- timestamp: null,
735
- error: getInvalidDateText(config, props)
736
- };
916
+ return createParseFailure(locale, getInvalidDateText(config, props));
737
917
  }
738
- return {
739
- isValid: true,
740
- locale,
741
- date: parsed.toDate(),
742
- iso: parsed.toISOString(),
743
- timestamp: parsed.valueOf(),
744
- format: (output = DEFAULT_FORMAT) => parsed.format(output)
745
- };
918
+ return createParseSuccess(parsed, locale);
746
919
  };
747
920
  };
748
921
  var createFormatterIsValidDate = (parseDate2) => {
@@ -755,10 +928,10 @@
755
928
  const locale = getTargetLocale(config.locale, props);
756
929
  const output = props?.output ?? DEFAULT_FORMAT;
757
930
  if (!props) {
758
- return (0, import_dayjs.default)().locale(locale).format(DEFAULT_FORMAT);
931
+ return getCurrentDayjs(locale).format(DEFAULT_FORMAT);
759
932
  }
760
933
  if (props.date === void 0) {
761
- return (0, import_dayjs.default)().locale(locale).format(output);
934
+ return getCurrentDayjs(locale).format(output);
762
935
  }
763
936
  const parsed = parseDate2({
764
937
  date: props.date,
@@ -772,6 +945,11 @@
772
945
  return parsed.format(output);
773
946
  };
774
947
  };
948
+ var createFormatterCreateDateChain = (getConfig) => {
949
+ return (props) => {
950
+ return createDateChainFromResolvedConfig(props, getConfig());
951
+ };
952
+ };
775
953
  function resolveLocaleOrThrow(locale) {
776
954
  const resolvedLocale = resolveLocale(locale);
777
955
  if (!resolvedLocale) {
@@ -782,6 +960,10 @@
782
960
  return resolvedLocale;
783
961
  }
784
962
  var getSupportedLocales = () => SUPPORTED_LOCALES;
963
+ var createDateChain = (props, config) => {
964
+ const locale = getHelperLocale(config, props);
965
+ return createDateChainFromResolvedConfig(props, createResolvedConfig(locale, config));
966
+ };
785
967
  var getDate = async (props, config) => {
786
968
  const locale = getHelperLocale(config, props);
787
969
  const formatter = createDateFormatter({ ...config, locale });
@@ -809,6 +991,7 @@
809
991
  getDate: createFormatterGetDate(getConfig),
810
992
  parseDate: parseDate2,
811
993
  isValidDate: createFormatterIsValidDate(parseDate2),
994
+ createDateChain: createFormatterCreateDateChain(getConfig),
812
995
  getSupportedLocales,
813
996
  getCurrentLocale: () => currentLocale,
814
997
  setLocale: async (locale) => {
@@ -822,6 +1005,7 @@
822
1005
 
823
1006
  // src/browser/global.ts
824
1007
  var DateKit = {
1008
+ createDateChain,
825
1009
  createDateFormatter,
826
1010
  getDate,
827
1011
  getSupportedLocales,