ponomar-ts 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +674 -0
- package/NOTICE.md +27 -0
- package/README.md +105 -0
- package/dist/core/calendar/jdate.d.ts +52 -0
- package/dist/core/calendar/jdate.d.ts.map +1 -0
- package/dist/core/calendar/jdate.js +151 -0
- package/dist/core/calendar/jdate.js.map +1 -0
- package/dist/core/calendar/pcalendar.d.ts +21 -0
- package/dist/core/calendar/pcalendar.d.ts.map +1 -0
- package/dist/core/calendar/pcalendar.js +33 -0
- package/dist/core/calendar/pcalendar.js.map +1 -0
- package/dist/core/dsl/eval.d.ts +10 -0
- package/dist/core/dsl/eval.d.ts.map +1 -0
- package/dist/core/dsl/eval.js +83 -0
- package/dist/core/dsl/eval.js.map +1 -0
- package/dist/core/dsl/index.d.ts +4 -0
- package/dist/core/dsl/index.d.ts.map +1 -0
- package/dist/core/dsl/index.js +4 -0
- package/dist/core/dsl/index.js.map +1 -0
- package/dist/core/dsl/lexer.d.ts +14 -0
- package/dist/core/dsl/lexer.d.ts.map +1 -0
- package/dist/core/dsl/lexer.js +121 -0
- package/dist/core/dsl/lexer.js.map +1 -0
- package/dist/core/dsl/parser.d.ts +25 -0
- package/dist/core/dsl/parser.d.ts.map +1 -0
- package/dist/core/dsl/parser.js +103 -0
- package/dist/core/dsl/parser.js.map +1 -0
- package/dist/data/divineLiturgy.d.ts +3 -0
- package/dist/data/divineLiturgy.d.ts.map +1 -0
- package/dist/data/divineLiturgy.js +80 -0
- package/dist/data/divineLiturgy.js.map +1 -0
- package/dist/data/index.d.ts +6 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +8 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/menaion.d.ts +3 -0
- package/dist/data/menaion.d.ts.map +1 -0
- package/dist/data/menaion.js +18363 -0
- package/dist/data/menaion.js.map +1 -0
- package/dist/data/pentecostarion.d.ts +3 -0
- package/dist/data/pentecostarion.d.ts.map +1 -0
- package/dist/data/pentecostarion.js +4096 -0
- package/dist/data/pentecostarion.js.map +1 -0
- package/dist/data/triodion.d.ts +3 -0
- package/dist/data/triodion.d.ts.map +1 -0
- package/dist/data/triodion.js +841 -0
- package/dist/data/triodion.js.map +1 -0
- package/dist/data/types.d.ts +30 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/data/types.js +5 -0
- package/dist/data/types.js.map +1 -0
- package/dist/engine/day.d.ts +23 -0
- package/dist/engine/day.d.ts.map +1 -0
- package/dist/engine/day.js +41 -0
- package/dist/engine/day.js.map +1 -0
- package/dist/engine/index.d.ts +20 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +26 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/lookup.d.ts +7 -0
- package/dist/engine/lookup.d.ts.map +1 -0
- package/dist/engine/lookup.js +30 -0
- package/dist/engine/lookup.js.map +1 -0
- package/dist/engine/resolve.d.ts +15 -0
- package/dist/engine/resolve.d.ts.map +1 -0
- package/dist/engine/resolve.js +30 -0
- package/dist/engine/resolve.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/paschalion.d.ts +31 -0
- package/dist/paschalion.d.ts.map +1 -0
- package/dist/paschalion.js +60 -0
- package/dist/paschalion.js.map +1 -0
- package/package.json +89 -0
- package/src/core/calendar/jdate.ts +192 -0
- package/src/core/calendar/pcalendar.ts +47 -0
- package/src/core/dsl/eval.ts +94 -0
- package/src/core/dsl/index.ts +15 -0
- package/src/core/dsl/lexer.ts +121 -0
- package/src/core/dsl/parser.ts +148 -0
- package/src/data/divineLiturgy.ts +82 -0
- package/src/data/index.ts +8 -0
- package/src/data/menaion.ts +18365 -0
- package/src/data/pentecostarion.ts +4098 -0
- package/src/data/triodion.ts +843 -0
- package/src/data/types.ts +35 -0
- package/src/engine/day.ts +67 -0
- package/src/engine/index.ts +44 -0
- package/src/engine/lookup.ts +34 -0
- package/src/engine/resolve.ts +49 -0
- package/src/index.ts +17 -0
- package/src/paschalion.ts +80 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// Ported from Ponomar/JDate.java (typiconman/ponomar).
|
|
2
|
+
// The upstream Java class is a mutable Julian-calendar date backed by a
|
|
3
|
+
// Julian Day Number (JDN). This TS port is an immutable value object with the
|
|
4
|
+
// same JDN as the source of truth, plus pre-computed Y/M/D fields.
|
|
5
|
+
//
|
|
6
|
+
// Notes on differences vs upstream:
|
|
7
|
+
// * All operations return new values (no mutation).
|
|
8
|
+
// * `dayOfYear` matches upstream's zero-indexed convention (Jan 1 == 0).
|
|
9
|
+
// * `difference(a, b) = a.jdn - b.jdn` matches upstream's static method
|
|
10
|
+
// `JDate.difference(former, latter)`.
|
|
11
|
+
|
|
12
|
+
import type { CalendarDate } from "./pcalendar.ts";
|
|
13
|
+
|
|
14
|
+
/** A calendar date on the Julian calendar plus its Julian Day Number. */
|
|
15
|
+
export interface JulianDate {
|
|
16
|
+
readonly year: number;
|
|
17
|
+
readonly month: number;
|
|
18
|
+
readonly day: number;
|
|
19
|
+
readonly jdn: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as const;
|
|
23
|
+
const DAYS_IN_MONTH_LEAP = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] as const;
|
|
24
|
+
|
|
25
|
+
const MONTH_DOY = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334] as const;
|
|
26
|
+
const MONTH_DOY_LEAP = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335] as const;
|
|
27
|
+
|
|
28
|
+
/** Julian calendar leap year rule: every year divisible by 4. */
|
|
29
|
+
export function isJulianLeapYear(year: number): boolean {
|
|
30
|
+
return year % 4 === 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Maximum day number for a (Julian-calendar) month/year combination. */
|
|
34
|
+
export function daysInMonth(year: number, month: number): number {
|
|
35
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) {
|
|
36
|
+
throw new RangeError(`month must be 1..12, got ${month}`);
|
|
37
|
+
}
|
|
38
|
+
const table = isJulianLeapYear(year) ? DAYS_IN_MONTH_LEAP : DAYS_IN_MONTH;
|
|
39
|
+
return table[month - 1]!;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function assertValid(year: number, month: number, day: number): void {
|
|
43
|
+
if (!Number.isInteger(year) || year < 1) {
|
|
44
|
+
throw new RangeError(`year must be a positive integer, got ${year}`);
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isInteger(month) || month < 1 || month > 12) {
|
|
47
|
+
throw new RangeError(`month must be 1..12, got ${month}`);
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isInteger(day) || day < 1 || day > daysInMonth(year, month)) {
|
|
50
|
+
throw new RangeError(
|
|
51
|
+
`day must be 1..${daysInMonth(year, month)} for ${year}-${month}, got ${day}`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Build a `JulianDate` from Julian-calendar year/month/day. Throws
|
|
58
|
+
* `RangeError` if any field is out of range for the calendar.
|
|
59
|
+
*/
|
|
60
|
+
export function julianDate(year: number, month: number, day: number): JulianDate {
|
|
61
|
+
assertValid(year, month, day);
|
|
62
|
+
return { year, month, day, jdn: julianYmdToJdn(year, month, day) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Build a `JulianDate` from its Julian Day Number. */
|
|
66
|
+
export function julianDateFromJdn(jdn: number): JulianDate {
|
|
67
|
+
if (!Number.isInteger(jdn) || jdn < 0) {
|
|
68
|
+
throw new RangeError(`jdn must be a non-negative integer, got ${jdn}`);
|
|
69
|
+
}
|
|
70
|
+
const { year, month, day } = jdnToJulianYmd(jdn);
|
|
71
|
+
return { year, month, day, jdn };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Add `n` days (may be negative); returns a new `JulianDate`. */
|
|
75
|
+
export function addDays(date: JulianDate, n: number): JulianDate {
|
|
76
|
+
if (!Number.isInteger(n)) {
|
|
77
|
+
throw new RangeError(`n must be an integer, got ${n}`);
|
|
78
|
+
}
|
|
79
|
+
return julianDateFromJdn(date.jdn + n);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Subtract `n` days (may be negative); returns a new `JulianDate`. */
|
|
83
|
+
export function subtractDays(date: JulianDate, n: number): JulianDate {
|
|
84
|
+
return addDays(date, -n);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Signed difference in days between two dates: `a.jdn - b.jdn`. Matches
|
|
89
|
+
* upstream `JDate.difference(former, latter)`.
|
|
90
|
+
*/
|
|
91
|
+
export function difference(a: JulianDate, b: JulianDate): number {
|
|
92
|
+
return a.jdn - b.jdn;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Day of week where Sunday = 0, Monday = 1, ... Saturday = 6. */
|
|
96
|
+
export function dayOfWeek(date: JulianDate): 0 | 1 | 2 | 3 | 4 | 5 | 6 {
|
|
97
|
+
// JDN 0 was a Monday; (jdn + 1) mod 7 yields Sunday-first index.
|
|
98
|
+
return ((date.jdn + 1) % 7) as 0 | 1 | 2 | 3 | 4 | 5 | 6;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Zero-indexed day of year on the Julian calendar (Jan 1 == 0). Matches
|
|
103
|
+
* upstream `JDate.getDoy()`.
|
|
104
|
+
*/
|
|
105
|
+
export function dayOfYear(date: JulianDate): number {
|
|
106
|
+
const table = isJulianLeapYear(date.year) ? MONTH_DOY_LEAP : MONTH_DOY;
|
|
107
|
+
return table[date.month - 1]! + date.day - 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function equals(a: JulianDate, b: JulianDate): boolean {
|
|
111
|
+
return a.jdn === b.jdn;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function compare(a: JulianDate, b: JulianDate): number {
|
|
115
|
+
return a.jdn - b.jdn;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Convert a Julian-calendar date to the proleptic Gregorian calendar via
|
|
120
|
+
* Julian Day Number.
|
|
121
|
+
*/
|
|
122
|
+
export function toGregorian(date: JulianDate): CalendarDate {
|
|
123
|
+
return jdnToGregorianYmd(date.jdn);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Convert a proleptic Gregorian date to a Julian-calendar `JulianDate`. */
|
|
127
|
+
export function fromGregorian(date: CalendarDate): JulianDate {
|
|
128
|
+
return julianDateFromJdn(gregorianYmdToJdn(date.year, date.month, date.day));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Low-level JDN conversions (Meeus / Fliegel-van Flandern style, integer only)
|
|
133
|
+
|
|
134
|
+
function julianYmdToJdn(year: number, month: number, day: number): number {
|
|
135
|
+
const a = Math.floor((14 - month) / 12);
|
|
136
|
+
const y = year + 4800 - a;
|
|
137
|
+
const m = month + 12 * a - 3;
|
|
138
|
+
return (
|
|
139
|
+
day + Math.floor((153 * m + 2) / 5) + 365 * y + Math.floor(y / 4) - 32083
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function jdnToJulianYmd(jdn: number): {
|
|
144
|
+
year: number;
|
|
145
|
+
month: number;
|
|
146
|
+
day: number;
|
|
147
|
+
} {
|
|
148
|
+
const c = jdn + 32082;
|
|
149
|
+
const d = Math.floor((4 * c + 3) / 1461);
|
|
150
|
+
const e = c - Math.floor((1461 * d) / 4);
|
|
151
|
+
const m = Math.floor((5 * e + 2) / 153);
|
|
152
|
+
const day = e - Math.floor((153 * m + 2) / 5) + 1;
|
|
153
|
+
const month = m + 3 - 12 * Math.floor(m / 10);
|
|
154
|
+
const year = d - 4800 + Math.floor(m / 10);
|
|
155
|
+
return { year, month, day };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function gregorianYmdToJdn(year: number, month: number, day: number): number {
|
|
159
|
+
const a = Math.floor((14 - month) / 12);
|
|
160
|
+
const y = year + 4800 - a;
|
|
161
|
+
const m = month + 12 * a - 3;
|
|
162
|
+
return (
|
|
163
|
+
day +
|
|
164
|
+
Math.floor((153 * m + 2) / 5) +
|
|
165
|
+
365 * y +
|
|
166
|
+
Math.floor(y / 4) -
|
|
167
|
+
Math.floor(y / 100) +
|
|
168
|
+
Math.floor(y / 400) -
|
|
169
|
+
32045
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function jdnToGregorianYmd(jdn: number): CalendarDate {
|
|
174
|
+
const a = jdn + 32044;
|
|
175
|
+
const b = Math.floor((4 * a + 3) / 146097);
|
|
176
|
+
const c = a - Math.floor((146097 * b) / 4);
|
|
177
|
+
const d = Math.floor((4 * c + 3) / 1461);
|
|
178
|
+
const e = c - Math.floor((1461 * d) / 4);
|
|
179
|
+
const m = Math.floor((5 * e + 2) / 153);
|
|
180
|
+
const day = e - Math.floor((153 * m + 2) / 5) + 1;
|
|
181
|
+
const month = m + 3 - 12 * Math.floor(m / 10);
|
|
182
|
+
const year = 100 * b + d - 4800 + Math.floor(m / 10);
|
|
183
|
+
return { year, month, day };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Internal accessors reused by pcalendar.ts to avoid duplicating conversion
|
|
187
|
+
// logic. Kept `internal` in name to signal they are not part of the stable
|
|
188
|
+
// public API.
|
|
189
|
+
export const _internal = {
|
|
190
|
+
gregorianYmdToJdn,
|
|
191
|
+
jdnToGregorianYmd,
|
|
192
|
+
} as const;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Proleptic Gregorian calendar utilities. The Gregorian date shape and its
|
|
2
|
+
// small helper set live here so that `paschalion.ts` and `core/calendar/jdate.ts`
|
|
3
|
+
// can share one definition of `CalendarDate` and one JDN-based conversion path.
|
|
4
|
+
|
|
5
|
+
import { _internal } from "./jdate.ts";
|
|
6
|
+
|
|
7
|
+
/** A proleptic Gregorian calendar date (all fields 1-based). */
|
|
8
|
+
export interface CalendarDate {
|
|
9
|
+
readonly year: number;
|
|
10
|
+
readonly month: number;
|
|
11
|
+
readonly day: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Gregorian calendar offset (in days) that must be *added* to a Julian date
|
|
16
|
+
* to obtain the same civil day on the Gregorian calendar, for the given AD
|
|
17
|
+
* year. Every non-leap Gregorian century adds one day. Correct from 1583
|
|
18
|
+
* (Gregorian reform) forward.
|
|
19
|
+
*/
|
|
20
|
+
export function julianToGregorianOffset(year: number): number {
|
|
21
|
+
if (!Number.isInteger(year) || year < 1583) {
|
|
22
|
+
throw new RangeError(
|
|
23
|
+
`Gregorian conversion is only defined from 1583 onward, got ${year}`,
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
return Math.floor(year / 100) - Math.floor(year / 400) - 2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add `n` days to a Gregorian calendar date. Handles month/year boundaries.
|
|
31
|
+
* `n` may be negative.
|
|
32
|
+
*/
|
|
33
|
+
export function addDays(date: CalendarDate, n: number): CalendarDate {
|
|
34
|
+
if (!Number.isInteger(n)) {
|
|
35
|
+
throw new RangeError(`n must be an integer, got ${n}`);
|
|
36
|
+
}
|
|
37
|
+
const jdn = _internal.gregorianYmdToJdn(date.year, date.month, date.day);
|
|
38
|
+
return _internal.jdnToGregorianYmd(jdn + n);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Signed difference in days between two Gregorian dates (`a - b`). */
|
|
42
|
+
export function difference(a: CalendarDate, b: CalendarDate): number {
|
|
43
|
+
return (
|
|
44
|
+
_internal.gregorianYmdToJdn(a.year, a.month, a.day) -
|
|
45
|
+
_internal.gregorianYmdToJdn(b.year, b.month, b.day)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// Ported from Ponomar/StringOp.java (typiconman/ponomar) — evaluator.
|
|
2
|
+
//
|
|
3
|
+
// Semantics mirror upstream:
|
|
4
|
+
// * Every operator returns a `double`. Booleans are 1.0 / 0.0.
|
|
5
|
+
// * `&&`, `||`, `!` coerce their operands via `value !== 0 → true`.
|
|
6
|
+
// * `true` / `false` are reserved identifiers.
|
|
7
|
+
// * Variables come from a plain lookup table. Missing names throw.
|
|
8
|
+
// * Numeric semantics use JS number (IEEE-754 double), matching Java double.
|
|
9
|
+
|
|
10
|
+
import { type Expr, ParseError, parse } from "./parser.ts";
|
|
11
|
+
|
|
12
|
+
export type Context = Readonly<Record<string, number | boolean>>;
|
|
13
|
+
|
|
14
|
+
export class EvalError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = "EvalError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const RESERVED: Readonly<Record<string, number>> = {
|
|
22
|
+
true: 1,
|
|
23
|
+
false: 0,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function evaluate(source: string, ctx: Context): number {
|
|
27
|
+
return evalExpr(parse(source), ctx);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function evaluateBool(source: string, ctx: Context): boolean {
|
|
31
|
+
return evalExpr(parse(source), ctx) !== 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function evaluateAst(expr: Expr, ctx: Context): number {
|
|
35
|
+
return evalExpr(expr, ctx);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function evalExpr(expr: Expr, ctx: Context): number {
|
|
39
|
+
switch (expr.kind) {
|
|
40
|
+
case "number":
|
|
41
|
+
return expr.value;
|
|
42
|
+
case "ident": {
|
|
43
|
+
const reserved = RESERVED[expr.name];
|
|
44
|
+
if (reserved !== undefined) return reserved;
|
|
45
|
+
if (!Object.prototype.hasOwnProperty.call(ctx, expr.name)) {
|
|
46
|
+
throw new EvalError(
|
|
47
|
+
`Unknown variable ${JSON.stringify(expr.name)}`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
const v = ctx[expr.name];
|
|
51
|
+
if (typeof v === "boolean") return v ? 1 : 0;
|
|
52
|
+
if (typeof v !== "number" || !Number.isFinite(v)) {
|
|
53
|
+
throw new EvalError(
|
|
54
|
+
`Variable ${JSON.stringify(expr.name)} is not a finite number`,
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
}
|
|
59
|
+
case "unary": {
|
|
60
|
+
const v = evalExpr(expr.operand, ctx);
|
|
61
|
+
return expr.op === "!" ? (v !== 0 ? 0 : 1) : -v;
|
|
62
|
+
}
|
|
63
|
+
case "binary": {
|
|
64
|
+
// Short-circuit && / ||: safe here because expressions are pure.
|
|
65
|
+
if (expr.op === "&&") {
|
|
66
|
+
return evalExpr(expr.left, ctx) !== 0 && evalExpr(expr.right, ctx) !== 0
|
|
67
|
+
? 1
|
|
68
|
+
: 0;
|
|
69
|
+
}
|
|
70
|
+
if (expr.op === "||") {
|
|
71
|
+
return evalExpr(expr.left, ctx) !== 0 || evalExpr(expr.right, ctx) !== 0
|
|
72
|
+
? 1
|
|
73
|
+
: 0;
|
|
74
|
+
}
|
|
75
|
+
const l = evalExpr(expr.left, ctx);
|
|
76
|
+
const r = evalExpr(expr.right, ctx);
|
|
77
|
+
switch (expr.op) {
|
|
78
|
+
case "==": return l === r ? 1 : 0;
|
|
79
|
+
case "!=": return l !== r ? 1 : 0;
|
|
80
|
+
case "<": return l < r ? 1 : 0;
|
|
81
|
+
case "<=": return l <= r ? 1 : 0;
|
|
82
|
+
case ">": return l > r ? 1 : 0;
|
|
83
|
+
case ">=": return l >= r ? 1 : 0;
|
|
84
|
+
case "+": return l + r;
|
|
85
|
+
case "-": return l - r;
|
|
86
|
+
case "*": return l * r;
|
|
87
|
+
case "/": return l / r;
|
|
88
|
+
case "%": return l % r;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { ParseError };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export {
|
|
2
|
+
EvalError,
|
|
3
|
+
evaluate,
|
|
4
|
+
evaluateAst,
|
|
5
|
+
evaluateBool,
|
|
6
|
+
type Context,
|
|
7
|
+
} from "./eval.ts";
|
|
8
|
+
export { LexError, tokenize, type Token, type TokenKind } from "./lexer.ts";
|
|
9
|
+
export {
|
|
10
|
+
ParseError,
|
|
11
|
+
parse,
|
|
12
|
+
type BinaryOp,
|
|
13
|
+
type Expr,
|
|
14
|
+
type UnaryOp,
|
|
15
|
+
} from "./parser.ts";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Ported from Ponomar/StringOp.java (typiconman/ponomar) — tokenizer.
|
|
2
|
+
//
|
|
3
|
+
// The upstream Java `StringOp.eval` is a recursive text-splitting parser.
|
|
4
|
+
// This port tokenises first and then runs a Pratt parser, which yields the
|
|
5
|
+
// same semantics for well-formed expressions but tolerates arbitrary
|
|
6
|
+
// whitespace (upstream requires spaces around binary operators due to its
|
|
7
|
+
// `substring(0, n - 1)` / `substring(n + 2)` slicing).
|
|
8
|
+
|
|
9
|
+
export type TokenKind =
|
|
10
|
+
| "NUMBER"
|
|
11
|
+
| "IDENT"
|
|
12
|
+
| "LP"
|
|
13
|
+
| "RP"
|
|
14
|
+
| "PLUS"
|
|
15
|
+
| "MINUS"
|
|
16
|
+
| "STAR"
|
|
17
|
+
| "SLASH"
|
|
18
|
+
| "PERCENT"
|
|
19
|
+
| "EQ"
|
|
20
|
+
| "NE"
|
|
21
|
+
| "LT"
|
|
22
|
+
| "LE"
|
|
23
|
+
| "GT"
|
|
24
|
+
| "GE"
|
|
25
|
+
| "AND"
|
|
26
|
+
| "OR"
|
|
27
|
+
| "NOT"
|
|
28
|
+
| "EOF";
|
|
29
|
+
|
|
30
|
+
export interface Token {
|
|
31
|
+
readonly kind: TokenKind;
|
|
32
|
+
readonly text: string;
|
|
33
|
+
/** 0-based source offset where this token starts. */
|
|
34
|
+
readonly pos: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class LexError extends Error {
|
|
38
|
+
readonly source: string;
|
|
39
|
+
readonly pos: number;
|
|
40
|
+
constructor(message: string, source: string, pos: number) {
|
|
41
|
+
super(`${message} at position ${pos} in ${JSON.stringify(source)}`);
|
|
42
|
+
this.name = "LexError";
|
|
43
|
+
this.source = source;
|
|
44
|
+
this.pos = pos;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const SINGLE: Readonly<Record<string, TokenKind>> = {
|
|
49
|
+
"(": "LP",
|
|
50
|
+
")": "RP",
|
|
51
|
+
"+": "PLUS",
|
|
52
|
+
"-": "MINUS",
|
|
53
|
+
"*": "STAR",
|
|
54
|
+
"/": "SLASH",
|
|
55
|
+
"%": "PERCENT",
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export function tokenize(source: string): Token[] {
|
|
59
|
+
const out: Token[] = [];
|
|
60
|
+
let i = 0;
|
|
61
|
+
const n = source.length;
|
|
62
|
+
while (i < n) {
|
|
63
|
+
const c = source[i] as string;
|
|
64
|
+
if (c === " " || c === "\t" || c === "\n" || c === "\r") {
|
|
65
|
+
i++;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const start = i;
|
|
69
|
+
const two = source.slice(i, i + 2);
|
|
70
|
+
switch (two) {
|
|
71
|
+
case "==": out.push({ kind: "EQ", text: two, pos: start }); i += 2; continue;
|
|
72
|
+
case "!=": out.push({ kind: "NE", text: two, pos: start }); i += 2; continue;
|
|
73
|
+
case "<=": out.push({ kind: "LE", text: two, pos: start }); i += 2; continue;
|
|
74
|
+
case ">=": out.push({ kind: "GE", text: two, pos: start }); i += 2; continue;
|
|
75
|
+
case "&&": out.push({ kind: "AND", text: two, pos: start }); i += 2; continue;
|
|
76
|
+
case "||": out.push({ kind: "OR", text: two, pos: start }); i += 2; continue;
|
|
77
|
+
}
|
|
78
|
+
if (c === "<") { out.push({ kind: "LT", text: "<", pos: start }); i++; continue; }
|
|
79
|
+
if (c === ">") { out.push({ kind: "GT", text: ">", pos: start }); i++; continue; }
|
|
80
|
+
if (c === "!") { out.push({ kind: "NOT", text: "!", pos: start }); i++; continue; }
|
|
81
|
+
const single = SINGLE[c];
|
|
82
|
+
if (single !== undefined) {
|
|
83
|
+
out.push({ kind: single, text: c, pos: start });
|
|
84
|
+
i++;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isDigit(c) || (c === "." && isDigit(source[i + 1] ?? ""))) {
|
|
88
|
+
let j = i;
|
|
89
|
+
while (j < n && isDigit(source[j] as string)) j++;
|
|
90
|
+
if (source[j] === ".") {
|
|
91
|
+
j++;
|
|
92
|
+
while (j < n && isDigit(source[j] as string)) j++;
|
|
93
|
+
}
|
|
94
|
+
out.push({ kind: "NUMBER", text: source.slice(i, j), pos: start });
|
|
95
|
+
i = j;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (isIdentStart(c)) {
|
|
99
|
+
let j = i + 1;
|
|
100
|
+
while (j < n && isIdentPart(source[j] as string)) j++;
|
|
101
|
+
out.push({ kind: "IDENT", text: source.slice(i, j), pos: start });
|
|
102
|
+
i = j;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
throw new LexError(`Unexpected character ${JSON.stringify(c)}`, source, i);
|
|
106
|
+
}
|
|
107
|
+
out.push({ kind: "EOF", text: "", pos: n });
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function isDigit(c: string): boolean {
|
|
112
|
+
return c >= "0" && c <= "9";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isIdentStart(c: string): boolean {
|
|
116
|
+
return (c >= "a" && c <= "z") || (c >= "A" && c <= "Z") || c === "_";
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isIdentPart(c: string): boolean {
|
|
120
|
+
return isIdentStart(c) || isDigit(c);
|
|
121
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Ported from Ponomar/StringOp.java (typiconman/ponomar) — Pratt parser.
|
|
2
|
+
//
|
|
3
|
+
// Precedence (low → high) matches the upstream recursive text-splitting
|
|
4
|
+
// evaluator, which splits on operators in exactly this order:
|
|
5
|
+
// ||, &&, == / !=, < / <= / > / >=, + / -, * / / / %, unary ! / -.
|
|
6
|
+
|
|
7
|
+
import { type Token, type TokenKind, tokenize } from "./lexer.ts";
|
|
8
|
+
|
|
9
|
+
export type BinaryOp =
|
|
10
|
+
| "||"
|
|
11
|
+
| "&&"
|
|
12
|
+
| "=="
|
|
13
|
+
| "!="
|
|
14
|
+
| "<"
|
|
15
|
+
| "<="
|
|
16
|
+
| ">"
|
|
17
|
+
| ">="
|
|
18
|
+
| "+"
|
|
19
|
+
| "-"
|
|
20
|
+
| "*"
|
|
21
|
+
| "/"
|
|
22
|
+
| "%";
|
|
23
|
+
|
|
24
|
+
export type UnaryOp = "!" | "-";
|
|
25
|
+
|
|
26
|
+
export type Expr =
|
|
27
|
+
| { readonly kind: "number"; readonly value: number }
|
|
28
|
+
| { readonly kind: "ident"; readonly name: string }
|
|
29
|
+
| { readonly kind: "unary"; readonly op: UnaryOp; readonly operand: Expr }
|
|
30
|
+
| {
|
|
31
|
+
readonly kind: "binary";
|
|
32
|
+
readonly op: BinaryOp;
|
|
33
|
+
readonly left: Expr;
|
|
34
|
+
readonly right: Expr;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export class ParseError extends Error {
|
|
38
|
+
readonly source: string;
|
|
39
|
+
readonly pos: number;
|
|
40
|
+
constructor(message: string, source: string, pos: number) {
|
|
41
|
+
super(`${message} at position ${pos} in ${JSON.stringify(source)}`);
|
|
42
|
+
this.name = "ParseError";
|
|
43
|
+
this.source = source;
|
|
44
|
+
this.pos = pos;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const INFIX: Partial<Record<TokenKind, { prec: number; op: BinaryOp }>> = {
|
|
49
|
+
OR: { prec: 1, op: "||" },
|
|
50
|
+
AND: { prec: 2, op: "&&" },
|
|
51
|
+
EQ: { prec: 3, op: "==" },
|
|
52
|
+
NE: { prec: 3, op: "!=" },
|
|
53
|
+
LT: { prec: 4, op: "<" },
|
|
54
|
+
LE: { prec: 4, op: "<=" },
|
|
55
|
+
GT: { prec: 4, op: ">" },
|
|
56
|
+
GE: { prec: 4, op: ">=" },
|
|
57
|
+
PLUS: { prec: 5, op: "+" },
|
|
58
|
+
MINUS: { prec: 5, op: "-" },
|
|
59
|
+
STAR: { prec: 6, op: "*" },
|
|
60
|
+
SLASH: { prec: 6, op: "/" },
|
|
61
|
+
PERCENT: { prec: 6, op: "%" },
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const UNARY_PREC = 7;
|
|
65
|
+
|
|
66
|
+
export function parse(source: string): Expr {
|
|
67
|
+
const tokens = tokenize(source);
|
|
68
|
+
const p = new Parser(source, tokens);
|
|
69
|
+
const expr = p.parseExpr(0);
|
|
70
|
+
p.expect("EOF");
|
|
71
|
+
return expr;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class Parser {
|
|
75
|
+
private i = 0;
|
|
76
|
+
private readonly source: string;
|
|
77
|
+
private readonly tokens: readonly Token[];
|
|
78
|
+
constructor(source: string, tokens: readonly Token[]) {
|
|
79
|
+
this.source = source;
|
|
80
|
+
this.tokens = tokens;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
parseExpr(minPrec: number): Expr {
|
|
84
|
+
let left = this.parseUnary();
|
|
85
|
+
while (true) {
|
|
86
|
+
const tok = this.peek();
|
|
87
|
+
const inf = INFIX[tok.kind];
|
|
88
|
+
if (inf === undefined || inf.prec < minPrec) break;
|
|
89
|
+
this.i++;
|
|
90
|
+
const right = this.parseExpr(inf.prec + 1);
|
|
91
|
+
left = { kind: "binary", op: inf.op, left, right };
|
|
92
|
+
}
|
|
93
|
+
return left;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private parseUnary(): Expr {
|
|
97
|
+
const tok = this.peek();
|
|
98
|
+
if (tok.kind === "NOT") {
|
|
99
|
+
this.i++;
|
|
100
|
+
return { kind: "unary", op: "!", operand: this.parseExpr(UNARY_PREC) };
|
|
101
|
+
}
|
|
102
|
+
if (tok.kind === "MINUS") {
|
|
103
|
+
this.i++;
|
|
104
|
+
return { kind: "unary", op: "-", operand: this.parseExpr(UNARY_PREC) };
|
|
105
|
+
}
|
|
106
|
+
return this.parsePrimary();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private parsePrimary(): Expr {
|
|
110
|
+
const tok = this.peek();
|
|
111
|
+
if (tok.kind === "NUMBER") {
|
|
112
|
+
this.i++;
|
|
113
|
+
return { kind: "number", value: Number(tok.text) };
|
|
114
|
+
}
|
|
115
|
+
if (tok.kind === "IDENT") {
|
|
116
|
+
this.i++;
|
|
117
|
+
return { kind: "ident", name: tok.text };
|
|
118
|
+
}
|
|
119
|
+
if (tok.kind === "LP") {
|
|
120
|
+
this.i++;
|
|
121
|
+
const inner = this.parseExpr(0);
|
|
122
|
+
this.expect("RP");
|
|
123
|
+
return inner;
|
|
124
|
+
}
|
|
125
|
+
throw new ParseError(
|
|
126
|
+
`Expected expression, got ${tok.kind} ${JSON.stringify(tok.text)}`,
|
|
127
|
+
this.source,
|
|
128
|
+
tok.pos,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private peek(): Token {
|
|
133
|
+
return this.tokens[this.i] as Token;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
expect(kind: TokenKind): Token {
|
|
137
|
+
const tok = this.peek();
|
|
138
|
+
if (tok.kind !== kind) {
|
|
139
|
+
throw new ParseError(
|
|
140
|
+
`Expected ${kind}, got ${tok.kind} ${JSON.stringify(tok.text)}`,
|
|
141
|
+
this.source,
|
|
142
|
+
tok.pos,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
this.i++;
|
|
146
|
+
return tok;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/codegen — DO NOT EDIT.
|
|
2
|
+
// Source: vendor/ponomar/Ponomar/languages/xml (git submodule).
|
|
3
|
+
// Regenerate with `npm run codegen`.
|
|
4
|
+
|
|
5
|
+
import type { Command } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
export const DIVINE_LITURGY_COMMANDS: ReadonlyArray<Command> = [
|
|
8
|
+
{
|
|
9
|
+
"name": "Transfer",
|
|
10
|
+
"value": "nday >= 52 || nday < -55",
|
|
11
|
+
"cmd": "GS == 1",
|
|
12
|
+
"comment": "This determines whether the sequential readings may be skipped or transferred."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "Transfer",
|
|
16
|
+
"value": "nday >= 52 || nday < -55",
|
|
17
|
+
"cmd": "GS == 0",
|
|
18
|
+
"comment": "This determines whether the sequential readings may be skipped or transferred."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "Suppress",
|
|
22
|
+
"value": "doy == 4 && dow != 0 && dow != 6",
|
|
23
|
+
"cmd": "GS == 1",
|
|
24
|
+
"comment": "Eve of Theophany can have seq. readings on Sat. (see Table 25b, 2019) and on Sun. (see Table 24b, 2087)"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "Suppress",
|
|
28
|
+
"value": "doy == 357 && dow != 6",
|
|
29
|
+
"cmd": "GS == 1",
|
|
30
|
+
"comment": "Eve of Nativity can have seq. readings on Sat. (see Table 27a, 2084)"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "Suppress",
|
|
34
|
+
"value": "(doy == 358 || doy == 5)",
|
|
35
|
+
"comment": "Nativity and Theophany never have seq. readings"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "Suppress",
|
|
39
|
+
"value": "(doy == 256 || doy == 217) && dow == 0",
|
|
40
|
+
"comment": "Suppress seq. readings if Exaltation or Transfiguration falls on Sunday"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "Class3Transfers",
|
|
44
|
+
"value": "!(doy == 358 || doy == 357 || doy == 4 || doy == 5 || doy == 256 || doy == 217) && ((dRank >= 5 && dow != 0) || dRank == 8)",
|
|
45
|
+
"cmd": "GS == 1 || GS == 0"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "Class3Transfers",
|
|
49
|
+
"value": "(doy == 3 && dow == 5) || (doy == 356 && dow == 5)",
|
|
50
|
+
"cmd": "GS == 1 || GS == 0",
|
|
51
|
+
"comment": "There is no Liturgy on Friday when the Royal Hours occur. The readings are moved to Thursday."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"name": "Class3Transfers",
|
|
55
|
+
"value": "(doy == 256 || doy == 217) && dow != 0",
|
|
56
|
+
"comment": "Exaltation and Transfiguration readings are only transferred if it's not a Sunday"
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"name": "TransferRulesB",
|
|
60
|
+
"value": "dow != 0 && !(nday >= -63 && nday <= 0) && doy != 338",
|
|
61
|
+
"cmd": "GS == 1",
|
|
62
|
+
"comment": "This determines the rules used to transfer the readings to the previous day. Note that St. Nicholas's day has a special set of rules"
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "TransferRulesF",
|
|
66
|
+
"value": "!(nday >= -63 && nday <= 0) && (dow == 2 && doy != 339) || ((doy == 340 && (dow != 0 && dow != 2)))",
|
|
67
|
+
"cmd": "GS == 1",
|
|
68
|
+
"comment": "This determines the rules used to transfer the readings to the next day (forwards). Note that St. Nicholas's day has a special set of rules."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"name": "TransferRulesB",
|
|
72
|
+
"value": "dow != 0 && !(nday >= -63 && nday <= 0) && doy != 338",
|
|
73
|
+
"cmd": "GS == 0",
|
|
74
|
+
"comment": "This determines the rules used to transfer the readings to the previous day. Note that St. Nicholas's day has a special set of rules"
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
"name": "TransferRulesF",
|
|
78
|
+
"value": "!(nday >= -63 && nday <= 0) && (dow == 2 && doy != 339) || ((doy == 340 && (dow != 0 && dow != 2)))",
|
|
79
|
+
"cmd": "GS == 0",
|
|
80
|
+
"comment": "This determines the rules used to transfer the readings to the next day (forwards). Note that St. Nicholas's day has a special set of rules."
|
|
81
|
+
}
|
|
82
|
+
] as const;
|