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.
- package/examples/math-calculations.cbx +128 -0
- package/examples/string-processing.cbx +105 -0
- package/package.json +1 -1
- package/runtime/src/date_utils.ts +400 -0
- package/runtime/src/file_utils.ts +414 -0
- package/runtime/src/index.ts +19 -0
- package/runtime/src/math_utils.ts +316 -0
- package/runtime/src/string_utils.ts +307 -0
|
@@ -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
|
@@ -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
|
+
}
|