@corbet-labs/cdate 0.1.0 → 0.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.
@@ -0,0 +1,105 @@
1
+ # Functional Source License, Version 1.1, ALv2 Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-ALv2
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Julian Y. Richard Corbet
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the Apache License, Version 2.0 that is effective on the second anniversary of
91
+ the date we make the Software available. On or after that date, you may use the
92
+ Software under the Apache License, Version 2.0, in which case the following
93
+ will apply:
94
+
95
+ Licensed under the Apache License, Version 2.0 (the "License"); you may not use
96
+ this file except in compliance with the License.
97
+
98
+ You may obtain a copy of the License at
99
+
100
+ http://www.apache.org/licenses/LICENSE-2.0
101
+
102
+ Unless required by applicable law or agreed to in writing, software distributed
103
+ under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
104
+ CONDITIONS OF ANY KIND, either express or implied. See the License for the
105
+ specific language governing permissions and limitations under the License.
package/README.md CHANGED
@@ -1,25 +1,102 @@
1
- # @corbet-labs/cdate
1
+ # cdate
2
2
 
3
- Deterministic locale-correct date formats for formal correspondence. Pure
4
- port of the [cdate](https://github.com/corbet-labs/cdate) Rust crate: zero
5
- dependencies, zero Node APIs, synchronous, no I/O.
3
+ **Reproducible date formats for correspondence.**
6
4
 
7
- ```ts
8
- import { longDate, monthYear } from '@corbet-labs/cdate';
5
+ [![crates.io](https://img.shields.io/crates/v/cdate.svg)](https://crates.io/crates/cdate) [![npm](https://img.shields.io/npm/v/@corbet-labs/cdate.svg)](https://www.npmjs.com/package/@corbet-labs/cdate) [![Rust API](https://docs.rs/cdate/badge.svg)](https://docs.rs/cdate)
9
6
 
10
- longDate('de-CH', 2026, 9, 7); // 7. September 2026
11
- monthYear('en', 2026, 9); // September 2026
7
+ Format a Gregorian calendar date without a timezone, operating-system locale, or network dependency. The same inputs produce the same text in Rust, JavaScript, Python, and Typst.
8
+
9
+ ```js
10
+ import { longDate } from '@corbet-labs/cdate';
11
+
12
+ longDate('de-ch', 2026, 9, 7);
13
+ // 7. September 2026
14
+ ```
15
+
16
+ ## Install
17
+
18
+ | Environment | Command |
19
+ | --- | --- |
20
+ | Rust / Cargo | `cargo add cdate` |
21
+ | Node.js / npm | `npm install @corbet-labs/cdate` |
22
+ | pnpm | `pnpm add @corbet-labs/cdate` |
23
+ | Yarn | `yarn add @corbet-labs/cdate` |
24
+ | Bun | `bun add @corbet-labs/cdate` |
25
+ | Deno | `deno add npm:@corbet-labs/cdate` |
26
+
27
+ The 0.2.0 JavaScript distribution includes compiled ESM, CommonJS,
28
+ TypeScript declarations, and a standalone browser module. Node.js 20+ is
29
+ supported; no TypeScript loader is required.
30
+
31
+ ```js
32
+ // CommonJS
33
+ const { longDate } = require('@corbet-labs/cdate');
12
34
  ```
13
35
 
14
- Unknown locales fall back through the base language to English. Invalid
15
- calendar dates yield `null`, never a best effort. Behavior is defined by
16
- `tables/*.json` at the repository root; `tests/vectors/*.json` is the
17
- shared conformance suite.
36
+ ```html
37
+ <script type="module">
38
+ import { longDate } from 'https://cdn.jsdelivr.net/npm/@corbet-labs/cdate@0.2.0/dist/browser.js';
39
+ console.log(longDate('de-ch', 2026, 9, 7));
40
+ </script>
41
+ ```
42
+
43
+ Python wheels, source distributions, and CLI instructions are in the
44
+ [installation guide](https://github.com/corbet-labs/cdate/blob/main/docs/installation.md).
45
+ JSR publication and Typst availability are listed there explicitly.
46
+
47
+ ## Rust
48
+
49
+ ```rust
50
+ use cdate::{long_date, month_year};
51
+
52
+ assert_eq!(long_date("de-ch", 2026, 9, 7).as_deref(), Some("7. September 2026"));
53
+ assert_eq!(month_year("en", 2026, 9).as_deref(), Some("September 2026"));
54
+ ```
55
+
56
+ ## Python
57
+
58
+ ```python
59
+ from cdate import long_date
60
+
61
+ assert long_date("de-ch", 2026, 9, 7) == '7. September 2026'
62
+ ```
63
+
64
+ ## API
65
+
66
+ | JavaScript / Python or Rust | Example for `de-ch` |
67
+ | --- | --- |
68
+ | `longDate` / `long_date` | `7. September 2026` |
69
+ | `mediumDate` / `medium_date` | `07.09.2026` |
70
+ | `shortDate` / `short_date` | `07.09.26` |
71
+ | `monthYear` / `month_year` | `September 2026` |
72
+ | `isValidDate` / `is_valid_date` | Calendar-date validation |
73
+
74
+ The seven date-table entries are `de`, `de-at`, `de-ch`, `de-li`, `en`, `en-gb`, and `en-us`. Matching is case-insensitive; other locales fall back through the base language to English. Impossible calendar dates return `null`/`None`. Inputs are calendar components, not timestamps. Other family libraries have broader locale coverage.
75
+
76
+ ## Correspondence family
77
+
78
+ | Library | Responsibility |
79
+ | --- | --- |
80
+ | [cletter](https://github.com/corbet-labs/cletter) | Compose the correspondence helpers |
81
+ | [cgreet](https://github.com/corbet-labs/cgreet) | German salutations and titles |
82
+ | [cfarewell](https://github.com/corbet-labs/cfarewell) | Locale-specific closings |
83
+ | [cdate](https://github.com/corbet-labs/cdate) | Calendar-date formatting |
84
+ | [cink](https://github.com/corbet-labs/cink) | Handwritten signature images |
85
+
86
+
87
+ ## Development
88
+
89
+ Behavior is defined by [the locale tables](https://github.com/corbet-labs/cdate/tree/main/tables)
90
+ and [shared conformance vectors](https://github.com/corbet-labs/cdate/tree/main/tests/vectors).
91
+ Rust, JavaScript, and Python run the same vectors. Selected CI checks exercise
92
+ installed JavaScript tarballs, Python wheels and command-line entrypoints, and
93
+ Typst packages. Release validation records the actual runtime and platform;
94
+ Linux results do not establish native Windows or macOS coverage.
95
+ All five Rust crates forbid unsafe code in their own source.
18
96
 
19
- Run `bun ./scripts/conformance.mts` and `tsc --noEmit -p ./tsconfig.json`
20
- before pushing.
97
+ See [the release guide](https://github.com/corbet-labs/cdate/blob/main/docs/releasing.md)
98
+ for generation, verification, and publication commands.
21
99
 
22
100
  ## License
23
101
 
24
- FSL-1.1-ALv2. Each published version becomes available under Apache-2.0
25
- two years after publication.
102
+ FSL-1.1-ALv2. Each release becomes available under Apache-2.0 two years after publication. See [the full license](https://github.com/corbet-labs/cdate/blob/main/LICENSES/FSL-1.1-ALv2.txt) for permitted use.
@@ -0,0 +1,97 @@
1
+ // src/generated/tables.ts
2
+ var DATES_TABLE = { fallback: "en", locales: { de: { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-at": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-ch": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-li": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, en: { long: "{month_long} {day}, {yyyy}", medium: "{month_short} {day}, {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], short: "{month}/{day}/{yy}" }, "en-gb": { long: "{day} {month_long} {yyyy}", medium: "{day} {month_short} {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], short: "{dd}/{mm}/{yyyy}" }, "en-us": { long: "{month_long} {day}, {yyyy}", medium: "{month_short} {day}, {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], short: "{month}/{day}/{yy}" } }, supported: ["de", "de-at", "de-ch", "de-li", "en", "en-gb", "en-us"] };
3
+
4
+ // src/index.ts
5
+ var table = DATES_TABLE;
6
+ function baseLanguage(locale) {
7
+ return locale.split("-")[0] ?? locale;
8
+ }
9
+ function resolveKey(locale) {
10
+ const lower = locale.toLowerCase();
11
+ if (Object.hasOwn(table.locales, lower))
12
+ return lower;
13
+ const base = baseLanguage(lower);
14
+ return Object.hasOwn(table.locales, base) ? base : table.fallback;
15
+ }
16
+ function daysInMonth(year, month) {
17
+ switch (month) {
18
+ case 1:
19
+ case 3:
20
+ case 5:
21
+ case 7:
22
+ case 8:
23
+ case 10:
24
+ case 12:
25
+ return 31;
26
+ case 4:
27
+ case 6:
28
+ case 9:
29
+ case 11:
30
+ return 30;
31
+ case 2:
32
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
33
+ default:
34
+ return 0;
35
+ }
36
+ }
37
+ function isValidDate(year, month, day) {
38
+ return Number.isInteger(year) && Number.isInteger(month) && Number.isInteger(day) && month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
39
+ }
40
+ function padTwo(value) {
41
+ return value < 10 ? `0${value}` : `${value}`;
42
+ }
43
+ function padYear(year) {
44
+ const text = `${year}`;
45
+ if (year < 0 || year >= 1000)
46
+ return text;
47
+ if (year >= 100)
48
+ return `0${text}`;
49
+ if (year >= 10)
50
+ return `00${text}`;
51
+ return `000${text}`;
52
+ }
53
+ function render(entry, pattern, year, month, day) {
54
+ return pattern.replaceAll("{yyyy}", padYear(year)).replaceAll("{yy}", padTwo((year % 100 + 100) % 100)).replaceAll("{month_long}", entry.months_long[month - 1] ?? "").replaceAll("{month_short}", entry.months_short?.[month - 1] ?? "").replaceAll("{dd}", padTwo(day)).replaceAll("{mm}", padTwo(month)).replaceAll("{day}", `${day}`).replaceAll("{month}", `${month}`);
55
+ }
56
+ function formatWith(locale, select, year, month, day) {
57
+ if (!isValidDate(year, month, day))
58
+ return null;
59
+ const entry = table.locales[resolveKey(locale)];
60
+ if (entry === undefined)
61
+ return null;
62
+ return render(entry, select(entry), year, month, day);
63
+ }
64
+ function longDate(locale, year, month, day) {
65
+ return formatWith(locale, (entry) => entry.long, year, month, day);
66
+ }
67
+ function mediumDate(locale, year, month, day) {
68
+ return formatWith(locale, (entry) => entry.medium, year, month, day);
69
+ }
70
+ function shortDate(locale, year, month, day) {
71
+ return formatWith(locale, (entry) => entry.short, year, month, day);
72
+ }
73
+ function monthYear(locale, year, month) {
74
+ if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12)
75
+ return null;
76
+ const entry = table.locales[resolveKey(locale)];
77
+ if (entry === undefined)
78
+ return null;
79
+ return render(entry, entry.month_year, year, month, 1);
80
+ }
81
+ function availableLocales() {
82
+ return Object.keys(table.locales).sort();
83
+ }
84
+ function isSupported(locale) {
85
+ if (table.supported.includes(locale))
86
+ return true;
87
+ return table.supported.includes(baseLanguage(locale.toLowerCase()));
88
+ }
89
+ export {
90
+ shortDate,
91
+ monthYear,
92
+ mediumDate,
93
+ longDate,
94
+ isValidDate,
95
+ isSupported,
96
+ availableLocales
97
+ };
package/dist/index.cjs ADDED
@@ -0,0 +1,139 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ function __accessProp(key) {
6
+ return this[key];
7
+ }
8
+ var __toCommonJS = (from) => {
9
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
10
+ if (entry)
11
+ return entry;
12
+ entry = __defProp({}, "__esModule", { value: true });
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (var key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(entry, key))
16
+ __defProp(entry, key, {
17
+ get: __accessProp.bind(from, key),
18
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
19
+ });
20
+ }
21
+ __moduleCache.set(from, entry);
22
+ return entry;
23
+ };
24
+ var __moduleCache;
25
+ var __returnValue = (v) => v;
26
+ function __exportSetter(name, newValue) {
27
+ this[name] = __returnValue.bind(null, newValue);
28
+ }
29
+ var __export = (target, all) => {
30
+ for (var name in all)
31
+ __defProp(target, name, {
32
+ get: all[name],
33
+ enumerable: true,
34
+ configurable: true,
35
+ set: __exportSetter.bind(all, name)
36
+ });
37
+ };
38
+
39
+ // src/index.ts
40
+ var exports_src = {};
41
+ __export(exports_src, {
42
+ shortDate: () => shortDate,
43
+ monthYear: () => monthYear,
44
+ mediumDate: () => mediumDate,
45
+ longDate: () => longDate,
46
+ isValidDate: () => isValidDate,
47
+ isSupported: () => isSupported,
48
+ availableLocales: () => availableLocales
49
+ });
50
+ module.exports = __toCommonJS(exports_src);
51
+
52
+ // src/generated/tables.ts
53
+ var DATES_TABLE = { fallback: "en", locales: { de: { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-at": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-ch": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-li": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, en: { long: "{month_long} {day}, {yyyy}", medium: "{month_short} {day}, {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], short: "{month}/{day}/{yy}" }, "en-gb": { long: "{day} {month_long} {yyyy}", medium: "{day} {month_short} {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], short: "{dd}/{mm}/{yyyy}" }, "en-us": { long: "{month_long} {day}, {yyyy}", medium: "{month_short} {day}, {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], short: "{month}/{day}/{yy}" } }, supported: ["de", "de-at", "de-ch", "de-li", "en", "en-gb", "en-us"] };
54
+
55
+ // src/index.ts
56
+ var table = DATES_TABLE;
57
+ function baseLanguage(locale) {
58
+ return locale.split("-")[0] ?? locale;
59
+ }
60
+ function resolveKey(locale) {
61
+ const lower = locale.toLowerCase();
62
+ if (Object.hasOwn(table.locales, lower))
63
+ return lower;
64
+ const base = baseLanguage(lower);
65
+ return Object.hasOwn(table.locales, base) ? base : table.fallback;
66
+ }
67
+ function daysInMonth(year, month) {
68
+ switch (month) {
69
+ case 1:
70
+ case 3:
71
+ case 5:
72
+ case 7:
73
+ case 8:
74
+ case 10:
75
+ case 12:
76
+ return 31;
77
+ case 4:
78
+ case 6:
79
+ case 9:
80
+ case 11:
81
+ return 30;
82
+ case 2:
83
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
84
+ default:
85
+ return 0;
86
+ }
87
+ }
88
+ function isValidDate(year, month, day) {
89
+ return Number.isInteger(year) && Number.isInteger(month) && Number.isInteger(day) && month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
90
+ }
91
+ function padTwo(value) {
92
+ return value < 10 ? `0${value}` : `${value}`;
93
+ }
94
+ function padYear(year) {
95
+ const text = `${year}`;
96
+ if (year < 0 || year >= 1000)
97
+ return text;
98
+ if (year >= 100)
99
+ return `0${text}`;
100
+ if (year >= 10)
101
+ return `00${text}`;
102
+ return `000${text}`;
103
+ }
104
+ function render(entry, pattern, year, month, day) {
105
+ return pattern.replaceAll("{yyyy}", padYear(year)).replaceAll("{yy}", padTwo((year % 100 + 100) % 100)).replaceAll("{month_long}", entry.months_long[month - 1] ?? "").replaceAll("{month_short}", entry.months_short?.[month - 1] ?? "").replaceAll("{dd}", padTwo(day)).replaceAll("{mm}", padTwo(month)).replaceAll("{day}", `${day}`).replaceAll("{month}", `${month}`);
106
+ }
107
+ function formatWith(locale, select, year, month, day) {
108
+ if (!isValidDate(year, month, day))
109
+ return null;
110
+ const entry = table.locales[resolveKey(locale)];
111
+ if (entry === undefined)
112
+ return null;
113
+ return render(entry, select(entry), year, month, day);
114
+ }
115
+ function longDate(locale, year, month, day) {
116
+ return formatWith(locale, (entry) => entry.long, year, month, day);
117
+ }
118
+ function mediumDate(locale, year, month, day) {
119
+ return formatWith(locale, (entry) => entry.medium, year, month, day);
120
+ }
121
+ function shortDate(locale, year, month, day) {
122
+ return formatWith(locale, (entry) => entry.short, year, month, day);
123
+ }
124
+ function monthYear(locale, year, month) {
125
+ if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12)
126
+ return null;
127
+ const entry = table.locales[resolveKey(locale)];
128
+ if (entry === undefined)
129
+ return null;
130
+ return render(entry, entry.month_year, year, month, 1);
131
+ }
132
+ function availableLocales() {
133
+ return Object.keys(table.locales).sort();
134
+ }
135
+ function isSupported(locale) {
136
+ if (table.supported.includes(locale))
137
+ return true;
138
+ return table.supported.includes(baseLanguage(locale.toLowerCase()));
139
+ }
package/dist/index.js ADDED
@@ -0,0 +1,97 @@
1
+ // src/generated/tables.ts
2
+ var DATES_TABLE = { fallback: "en", locales: { de: { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-at": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-ch": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, "de-li": { long: "{day}. {month_long} {yyyy}", medium: "{dd}.{mm}.{yyyy}", month_year: "{month_long} {yyyy}", months_long: ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], short: "{dd}.{mm}.{yy}" }, en: { long: "{month_long} {day}, {yyyy}", medium: "{month_short} {day}, {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], short: "{month}/{day}/{yy}" }, "en-gb": { long: "{day} {month_long} {yyyy}", medium: "{day} {month_short} {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], short: "{dd}/{mm}/{yyyy}" }, "en-us": { long: "{month_long} {day}, {yyyy}", medium: "{month_short} {day}, {yyyy}", month_year: "{month_long} {yyyy}", months_long: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], months_short: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], short: "{month}/{day}/{yy}" } }, supported: ["de", "de-at", "de-ch", "de-li", "en", "en-gb", "en-us"] };
3
+
4
+ // src/index.ts
5
+ var table = DATES_TABLE;
6
+ function baseLanguage(locale) {
7
+ return locale.split("-")[0] ?? locale;
8
+ }
9
+ function resolveKey(locale) {
10
+ const lower = locale.toLowerCase();
11
+ if (Object.hasOwn(table.locales, lower))
12
+ return lower;
13
+ const base = baseLanguage(lower);
14
+ return Object.hasOwn(table.locales, base) ? base : table.fallback;
15
+ }
16
+ function daysInMonth(year, month) {
17
+ switch (month) {
18
+ case 1:
19
+ case 3:
20
+ case 5:
21
+ case 7:
22
+ case 8:
23
+ case 10:
24
+ case 12:
25
+ return 31;
26
+ case 4:
27
+ case 6:
28
+ case 9:
29
+ case 11:
30
+ return 30;
31
+ case 2:
32
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
33
+ default:
34
+ return 0;
35
+ }
36
+ }
37
+ function isValidDate(year, month, day) {
38
+ return Number.isInteger(year) && Number.isInteger(month) && Number.isInteger(day) && month >= 1 && month <= 12 && day >= 1 && day <= daysInMonth(year, month);
39
+ }
40
+ function padTwo(value) {
41
+ return value < 10 ? `0${value}` : `${value}`;
42
+ }
43
+ function padYear(year) {
44
+ const text = `${year}`;
45
+ if (year < 0 || year >= 1000)
46
+ return text;
47
+ if (year >= 100)
48
+ return `0${text}`;
49
+ if (year >= 10)
50
+ return `00${text}`;
51
+ return `000${text}`;
52
+ }
53
+ function render(entry, pattern, year, month, day) {
54
+ return pattern.replaceAll("{yyyy}", padYear(year)).replaceAll("{yy}", padTwo((year % 100 + 100) % 100)).replaceAll("{month_long}", entry.months_long[month - 1] ?? "").replaceAll("{month_short}", entry.months_short?.[month - 1] ?? "").replaceAll("{dd}", padTwo(day)).replaceAll("{mm}", padTwo(month)).replaceAll("{day}", `${day}`).replaceAll("{month}", `${month}`);
55
+ }
56
+ function formatWith(locale, select, year, month, day) {
57
+ if (!isValidDate(year, month, day))
58
+ return null;
59
+ const entry = table.locales[resolveKey(locale)];
60
+ if (entry === undefined)
61
+ return null;
62
+ return render(entry, select(entry), year, month, day);
63
+ }
64
+ function longDate(locale, year, month, day) {
65
+ return formatWith(locale, (entry) => entry.long, year, month, day);
66
+ }
67
+ function mediumDate(locale, year, month, day) {
68
+ return formatWith(locale, (entry) => entry.medium, year, month, day);
69
+ }
70
+ function shortDate(locale, year, month, day) {
71
+ return formatWith(locale, (entry) => entry.short, year, month, day);
72
+ }
73
+ function monthYear(locale, year, month) {
74
+ if (!Number.isInteger(year) || !Number.isInteger(month) || month < 1 || month > 12)
75
+ return null;
76
+ const entry = table.locales[resolveKey(locale)];
77
+ if (entry === undefined)
78
+ return null;
79
+ return render(entry, entry.month_year, year, month, 1);
80
+ }
81
+ function availableLocales() {
82
+ return Object.keys(table.locales).sort();
83
+ }
84
+ function isSupported(locale) {
85
+ if (table.supported.includes(locale))
86
+ return true;
87
+ return table.supported.includes(baseLanguage(locale.toLowerCase()));
88
+ }
89
+ export {
90
+ shortDate,
91
+ monthYear,
92
+ mediumDate,
93
+ longDate,
94
+ isValidDate,
95
+ isSupported,
96
+ availableLocales
97
+ };
@@ -0,0 +1,2 @@
1
+ export declare const PACKAGE_VERSION: string;
2
+ export declare const DATES_TABLE: unknown;
@@ -0,0 +1,41 @@
1
+ export interface LocaleEntry {
2
+ long: string;
3
+ medium: string;
4
+ short: string;
5
+ month_year: string;
6
+ months_long: string[];
7
+ months_short?: string[];
8
+ }
9
+ export interface DatesTable {
10
+ locales: Record<string, LocaleEntry>;
11
+ supported: string[];
12
+ fallback: string;
13
+ }
14
+ /** Whether a proleptic Gregorian calendar date exists. */
15
+ export declare function isValidDate(year: number, month: number, day: number): boolean;
16
+ /**
17
+ * Long date for a locale: `7. September 2026` (de),
18
+ * `September 7, 2026` (en). Unknown locales fall back through the base
19
+ * language to English. Returns `null` for invalid calendar dates.
20
+ */
21
+ export declare function longDate(locale: string, year: number, month: number, day: number): string | null;
22
+ /**
23
+ * Medium date for a locale: `07.09.2026` (de), `Sep 7, 2026` (en).
24
+ * Returns `null` for invalid calendar dates.
25
+ */
26
+ export declare function mediumDate(locale: string, year: number, month: number, day: number): string | null;
27
+ /**
28
+ * Short numeric date for a locale: `07.09.26` (de), `9/7/26` (en).
29
+ * Returns `null` for invalid calendar dates.
30
+ */
31
+ export declare function shortDate(locale: string, year: number, month: number, day: number): string | null;
32
+ /**
33
+ * Month and year for a correspondence dateline: `September 2026`.
34
+ * Returns `null` for an invalid month.
35
+ */
36
+ export declare function monthYear(locale: string, year: number, month: number): string | null;
37
+ /** BCP 47 locale codes with a date-format entry, sorted. */
38
+ export declare function availableLocales(): string[];
39
+ /** Whether a locale code is supported: present in the supported set
40
+ * directly or through its (lowercased) base language. */
41
+ export declare function isSupported(locale: string): boolean;
@@ -0,0 +1,41 @@
1
+ export interface LocaleEntry {
2
+ long: string;
3
+ medium: string;
4
+ short: string;
5
+ month_year: string;
6
+ months_long: string[];
7
+ months_short?: string[];
8
+ }
9
+ export interface DatesTable {
10
+ locales: Record<string, LocaleEntry>;
11
+ supported: string[];
12
+ fallback: string;
13
+ }
14
+ /** Whether a proleptic Gregorian calendar date exists. */
15
+ export declare function isValidDate(year: number, month: number, day: number): boolean;
16
+ /**
17
+ * Long date for a locale: `7. September 2026` (de),
18
+ * `September 7, 2026` (en). Unknown locales fall back through the base
19
+ * language to English. Returns `null` for invalid calendar dates.
20
+ */
21
+ export declare function longDate(locale: string, year: number, month: number, day: number): string | null;
22
+ /**
23
+ * Medium date for a locale: `07.09.2026` (de), `Sep 7, 2026` (en).
24
+ * Returns `null` for invalid calendar dates.
25
+ */
26
+ export declare function mediumDate(locale: string, year: number, month: number, day: number): string | null;
27
+ /**
28
+ * Short numeric date for a locale: `07.09.26` (de), `9/7/26` (en).
29
+ * Returns `null` for invalid calendar dates.
30
+ */
31
+ export declare function shortDate(locale: string, year: number, month: number, day: number): string | null;
32
+ /**
33
+ * Month and year for a correspondence dateline: `September 2026`.
34
+ * Returns `null` for an invalid month.
35
+ */
36
+ export declare function monthYear(locale: string, year: number, month: number): string | null;
37
+ /** BCP 47 locale codes with a date-format entry, sorted. */
38
+ export declare function availableLocales(): string[];
39
+ /** Whether a locale code is supported: present in the supported set
40
+ * directly or through its (lowercased) base language. */
41
+ export declare function isSupported(locale: string): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corbet-labs/cdate",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Deterministic locale-correct date formats for formal correspondence. Mirrors the cdate Rust crate release line.",
5
5
  "license": "FSL-1.1-ALv2",
6
6
  "publishConfig": {
@@ -9,26 +9,62 @@
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/corbet-labs/cdate.git",
12
+ "url": "git+https://github.com/corbet-labs/cdate.git",
13
13
  "directory": "js/@corbet-labs/cdate"
14
14
  },
15
15
  "type": "module",
16
- "main": "./src/index.ts",
17
- "types": "./src/index.ts",
16
+ "main": "./dist/index.cjs",
17
+ "types": "./dist/types/index.d.ts",
18
18
  "exports": {
19
- ".": "./src/index.ts"
19
+ ".": {
20
+ "import": {
21
+ "types": "./dist/types/index.d.ts",
22
+ "default": "./dist/index.js"
23
+ },
24
+ "require": {
25
+ "types": "./dist/types/index.d.cts",
26
+ "default": "./dist/index.cjs"
27
+ },
28
+ "default": "./dist/index.js"
29
+ },
30
+ "./browser": {
31
+ "types": "./dist/types/index.d.ts",
32
+ "default": "./dist/browser.js"
33
+ },
34
+ "./package.json": "./package.json"
20
35
  },
21
36
  "files": [
37
+ "dist/**/*",
22
38
  "src/**/*",
23
- "README.md"
39
+ "README.md",
40
+ "LICENSES/**/*"
24
41
  ],
25
42
  "scripts": {
26
- "prepack": "bash ./scripts/sync-assets.sh",
43
+ "prepack": "bun run build",
27
44
  "conformance": "bun ./scripts/conformance.mts",
28
- "typecheck": "tsc --noEmit -p ./tsconfig.json"
45
+ "typecheck": "tsc --noEmit -p ./tsconfig.json",
46
+ "build": "bun ./scripts/build.mjs"
29
47
  },
30
48
  "devDependencies": {
31
49
  "@types/node": "25.8.0",
32
50
  "typescript": "6.0.3"
33
- }
51
+ },
52
+ "module": "./dist/index.js",
53
+ "unpkg": "./dist/browser.js",
54
+ "jsdelivr": "./dist/browser.js",
55
+ "sideEffects": false,
56
+ "engines": {
57
+ "node": ">=20"
58
+ },
59
+ "homepage": "https://github.com/corbet-labs/cdate#readme",
60
+ "bugs": {
61
+ "url": "https://github.com/corbet-labs/cdate/issues"
62
+ },
63
+ "keywords": [
64
+ "i18n",
65
+ "correspondence",
66
+ "typescript",
67
+ "rust",
68
+ "cdate"
69
+ ]
34
70
  }
@@ -1,3 +1,3 @@
1
1
  // GENERATED by scripts/sync-assets.sh — do not edit.
2
- export const PACKAGE_VERSION = "0.1.0";
3
- export const DATES_TABLE = {"fallback": "en", "locales": {"de": {"long": "{day}. {month_long} {yyyy}", "medium": "{dd}.{mm}.{yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], "short": "{dd}.{mm}.{yy}"}, "de-AT": {"long": "{day}. {month_long} {yyyy}", "medium": "{dd}.{mm}.{yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], "short": "{dd}.{mm}.{yy}"}, "de-CH": {"long": "{day}. {month_long} {yyyy}", "medium": "{dd}.{mm}.{yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], "short": "{dd}.{mm}.{yy}"}, "en": {"long": "{month_long} {day}, {yyyy}", "medium": "{month_short} {day}, {yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "months_short": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "short": "{month}/{day}/{yy}"}, "en-GB": {"long": "{day} {month_long} {yyyy}", "medium": "{day} {month_short} {yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "months_short": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], "short": "{dd}/{mm}/{yyyy}"}, "en-US": {"long": "{month_long} {day}, {yyyy}", "medium": "{month_short} {day}, {yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "months_short": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "short": "{month}/{day}/{yy}"}}, "supported": ["de", "de-AT", "de-CH", "en", "en-GB", "en-US"]};
2
+ export const PACKAGE_VERSION: string = "0.2.0";
3
+ export const DATES_TABLE: unknown = {"fallback": "en", "locales": {"de": {"long": "{day}. {month_long} {yyyy}", "medium": "{dd}.{mm}.{yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], "short": "{dd}.{mm}.{yy}"}, "de-at": {"long": "{day}. {month_long} {yyyy}", "medium": "{dd}.{mm}.{yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], "short": "{dd}.{mm}.{yy}"}, "de-ch": {"long": "{day}. {month_long} {yyyy}", "medium": "{dd}.{mm}.{yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], "short": "{dd}.{mm}.{yy}"}, "de-li": {"long": "{day}. {month_long} {yyyy}", "medium": "{dd}.{mm}.{yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"], "short": "{dd}.{mm}.{yy}"}, "en": {"long": "{month_long} {day}, {yyyy}", "medium": "{month_short} {day}, {yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "months_short": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "short": "{month}/{day}/{yy}"}, "en-gb": {"long": "{day} {month_long} {yyyy}", "medium": "{day} {month_short} {yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "months_short": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"], "short": "{dd}/{mm}/{yyyy}"}, "en-us": {"long": "{month_long} {day}, {yyyy}", "medium": "{month_short} {day}, {yyyy}", "month_year": "{month_long} {yyyy}", "months_long": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"], "months_short": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "short": "{month}/{day}/{yy}"}}, "supported": ["de", "de-at", "de-ch", "de-li", "en", "en-gb", "en-us"]};
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  * at the repository root; `tests/vectors/*.json` is the shared conformance
7
7
  * suite.
8
8
  */
9
- import { DATES_TABLE } from './generated/tables.js';
9
+ import { DATES_TABLE } from './generated/tables.ts';
10
10
 
11
11
  export interface LocaleEntry {
12
12
  long: string;
@@ -25,25 +25,15 @@ export interface DatesTable {
25
25
 
26
26
  const table = DATES_TABLE as DatesTable;
27
27
 
28
- /** Lowercased table key → canonical BCP 47 key. Tables keep canonical keys
29
- * for compatibility; inputs resolve case-insensitively through this index. */
30
- const canonicalByLower: Record<string, string> = {};
31
- for (const key of Object.keys(table.locales)) {
32
- canonicalByLower[key.toLowerCase()] = key;
33
- }
34
-
35
28
  function baseLanguage(locale: string): string {
36
29
  return locale.split('-')[0] ?? locale;
37
30
  }
38
31
 
39
32
  function resolveKey(locale: string): string {
40
- if (Object.hasOwn(table.locales, locale)) return locale;
41
- const lowered = locale.toLowerCase();
42
- const exact = canonicalByLower[lowered];
43
- if (exact !== undefined) return exact;
44
- const base = canonicalByLower[baseLanguage(lowered)];
45
- if (base !== undefined) return base;
46
- return table.fallback;
33
+ const lower = locale.toLowerCase();
34
+ if (Object.hasOwn(table.locales, lower)) return lower;
35
+ const base = baseLanguage(lower);
36
+ return Object.hasOwn(table.locales, base) ? base : table.fallback;
47
37
  }
48
38
 
49
39
  function daysInMonth(year: number, month: number): number {