cobolx-2 1.2.3 → 1.2.4

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,128 @@
1
+ PROGRAM MathCalculations
2
+ /// Demonstrates COBOL-X math built-in functions
3
+ /// These extend the standard COMPUTE verb with advanced
4
+ /// numerical operations commonly needed in business logic.
5
+
6
+ CONST PI_APPROX = 3.14159265
7
+ CONST TAX_RATE = 8.25
8
+
9
+ FUNCTION demo_power() BEGIN
10
+ DISPLAY "2 ^ 10 = " + COMPUTE_POWER(2, 10)
11
+ DISPLAY "3 ^ 3 = " + COMPUTE_POWER(3, 3)
12
+ DISPLAY "9 ^ 0.5 = " + COMPUTE_POWER(9, 0.5)
13
+ DISPLAY "2 ^ -2 = " + COMPUTE_POWER(2, -2)
14
+ END-FUNCTION
15
+
16
+ FUNCTION demo_sqrt() BEGIN
17
+ DISPLAY "SQRT(144) = " + COMPUTE_SQRT(144)
18
+ DISPLAY "SQRT(2) = " + COMPUTE_SQRT(2)
19
+ DISPLAY "SQRT(0) = " + COMPUTE_SQRT(0)
20
+ END-FUNCTION
21
+
22
+ FUNCTION demo_abs() BEGIN
23
+ DISPLAY "ABS(-42) = " + COMPUTE_ABS(-42)
24
+ DISPLAY "ABS(42) = " + COMPUTE_ABS(42)
25
+ DISPLAY "ABS(0) = " + COMPUTE_ABS(0)
26
+ DISPLAY "ABS(-3.7) = " + COMPUTE_ABS(-3.7)
27
+ END-FUNCTION
28
+
29
+ FUNCTION demo_mod() BEGIN
30
+ DISPLAY "17 MOD 5 = " + COMPUTE_MOD(17, 5)
31
+ DISPLAY "20 MOD 4 = " + COMPUTE_MOD(20, 4)
32
+ DISPLAY "-7 MOD 3 = " + COMPUTE_MOD(-7, 3)
33
+ DISPLAY "100 MOD 7 = " + COMPUTE_MOD(100, 7)
34
+ END-FUNCTION
35
+
36
+ FUNCTION demo_min_max() BEGIN
37
+ SET scores = [87, 92, 65, 99, 78, 100, 55]
38
+ DISPLAY "SCORES : 87, 92, 65, 99, 78, 100, 55"
39
+ DISPLAY "MIN : " + COMPUTE_MIN(scores)
40
+ DISPLAY "MAX : " + COMPUTE_MAX(scores)
41
+ DISPLAY "MIN(3,1) : " + COMPUTE_MIN([3, 1])
42
+ DISPLAY "MAX(3,1) : " + COMPUTE_MAX([3, 1])
43
+ END-FUNCTION
44
+
45
+ FUNCTION demo_rounding() BEGIN
46
+ DISPLAY "ROUND(3.456, 2) = " + COMPUTE_ROUND(3.456, 2)
47
+ DISPLAY "ROUND(3.454, 2) = " + COMPUTE_ROUND(3.454, 2)
48
+ DISPLAY "ROUND(2.5, 0) = " + COMPUTE_ROUND(2.5, 0)
49
+ DISPLAY "ROUND(-2.5, 0) = " + COMPUTE_ROUND(-2.5, 0)
50
+ DISPLAY ""
51
+ DISPLAY "FLOOR(3.7) = " + COMPUTE_FLOOR(3.7)
52
+ DISPLAY "FLOOR(-3.2) = " + COMPUTE_FLOOR(-3.2)
53
+ DISPLAY "CEIL(3.2) = " + COMPUTE_CEIL(3.2)
54
+ DISPLAY "CEIL(-3.7) = " + COMPUTE_CEIL(-3.7)
55
+ END-FUNCTION
56
+
57
+ FUNCTION demo_sign() BEGIN
58
+ DISPLAY "SIGN(-42) = " + COMPUTE_SIGN(-42)
59
+ DISPLAY "SIGN(0) = " + COMPUTE_SIGN(0)
60
+ DISPLAY "SIGN(42) = " + COMPUTE_SIGN(42)
61
+ END-FUNCTION
62
+
63
+ FUNCTION demo_clamp() BEGIN
64
+ DISPLAY "CLAMP(15, 0, 10) = " + COMPUTE_CLAMP(15, 0, 10)
65
+ DISPLAY "CLAMP(-5, 0, 10) = " + COMPUTE_CLAMP(-5, 0, 10)
66
+ DISPLAY "CLAMP(5, 0, 10) = " + COMPUTE_CLAMP(5, 0, 10)
67
+ END-FUNCTION
68
+
69
+ FUNCTION demo_percentage() BEGIN
70
+ SET subtotal = 199.99
71
+ SET tax = COMPUTE_PERCENTAGE(subtotal, TAX_RATE)
72
+ DISPLAY "SUBTOTAL : " + subtotal
73
+ DISPLAY "TAX RATE : " + TAX_RATE + "%"
74
+ DISPLAY "TAX AMOUNT : " + COMPUTE_ROUND(tax, 2)
75
+ DISPLAY "TOTAL : " + COMPUTE_ROUND(subtotal + tax, 2)
76
+ END-FUNCTION
77
+
78
+ FUNCTION demo_business_calc() BEGIN
79
+ DISPLAY ""
80
+ DISPLAY "--- Business Calculation Example ---"
81
+ SET principal = 10000
82
+ SET rate = 5
83
+ SET years = 3
84
+ SET total = principal
85
+ SET i = 1
86
+ WHILE i <= years
87
+ SET interest = COMPUTE_PERCENTAGE(total, rate)
88
+ SET total = total + interest
89
+ DISPLAY "YEAR " + i + ": BALANCE = " + COMPUTE_ROUND(total, 2)
90
+ SET i = i + 1
91
+ END-WHILE
92
+ DISPLAY "FINAL BALANCE : " + COMPUTE_ROUND(total, 2)
93
+ DISPLAY "TOTAL INTEREST : " + COMPUTE_ROUND(total - principal, 2)
94
+ END-FUNCTION
95
+
96
+ BEGIN
97
+ DISPLAY "=== COBOL-X Math Calculations Demo ==="
98
+ DISPLAY ""
99
+ DISPLAY "--- COMPUTE-POWER ---"
100
+ demo_power()
101
+ DISPLAY ""
102
+ DISPLAY "--- COMPUTE-SQRT ---"
103
+ demo_sqrt()
104
+ DISPLAY ""
105
+ DISPLAY "--- COMPUTE-ABS ---"
106
+ demo_abs()
107
+ DISPLAY ""
108
+ DISPLAY "--- COMPUTE-MOD ---"
109
+ demo_mod()
110
+ DISPLAY ""
111
+ DISPLAY "--- COMPUTE-MIN / COMPUTE-MAX ---"
112
+ demo_min_max()
113
+ DISPLAY ""
114
+ DISPLAY "--- COMPUTE-ROUND / FLOOR / CEIL ---"
115
+ demo_rounding()
116
+ DISPLAY ""
117
+ DISPLAY "--- COMPUTE-SIGN ---"
118
+ demo_sign()
119
+ DISPLAY ""
120
+ DISPLAY "--- COMPUTE-CLAMP ---"
121
+ demo_clamp()
122
+ DISPLAY ""
123
+ DISPLAY "--- COMPUTE-PERCENTAGE ---"
124
+ demo_percentage()
125
+ demo_business_calc()
126
+ DISPLAY ""
127
+ DISPLAY "=== Demo Complete ==="
128
+ END
@@ -0,0 +1,105 @@
1
+ PROGRAM StringProcessing
2
+ /// Demonstrates COBOL-X string handling built-in functions
3
+ /// These mirror classic COBOL STRING/UNSTRING operations
4
+ /// with modern conveniences.
5
+
6
+ CONST GREETING = " HELLO, COBOL-X WORLD "
7
+
8
+ FUNCTION demo_reverse() BEGIN
9
+ SET original = "STRESSED"
10
+ SET reversed = STRING_REVERSE(original)
11
+ DISPLAY "ORIGINAL : " + original
12
+ DISPLAY "REVERSED : " + reversed
13
+ DISPLAY "CHECK : " + STRING_REVERSE(reversed)
14
+ END-FUNCTION
15
+
16
+ FUNCTION demo_case() BEGIN
17
+ SET text = "Hello COBOL-X World"
18
+ DISPLAY "ORIGINAL : " + text
19
+ DISPLAY "UPPER : " + STRING_UPPER(text)
20
+ DISPLAY "LOWER : " + STRING_LOWER(text)
21
+ END-FUNCTION
22
+
23
+ FUNCTION demo_trim() BEGIN
24
+ SET raw = " padded string "
25
+ DISPLAY "BOTH : [" + STRING_TRIM(raw) + "]"
26
+ DISPLAY "LEADING : [" + STRING_TRIM(raw, "LEADING") + "]"
27
+ DISPLAY "TRAILING : [" + STRING_TRIM(raw, "TRAILING") + "]"
28
+ END-FUNCTION
29
+
30
+ FUNCTION demo_split_and_join() BEGIN
31
+ SET csv = "NAME,AGE,CITY,COUNTRY"
32
+ DISPLAY "CSV LINE : " + csv
33
+ SET parts = STRING_SPLIT(csv, ",")
34
+ DISPLAY "SPLIT[0] : " + parts[0]
35
+ DISPLAY "SPLIT[1] : " + parts[1]
36
+ DISPLAY "SPLIT[2] : " + parts[2]
37
+ SET joined = STRING_JOIN(parts, " | ")
38
+ DISPLAY "JOINED : " + joined
39
+ END-FUNCTION
40
+
41
+ FUNCTION demo_replace() BEGIN
42
+ SET template = "Dear {NAME}, your order {ID} is confirmed."
43
+ DISPLAY "TEMPLATE : " + template
44
+ SET step1 = STRING_REPLACE(template, "{NAME}", "Alice")
45
+ SET step2 = STRING_REPLACE(step1, "{ID}", "COB-0042")
46
+ DISPLAY "RESULT : " + step2
47
+ END-FUNCTION
48
+
49
+ FUNCTION demo_contains() BEGIN
50
+ SET document = "COBOL-X is a modern COBOL-inspired language"
51
+ DISPLAY "DOC : " + document
52
+ IF STRING_CONTAINS(document, "COBOL") = 1
53
+ DISPLAY "FOUND : 'COBOL' is present"
54
+ END-IF
55
+ IF STRING_CONTAINS(document, "FORTRAN") = 0
56
+ DISPLAY "NOT FOUND: 'FORTRAN' is absent"
57
+ END-IF
58
+ END-FUNCTION
59
+
60
+ FUNCTION demo_substring_and_pad() BEGIN
61
+ SET record = "20250113ALICE "
62
+ DISPLAY "RECORD : [" + record + "]"
63
+ SET datePart = STRING_SUBSTRING(record, 1, 8)
64
+ DISPLAY "DATE : " + datePart
65
+ SET name = STRING_SUBSTRING(record, 9, 5)
66
+ DISPLAY "NAME : " + name
67
+ SET padded = STRING_PAD("123", 10, "0", "LEFT")
68
+ DISPLAY "PADDED : [" + padded + "]"
69
+ END-FUNCTION
70
+
71
+ FUNCTION demo_length() BEGIN
72
+ SET words = "COBOL-X"
73
+ DISPLAY "TEXT : " + words
74
+ DISPLAY "LENGTH : " + STRING_LENGTH(words)
75
+ END-FUNCTION
76
+
77
+ BEGIN
78
+ DISPLAY "=== COBOL-X String Processing Demo ==="
79
+ DISPLAY ""
80
+ DISPLAY "--- STRING-REVERSE ---"
81
+ demo_reverse()
82
+ DISPLAY ""
83
+ DISPLAY "--- STRING-UPPER / STRING-LOWER ---"
84
+ demo_case()
85
+ DISPLAY ""
86
+ DISPLAY "--- STRING-TRIM ---"
87
+ demo_trim()
88
+ DISPLAY ""
89
+ DISPLAY "--- STRING-SPLIT / STRING-JOIN ---"
90
+ demo_split_and_join()
91
+ DISPLAY ""
92
+ DISPLAY "--- STRING-REPLACE ---"
93
+ demo_replace()
94
+ DISPLAY ""
95
+ DISPLAY "--- STRING-CONTAINS ---"
96
+ demo_contains()
97
+ DISPLAY ""
98
+ DISPLAY "--- STRING-SUBSTRING / STRING-PAD ---"
99
+ demo_substring_and_pad()
100
+ DISPLAY ""
101
+ DISPLAY "--- STRING-LENGTH ---"
102
+ demo_length()
103
+ DISPLAY ""
104
+ DISPLAY "=== Demo Complete ==="
105
+ END
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cobolx-2",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -0,0 +1,400 @@
1
+ /**
2
+ * COBOL-X Standard Library — Date and Time Utilities
3
+ *
4
+ * Provides COBOL-style date/time functions that mirror COBOL's
5
+ * FUNCTION CURRENT-DATE, FUNCTION DATE-OF-INTEGER, and related
6
+ * date arithmetic operations. Dates follow ISO 8601 format (YYYY-MM-DD)
7
+ * internally, with COBOL display format support.
8
+ *
9
+ * @module date_utils
10
+ */
11
+
12
+ /** Valid date unit values for arithmetic operations. */
13
+ type DateUnit = "DAYS" | "MONTHS" | "YEARS" | "HOURS" | "MINUTES" | "SECONDS";
14
+
15
+ /**
16
+ * CURRENT-DATE — Returns the current date and time as a COBOL-style formatted string.
17
+ *
18
+ * COBOL equivalent: FUNCTION CURRENT-DATE.
19
+ * Returns a string in the format "YYYYMMDDHHMMSSsss" (21 characters),
20
+ * matching the COBOL standard format where:
21
+ * - YYYY = 4-digit year
22
+ * - MM = 2-digit month (01-12)
23
+ * - DD = 2-digit day (01-31)
24
+ * - HH = 2-digit hour (00-23)
25
+ * - MM = 2-digit minute (00-59)
26
+ * - SS = 2-digit second (00-59)
27
+ * - sss = 3-digit milliseconds
28
+ *
29
+ * @returns The current date/time as a 21-character COBOL-formatted string.
30
+ *
31
+ * @example
32
+ * CURRENT_DATE() // => "20250113153045123"
33
+ */
34
+ export function CURRENT_DATE(): string {
35
+ const now = new Date();
36
+ const year = now.getFullYear().toString().padStart(4, "0");
37
+ const month = (now.getMonth() + 1).toString().padStart(2, "0");
38
+ const day = now.getDate().toString().padStart(2, "0");
39
+ const hours = now.getHours().toString().padStart(2, "0");
40
+ const minutes = now.getMinutes().toString().padStart(2, "0");
41
+ const seconds = now.getSeconds().toString().padStart(2, "0");
42
+ const millis = now.getMilliseconds().toString().padStart(3, "0");
43
+ return `${year}${month}${day}${hours}${minutes}${seconds}${millis}`;
44
+ }
45
+
46
+ /**
47
+ * CURRENT-DATE-ISO — Returns the current date and time in ISO 8601 format.
48
+ *
49
+ * More modern alternative to CURRENT-DATE, returns "YYYY-MM-DDTHH:MM:SS.sssZ".
50
+ *
51
+ * @returns ISO 8601 formatted date/time string.
52
+ *
53
+ * @example
54
+ * CURRENT_DATE_ISO() // => "2025-01-13T15:30:45.123Z"
55
+ */
56
+ export function CURRENT_DATE_ISO(): string {
57
+ return new Date().toISOString();
58
+ }
59
+
60
+ /**
61
+ * DATE-ADD — Adds a specified amount of time to a date string.
62
+ *
63
+ * COBOL equivalent: COMPUTE new-date = old-date + duration.
64
+ * Supports adding days, months, years, hours, minutes, and seconds.
65
+ *
66
+ * @param dateStr - The date string (ISO 8601 format, e.g., "2025-01-13" or "2025-01-13T10:00:00Z").
67
+ * @param amount - The amount to add (must be positive; use negative to subtract).
68
+ * @param unit - The unit of time: "DAYS", "MONTHS", "YEARS", "HOURS", "MINUTES", "SECONDS".
69
+ * @returns The resulting date in ISO 8601 format.
70
+ * @throws {Error} If the date string is invalid, amount is not a number, or unit is invalid.
71
+ *
72
+ * @example
73
+ * DATE_ADD("2025-01-13", 30, "DAYS") // => "2025-02-12T00:00:00.000Z"
74
+ * DATE_ADD("2025-01-13", 1, "MONTHS") // => "2025-02-13T00:00:00.000Z"
75
+ * DATE_ADD("2025-01-13", -1, "YEARS") // => "2024-01-13T00:00:00.000Z"
76
+ */
77
+ export function DATE_ADD(dateStr: string, amount: number, unit: DateUnit): string {
78
+ if (typeof dateStr !== "string") {
79
+ throw new Error(`DATE-ADD: expected PIC X for date, received ${typeof dateStr}`);
80
+ }
81
+ if (typeof amount !== "number" || !Number.isInteger(amount)) {
82
+ throw new Error(`DATE-ADD: amount must be a whole number, received ${amount}`);
83
+ }
84
+ const validUnits: DateUnit[] = ["DAYS", "MONTHS", "YEARS", "HOURS", "MINUTES", "SECONDS"];
85
+ if (!validUnits.includes(unit)) {
86
+ throw new Error(`DATE-ADD: invalid unit '${unit}', expected one of ${validUnits.join(", ")}`);
87
+ }
88
+
89
+ const date = new Date(dateStr);
90
+ if (Number.isNaN(date.getTime())) {
91
+ throw new Error(`DATE-ADD: invalid date string '${dateStr}'`);
92
+ }
93
+
94
+ switch (unit) {
95
+ case "DAYS":
96
+ date.setDate(date.getDate() + amount);
97
+ break;
98
+ case "MONTHS":
99
+ date.setMonth(date.getMonth() + amount);
100
+ break;
101
+ case "YEARS":
102
+ date.setFullYear(date.getFullYear() + amount);
103
+ break;
104
+ case "HOURS":
105
+ date.setHours(date.getHours() + amount);
106
+ break;
107
+ case "MINUTES":
108
+ date.setMinutes(date.getMinutes() + amount);
109
+ break;
110
+ case "SECONDS":
111
+ date.setSeconds(date.getSeconds() + amount);
112
+ break;
113
+ }
114
+
115
+ return date.toISOString();
116
+ }
117
+
118
+ /**
119
+ * DATE-DIFF — Computes the difference between two dates in the specified unit.
120
+ *
121
+ * COBOL equivalent: COMPUTE days = FUNCTION INTEGER-OF-DATE(end) - FUNCTION INTEGER-OF-DATE(start).
122
+ *
123
+ * @param startDateStr - The start date string (ISO 8601 or COBOL format "YYYYMMDD").
124
+ * @param endDateStr - The end date string (ISO 8601 or COBOL format "YYYYMMDD").
125
+ * @param unit - The unit for the result: "DAYS" (default), "MONTHS", "YEARS", "HOURS", "MINUTES", "SECONDS".
126
+ * @returns The difference as a whole number (truncated toward zero).
127
+ * @throws {Error} If date strings are invalid, or if using MONTHS/YEARS with time-only dates.
128
+ *
129
+ * @example
130
+ * DATE_DIFF("2025-01-01", "2025-01-31", "DAYS") // => 30
131
+ * DATE_DIFF("2025-01-01", "2026-01-01", "YEARS") // => 1
132
+ * DATE_DIFF("2025-01-13T10:00", "2025-01-13T12:30", "HOURS") // => 2
133
+ */
134
+ export function DATE_DIFF(
135
+ startDateStr: string,
136
+ endDateStr: string,
137
+ unit: DateUnit = "DAYS"
138
+ ): number {
139
+ if (typeof startDateStr !== "string" || typeof endDateStr !== "string") {
140
+ throw new Error("DATE-DIFF: both date arguments must be PIC X (strings)");
141
+ }
142
+ const validUnits: DateUnit[] = ["DAYS", "MONTHS", "YEARS", "HOURS", "MINUTES", "SECONDS"];
143
+ if (!validUnits.includes(unit)) {
144
+ throw new Error(`DATE-DIFF: invalid unit '${unit}', expected one of ${validUnits.join(", ")}`);
145
+ }
146
+
147
+ // Support COBOL format "YYYYMMDD" by converting to ISO
148
+ const normalizeDate = (s: string): string => {
149
+ if (/^\d{8}$/.test(s)) {
150
+ return `${s.substring(0, 4)}-${s.substring(4, 6)}-${s.substring(6, 8)}`;
151
+ }
152
+ return s;
153
+ };
154
+
155
+ const start = new Date(normalizeDate(startDateStr));
156
+ const end = new Date(normalizeDate(endDateStr));
157
+
158
+ if (Number.isNaN(start.getTime())) {
159
+ throw new Error(`DATE-DIFF: invalid start date '${startDateStr}'`);
160
+ }
161
+ if (Number.isNaN(end.getTime())) {
162
+ throw new Error(`DATE-DIFF: invalid end date '${endDateStr}'`);
163
+ }
164
+
165
+ const diffMs = end.getTime() - start.getTime();
166
+
167
+ switch (unit) {
168
+ case "DAYS":
169
+ return Math.trunc(diffMs / (1000 * 60 * 60 * 24));
170
+ case "HOURS":
171
+ return Math.trunc(diffMs / (1000 * 60 * 60));
172
+ case "MINUTES":
173
+ return Math.trunc(diffMs / (1000 * 60));
174
+ case "SECONDS":
175
+ return Math.trunc(diffMs / 1000);
176
+ case "MONTHS": {
177
+ const months = (end.getFullYear() - start.getFullYear()) * 12 + (end.getMonth() - start.getMonth());
178
+ return months;
179
+ }
180
+ case "YEARS": {
181
+ return end.getFullYear() - start.getFullYear();
182
+ }
183
+ default:
184
+ return Math.trunc(diffMs / (1000 * 60 * 60 * 24));
185
+ }
186
+ }
187
+
188
+ /**
189
+ * FORMAT-DATE — Formats a date string into a specified display format.
190
+ *
191
+ * COBOL equivalent: MOVE FUNCTION CURRENT-DATE TO WS-DATE-DISPLAY with editing.
192
+ *
193
+ * Supported format tokens:
194
+ * YYYY - 4-digit year
195
+ * YY - 2-digit year
196
+ * MM - 2-digit month (01-12)
197
+ * DD - 2-digit day (01-31)
198
+ * HH - 2-digit hour (00-23)
199
+ * MM - 2-digit minute (00-59)
200
+ * SS - 2-digit second (00-59)
201
+ * MON - Abbreviated month name (Jan, Feb, etc.)
202
+ * DAY - Abbreviated day name (Mon, Tue, etc.)
203
+ *
204
+ * Note: Use "MI" for minutes to avoid ambiguity with month "MM".
205
+ *
206
+ * @param dateStr - The date string to format (ISO 8601 or COBOL "YYYYMMDD" format).
207
+ * @param format - The format string with tokens.
208
+ * @returns The formatted date string.
209
+ * @throws {Error} If the date string is invalid.
210
+ *
211
+ * @example
212
+ * FORMAT_DATE("2025-01-13", "DD/MM/YYYY") // => "13/01/2025"
213
+ * FORMAT_DATE("2025-01-13", "YYYY-MM-DD") // => "2025-01-13"
214
+ * FORMAT_DATE("20250113", "MON DD, YYYY") // => "Jan 13, 2025"
215
+ * FORMAT_DATE("2025-01-13T15:30:00", "HH:MI:SS") // => "15:30:00"
216
+ */
217
+ export function FORMAT_DATE(dateStr: string, format: string): string {
218
+ if (typeof dateStr !== "string") {
219
+ throw new Error(`FORMAT-DATE: expected PIC X for date, received ${typeof dateStr}`);
220
+ }
221
+ if (typeof format !== "string") {
222
+ throw new Error(`FORMAT-DATE: expected PIC X for format, received ${typeof format}`);
223
+ }
224
+
225
+ // Support COBOL format "YYYYMMDD" by converting to ISO
226
+ let normalizedDateStr = dateStr;
227
+ if (/^\d{8}$/.test(dateStr)) {
228
+ normalizedDateStr = `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`;
229
+ }
230
+
231
+ const date = new Date(normalizedDateStr);
232
+ if (Number.isNaN(date.getTime())) {
233
+ throw new Error(`FORMAT-DATE: invalid date string '${dateStr}'`);
234
+ }
235
+
236
+ const abbreviatedMonths = [
237
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
238
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
239
+ ];
240
+ const abbreviatedDays = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
241
+
242
+ const year = date.getFullYear().toString();
243
+ const yearShort = year.substring(2);
244
+ const month = (date.getMonth() + 1).toString().padStart(2, "0");
245
+ const day = date.getDate().toString().padStart(2, "0");
246
+ const hours = date.getHours().toString().padStart(2, "0");
247
+ const minutes = date.getMinutes().toString().padStart(2, "0");
248
+ const seconds = date.getSeconds().toString().padStart(2, "0");
249
+ const monthName = abbreviatedMonths[date.getMonth()];
250
+ const dayName = abbreviatedDays[date.getDay()];
251
+
252
+ return format
253
+ .replace(/YYYY/g, year)
254
+ .replace(/YY/g, yearShort)
255
+ .replace(/MON/g, monthName)
256
+ .replace(/DAY/g, dayName)
257
+ .replace(/MM/g, month)
258
+ .replace(/DD/g, day)
259
+ .replace(/HH/g, hours)
260
+ .replace(/MI/g, minutes)
261
+ .replace(/SS/g, seconds);
262
+ }
263
+
264
+ /**
265
+ * PARSE-DATE — Parses a date string from a known format into ISO 8601.
266
+ *
267
+ * Reverse of FORMAT-DATE. Supports common COBOL date display formats.
268
+ *
269
+ * @param dateStr - The formatted date string.
270
+ * @param format - The format that describes the input string's layout.
271
+ * @returns ISO 8601 formatted date string.
272
+ * @throws {Error} If the date cannot be parsed with the given format.
273
+ *
274
+ * @example
275
+ * PARSE_DATE("13/01/2025", "DD/MM/YYYY") // => "2025-01-13T00:00:00.000Z"
276
+ * PARSE_DATE("2025-01-13", "YYYY-MM-DD") // => "2025-01-13T00:00:00.000Z"
277
+ */
278
+ export function PARSE_DATE(dateStr: string, format: string): string {
279
+ if (typeof dateStr !== "string") {
280
+ throw new Error(`PARSE-DATE: expected PIC X for date, received ${typeof dateStr}`);
281
+ }
282
+ if (typeof format !== "string") {
283
+ throw new Error(`PARSE-DATE: expected PIC X for format, received ${typeof format}`);
284
+ }
285
+
286
+ // Build a regex pattern from the format string
287
+ const tokenMap: [RegExp, string][] = [
288
+ [/YYYY/g, "(\\d{4})"],
289
+ [/YY/g, "(\\d{2})"],
290
+ [/MON/g, "(\\w{3})"],
291
+ [/DAY/g, "(\\w{3})"],
292
+ [/MM/g, "(\\d{2})"],
293
+ [/DD/g, "(\\d{2})"],
294
+ [/HH/g, "(\\d{2})"],
295
+ [/MI/g, "(\\d{2})"],
296
+ [/SS/g, "(\\d{2})"],
297
+ ];
298
+
299
+ let regexPattern = format;
300
+ const tokenNames: string[] = [];
301
+
302
+ for (const [token, group] of tokenMap) {
303
+ const matches = regexPattern.match(token);
304
+ if (matches && matches.length > 0) {
305
+ const tokenName = matches[0];
306
+ regexPattern = regexPattern.replace(token, group);
307
+ tokenNames.push(tokenName);
308
+ }
309
+ }
310
+
311
+ // Escape remaining special characters in the format
312
+ regexPattern = regexPattern.replace(/[\/\-.: ]/g, (ch) => `\\${ch}`);
313
+
314
+ const match = dateStr.match(new RegExp(`^${regexPattern}$`));
315
+ if (!match) {
316
+ throw new Error(`PARSE-DATE: date '${dateStr}' does not match format '${format}'`);
317
+ }
318
+
319
+ const parts: Record<string, string> = {};
320
+ for (let i = 0; i < tokenNames.length; i++) {
321
+ parts[tokenNames[i]] = match[i + 1];
322
+ }
323
+
324
+ // Map month names to numbers
325
+ const monthNames: Record<string, string> = {
326
+ Jan: "01", Feb: "02", Mar: "03", Apr: "04", May: "05", Jun: "06",
327
+ Jul: "07", Aug: "08", Sep: "09", Oct: "10", Nov: "11", Dec: "12",
328
+ };
329
+
330
+ let year = parts["YYYY"] || `20${parts["YY"] || "00"}`;
331
+ let month = parts["MON"] ? monthNames[parts["MON"]] : parts["MM"] || "01";
332
+ const day = parts["DD"] || "01";
333
+ const hours = parts["HH"] || "00";
334
+ const minutes = parts["MI"] || "00";
335
+ const seconds = parts["SS"] || "00";
336
+
337
+ const isoStr = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}.000Z`;
338
+ const parsed = new Date(isoStr);
339
+ if (Number.isNaN(parsed.getTime())) {
340
+ throw new Error(`PARSE-DATE: produced invalid date from '${dateStr}' with format '${format}'`);
341
+ }
342
+
343
+ return parsed.toISOString();
344
+ }
345
+
346
+ /**
347
+ * DAY-OF-WEEK — Returns the day of the week for a given date.
348
+ *
349
+ * COBOL equivalent: FUNCTION DAY-OF-WEEK.
350
+ *
351
+ * @param dateStr - The date string (ISO 8601 or COBOL "YYYYMMDD" format).
352
+ * @returns Day number: 1=Monday through 7=Sunday (ISO standard).
353
+ * @throws {Error} If the date string is invalid.
354
+ *
355
+ * @example
356
+ * DAY_OF_WEEK("2025-01-13") // => 1 (Monday)
357
+ * DAY_OF_WEEK("2025-01-19") // => 7 (Sunday)
358
+ */
359
+ export function DAY_OF_WEEK(dateStr: string): number {
360
+ if (typeof dateStr !== "string") {
361
+ throw new Error(`DAY-OF-WEEK: expected PIC X, received ${typeof dateStr}`);
362
+ }
363
+ let normalized = dateStr;
364
+ if (/^\d{8}$/.test(dateStr)) {
365
+ normalized = `${dateStr.substring(0, 4)}-${dateStr.substring(4, 6)}-${dateStr.substring(6, 8)}`;
366
+ }
367
+ const date = new Date(normalized);
368
+ if (Number.isNaN(date.getTime())) {
369
+ throw new Error(`DAY-OF-WEEK: invalid date string '${dateStr}'`);
370
+ }
371
+ // JavaScript getDay() returns 0=Sunday, convert to ISO 1=Monday
372
+ const jsDay = date.getDay();
373
+ return jsDay === 0 ? 7 : jsDay;
374
+ }
375
+
376
+ /**
377
+ * IS-LEAP-YEAR — Determines whether a given year is a leap year.
378
+ *
379
+ * COBOL equivalent: manual calculation using DIVIDE REMAINDER.
380
+ *
381
+ * @param year - The year to check (e.g., 2024, 2025).
382
+ * @returns 1 if leap year, 0 if not.
383
+ * @throws {Error} If year is not a valid integer.
384
+ *
385
+ * @example
386
+ * IS_LEAP_YEAR(2024) // => 1
387
+ * IS_LEAP_YEAR(2025) // => 0
388
+ * IS_LEAP_YEAR(2000) // => 1
389
+ * IS_LEAP_YEAR(1900) // => 0
390
+ */
391
+ export function IS_LEAP_YEAR(year: number): 0 | 1 {
392
+ if (typeof year !== "number" || !Number.isInteger(year)) {
393
+ throw new Error(`IS-LEAP-YEAR: expected whole number for year, received ${year}`);
394
+ }
395
+ // Divisible by 4, except centuries unless divisible by 400
396
+ if (year % 4 !== 0) return 0;
397
+ if (year % 100 !== 0) return 1;
398
+ if (year % 400 === 0) return 1;
399
+ return 0;
400
+ }