mdtg 4.2.2 → 5.0.1

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
@@ -73,6 +73,47 @@ Invalid calendar dates and times, such as `310224Zfeb24` or `126155Z`, throw a
73
73
  `referenceDate` is an optional valid `Date` used only for short values that omit
74
74
  the month and year. If omitted, the current UTC month and year are used.
75
75
 
76
+ #### Temporal API
77
+
78
+ `formatTemporalMDTG` and `parseTemporalMDTG` provide a separate API for the
79
+ native Temporal objects. They require a runtime with `globalThis.Temporal`
80
+ (available by default in Node.js 26 and newer); the existing `Date` API remains
81
+ available on older runtimes. Calling either Temporal function when Temporal is
82
+ unavailable throws an `Error`.
83
+
84
+ `formatTemporalMDTG` accepts `Temporal.PlainDate`, `Temporal.PlainDateTime`, or
85
+ `Temporal.ZonedDateTime`. A `PlainDate` is formatted at midnight UTC. A
86
+ `PlainDateTime` uses the `timezone` option (a NATO letter, defaulting to `Z`)
87
+ for its wall-clock time. A `ZonedDateTime` uses its own wall-clock time and
88
+ offset; the offset must be a whole number of hours supported by an MDTG
89
+ timezone letter.
90
+
91
+ ```js
92
+ import { formatTemporalMDTG, parseTemporalMDTG } from "mdtg";
93
+
94
+ formatTemporalMDTG(Temporal.PlainDate.from("2024-08-12"));
95
+ // "12000000Zaug24"
96
+
97
+ formatTemporalMDTG(Temporal.PlainDateTime.from("2024-08-12T11:55:30"), {
98
+ timezone: "A",
99
+ });
100
+ // "12115530Aaug24"
101
+
102
+ parseTemporalMDTG("121155Aaug24", { type: "zonedDateTime" }).toString();
103
+ // "2024-08-12T11:55:00+01:00[+01:00]"
104
+
105
+ parseTemporalMDTG("121155Zaug24", { type: "plainDate" }).toString();
106
+ // "2024-08-12"
107
+ ```
108
+
109
+ Parsing requires `type`: `"plainDate"`, `"plainDateTime"`, or
110
+ `"zonedDateTime"`. The returned `PlainDate` discards the MDTG time and zone;
111
+ `PlainDateTime` retains the wall-clock date/time but discards the zone. A
112
+ `ZonedDateTime` uses the fixed offset represented by the MDTG timezone letter.
113
+ For short values, `referenceDate` can be a `Temporal.PlainDate`; if omitted,
114
+ the current UTC month and year are used. Temporal-specific tests are skipped
115
+ when the runtime does not provide `globalThis.Temporal`.
116
+
76
117
  #### Format checks
77
118
 
78
119
  `isShortFormat`, `isStandardFormat`, and `isExtendedFormat` check whether a string
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdtg",
3
- "version": "4.2.2",
3
+ "version": "5.0.1",
4
4
  "main": "src/index.js",
5
5
  "types": "src/index.d.ts",
6
6
  "type": "module",
@@ -40,12 +40,12 @@
40
40
  "description": "This package offers you methods to convert Date Objects to military date time group and vice versa",
41
41
  "devDependencies": {
42
42
  "@amiceli/vitest-cucumber": "8.0.0",
43
- "@biomejs/biome": "2.6.4",
43
+ "@biomejs/biome": "2.6.6",
44
44
  "@systemfsoftware/stryker-js": "17.0.2",
45
45
  "@systemfsoftware/stryker-js-vitest-runner": "8.1.1",
46
46
  "@vitest/coverage-v8": "5.1.0",
47
47
  "fast-check": "4.10.2",
48
- "knip": "6.44.1",
48
+ "knip": "6.44.2",
49
49
  "prettier": "3.9.10",
50
50
  "vitest": "5.1.0"
51
51
  }
package/src/index.d.ts CHANGED
@@ -1,26 +1,47 @@
1
1
  type MDTGFormat = "short" | "extended" | "standard";
2
+ type MDTGTemporalType = "plainDate" | "plainDateTime" | "zonedDateTime";
2
3
 
3
4
  declare function formatMDTG(
4
5
  date?: Date,
5
6
  options?: { form?: MDTGFormat; timezone?: string },
6
7
  ): string;
7
8
 
9
+ declare function formatTemporalMDTG(
10
+ date: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.ZonedDateTime,
11
+ options?: { form?: MDTGFormat; timezone?: string },
12
+ ): string;
13
+
8
14
  declare function isValidMDTG(mdtg: string, options?: { referenceDate?: Date }): boolean;
9
15
  declare function parseMDTG(mdtg: string, options?: { referenceDate?: Date }): Date;
10
16
 
17
+ declare function parseTemporalMDTG(
18
+ mdtg: string,
19
+ options: { type: "plainDate"; referenceDate?: Temporal.PlainDate },
20
+ ): Temporal.PlainDate;
21
+ declare function parseTemporalMDTG(
22
+ mdtg: string,
23
+ options: { type: "plainDateTime"; referenceDate?: Temporal.PlainDate },
24
+ ): Temporal.PlainDateTime;
25
+ declare function parseTemporalMDTG(
26
+ mdtg: string,
27
+ options: { type: "zonedDateTime"; referenceDate?: Temporal.PlainDate },
28
+ ): Temporal.ZonedDateTime;
29
+
11
30
  declare function isExtendedFormat(mdtg: string): boolean;
12
31
  declare function isMDTG(mdtg: string): boolean;
13
32
  declare function isShortFormat(mdtg: string): boolean;
14
33
  declare function isStandardFormat(mdtg: string): boolean;
15
34
 
16
- export type { MDTGFormat };
35
+ export type { MDTGFormat, MDTGTemporalType };
17
36
 
18
37
  export {
19
38
  formatMDTG,
39
+ formatTemporalMDTG,
20
40
  isExtendedFormat,
21
41
  isMDTG,
22
42
  isShortFormat,
23
43
  isStandardFormat,
24
44
  isValidMDTG,
25
45
  parseMDTG,
46
+ parseTemporalMDTG,
26
47
  };
package/src/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { formatMDTG } from "./format.js";
2
2
  export { isValidMDTG, parseMDTG } from "./parse.js";
3
+ export { formatTemporalMDTG, parseTemporalMDTG } from "./temporal.js";
3
4
  export {
4
5
  isExtendedFormat,
5
6
  isMDTG,
@@ -0,0 +1,131 @@
1
+ import { OFFSETS } from "./constants.js";
2
+ import { formatMDTG } from "./format.js";
3
+ import { parseMDTG } from "./parse.js";
4
+ import {
5
+ isExtendedFormat,
6
+ isShortFormat,
7
+ isStandardFormat,
8
+ validateTimezone,
9
+ } from "./validation.js";
10
+
11
+ const HOUR_IN_NANOSECONDS = 3_600_000_000_000;
12
+
13
+ const getTemporal = () => {
14
+ if (!globalThis.Temporal) {
15
+ throw new Error("The Temporal API is not available in this runtime");
16
+ }
17
+
18
+ return globalThis.Temporal;
19
+ };
20
+
21
+ const createUTCDate = (year, month, day, hours = 0, minutes = 0, seconds = 0) => {
22
+ const date = new Date(0);
23
+ date.setUTCFullYear(year, month - 1, day);
24
+ date.setUTCHours(hours, minutes, seconds, 0);
25
+ return date;
26
+ };
27
+
28
+ const getTimezoneForOffset = (offsetNanoseconds) => {
29
+ if (offsetNanoseconds % HOUR_IN_NANOSECONDS !== 0) {
30
+ throw new RangeError("MDTG timezone letters only support whole-hour offsets");
31
+ }
32
+
33
+ const offsetHours = offsetNanoseconds / HOUR_IN_NANOSECONDS;
34
+ const entry = Object.entries(OFFSETS).find(([, offset]) => offset === -offsetHours);
35
+
36
+ if (!entry) {
37
+ throw new RangeError("ZonedDateTime offset has no corresponding MDTG timezone letter");
38
+ }
39
+
40
+ return entry[0];
41
+ };
42
+
43
+ const getTimezoneOffset = (timezone) => {
44
+ const offsetHours = -OFFSETS[timezone];
45
+ const sign = offsetHours < 0 ? "-" : "+";
46
+ const hours = String(Math.abs(offsetHours)).padStart(2, "0");
47
+
48
+ return `${sign}${hours}:00`;
49
+ };
50
+
51
+ const getTimezoneFromMDTG = (value) => {
52
+ if (isShortFormat(value) || isStandardFormat(value)) return value[6].toUpperCase();
53
+ if (isExtendedFormat(value)) return value[8].toUpperCase();
54
+ return undefined;
55
+ };
56
+
57
+ export const formatTemporalMDTG = (value, { form, timezone: timezoneInput } = {}) => {
58
+ const Temporal = getTemporal();
59
+ let date;
60
+ let timezone;
61
+
62
+ if (value instanceof Temporal.PlainDate) {
63
+ const plainDate = value.withCalendar("iso8601");
64
+ timezone = "Z";
65
+ date = createUTCDate(plainDate.year, plainDate.month, plainDate.day);
66
+ } else if (value instanceof Temporal.PlainDateTime) {
67
+ const plainDateTime = value.withCalendar("iso8601");
68
+ timezone = validateTimezone(timezoneInput);
69
+ date = createUTCDate(
70
+ plainDateTime.year,
71
+ plainDateTime.month,
72
+ plainDateTime.day,
73
+ plainDateTime.hour + OFFSETS[timezone],
74
+ plainDateTime.minute,
75
+ plainDateTime.second,
76
+ );
77
+ } else if (value instanceof Temporal.ZonedDateTime) {
78
+ const zonedDateTime = value.withCalendar("iso8601");
79
+ timezone = getTimezoneForOffset(zonedDateTime.offsetNanoseconds);
80
+ date = createUTCDate(
81
+ zonedDateTime.year,
82
+ zonedDateTime.month,
83
+ zonedDateTime.day,
84
+ zonedDateTime.hour + OFFSETS[timezone],
85
+ zonedDateTime.minute,
86
+ zonedDateTime.second,
87
+ );
88
+ } else {
89
+ throw new TypeError(
90
+ "Expected a Temporal.PlainDate, Temporal.PlainDateTime, or Temporal.ZonedDateTime",
91
+ );
92
+ }
93
+
94
+ return formatMDTG(date, { form, timezone });
95
+ };
96
+
97
+ export const parseTemporalMDTG = (value, { type, referenceDate } = {}) => {
98
+ const Temporal = getTemporal();
99
+
100
+ if (!["plainDate", "plainDateTime", "zonedDateTime"].includes(type)) {
101
+ throw new TypeError('Expected type to be "plainDate", "plainDateTime", or "zonedDateTime"');
102
+ }
103
+
104
+ let parseOptions;
105
+ if (referenceDate !== undefined) {
106
+ if (!(referenceDate instanceof Temporal.PlainDate)) {
107
+ throw new TypeError("Expected referenceDate to be a Temporal.PlainDate");
108
+ }
109
+
110
+ const plainDate = referenceDate.withCalendar("iso8601");
111
+ parseOptions = {
112
+ referenceDate: createUTCDate(plainDate.year, plainDate.month, 1),
113
+ };
114
+ }
115
+
116
+ const parsedDate = parseMDTG(value, parseOptions);
117
+ const timezone = getTimezoneFromMDTG(value);
118
+ const offset = getTimezoneOffset(timezone);
119
+ const zonedDateTime = Temporal.Instant.fromEpochMilliseconds(
120
+ parsedDate.getTime(),
121
+ ).toZonedDateTimeISO(offset);
122
+
123
+ switch (type) {
124
+ case "plainDate":
125
+ return zonedDateTime.toPlainDate();
126
+ case "plainDateTime":
127
+ return zonedDateTime.toPlainDateTime();
128
+ default:
129
+ return zonedDateTime;
130
+ }
131
+ };