littlewing 0.9.5 → 1.1.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/README.md +213 -129
- package/dist/index.d.ts +237 -399
- package/dist/index.js +998 -975
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -3,19 +3,19 @@
|
|
|
3
3
|
A minimal, high-performance arithmetic expression language for JavaScript. Pure numbers, zero dependencies, built for the browser.
|
|
4
4
|
|
|
5
5
|
```typescript
|
|
6
|
-
import {
|
|
6
|
+
import { evaluate, defaultContext } from "littlewing";
|
|
7
7
|
|
|
8
8
|
// Simple arithmetic
|
|
9
|
-
|
|
9
|
+
evaluate("2 + 3 * 4"); // → 14
|
|
10
10
|
|
|
11
11
|
// Variables and functions
|
|
12
|
-
|
|
12
|
+
evaluate("radius = 5; area = 3.14159 * radius ^ 2", defaultContext); // → 78.54
|
|
13
13
|
|
|
14
14
|
// Date arithmetic with timestamps
|
|
15
|
-
|
|
15
|
+
evaluate("deadline = NOW() + FROM_DAYS(7)", defaultContext); // → timestamp 7 days from now
|
|
16
16
|
|
|
17
17
|
// Conditional logic
|
|
18
|
-
|
|
18
|
+
evaluate("score = 85; grade = score >= 90 ? 100 : 90", {
|
|
19
19
|
variables: { score: 85 },
|
|
20
20
|
}); // → 90
|
|
21
21
|
```
|
|
@@ -29,7 +29,7 @@ execute("score = 85; grade = score >= 90 ? 100 : 90", {
|
|
|
29
29
|
- **Timestamp arithmetic** - Built-in date/time functions using numeric timestamps
|
|
30
30
|
- **Extensible** - Add custom functions and variables via context
|
|
31
31
|
- **Type-safe** - Full TypeScript support with strict types
|
|
32
|
-
- **Excellent test coverage** -
|
|
32
|
+
- **Excellent test coverage** - 502 tests with 99.45% function coverage, 98.57% line coverage
|
|
33
33
|
|
|
34
34
|
## Installation
|
|
35
35
|
|
|
@@ -42,77 +42,77 @@ npm install littlewing
|
|
|
42
42
|
### Basic Usage
|
|
43
43
|
|
|
44
44
|
```typescript
|
|
45
|
-
import {
|
|
45
|
+
import { evaluate } from "littlewing";
|
|
46
46
|
|
|
47
47
|
// Arithmetic expressions
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
evaluate("2 + 3 * 4"); // → 14
|
|
49
|
+
evaluate("10 ^ 2"); // → 100
|
|
50
|
+
evaluate("17 % 5"); // → 2
|
|
51
51
|
|
|
52
52
|
// Variables
|
|
53
|
-
|
|
53
|
+
evaluate("x = 10; y = 20; x + y"); // → 30
|
|
54
54
|
|
|
55
55
|
// Comparisons (return 1 for true, 0 for false)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
evaluate("5 > 3"); // → 1
|
|
57
|
+
evaluate("10 == 10"); // → 1
|
|
58
|
+
evaluate("2 != 2"); // → 0
|
|
59
59
|
|
|
60
60
|
// Logical operators
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
61
|
+
evaluate("!0"); // → 1 (NOT)
|
|
62
|
+
evaluate("1 && 1"); // → 1 (AND)
|
|
63
|
+
evaluate("0 || 1"); // → 1 (OR)
|
|
64
|
+
evaluate("!(5 > 10)"); // → 1 (negates comparison)
|
|
65
65
|
|
|
66
66
|
// Ternary conditionals
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
evaluate("age >= 18 ? 100 : 0", { variables: { age: 21 } }); // → 100
|
|
68
|
+
evaluate("!isBlocked ? 100 : 0", { variables: { isBlocked: 0 } }); // → 100
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
### With Built-in Functions
|
|
72
72
|
|
|
73
73
|
```typescript
|
|
74
|
-
import {
|
|
74
|
+
import { evaluate, defaultContext } from "littlewing";
|
|
75
75
|
|
|
76
76
|
// Math functions
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
evaluate("ABS(-42)", defaultContext); // → 42
|
|
78
|
+
evaluate("SQRT(16)", defaultContext); // → 4
|
|
79
|
+
evaluate("MAX(3, 7, 2)", defaultContext); // → 7
|
|
80
80
|
|
|
81
81
|
// Current timestamp
|
|
82
|
-
|
|
82
|
+
evaluate("NOW()", defaultContext); // → 1704067200000
|
|
83
83
|
|
|
84
84
|
// Date arithmetic
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
evaluate("NOW() + FROM_DAYS(7)", defaultContext); // → timestamp 7 days from now
|
|
86
|
+
evaluate("tomorrow = NOW() + FROM_DAYS(1)", defaultContext); // → tomorrow's timestamp
|
|
87
87
|
|
|
88
88
|
// Extract date components
|
|
89
89
|
const ctx = { ...defaultContext, variables: { ts: Date.now() } };
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
evaluate("GET_YEAR(ts)", ctx); // → 2024
|
|
91
|
+
evaluate("GET_MONTH(ts)", ctx); // → 11
|
|
92
|
+
evaluate("GET_DAY(ts)", ctx); // → 6
|
|
93
93
|
|
|
94
94
|
// Calculate time differences
|
|
95
95
|
const ts1 = Date.now();
|
|
96
96
|
const ts2 = ts1 + 1000 * 60 * 60 * 5; // 5 hours later
|
|
97
97
|
const context = { ...defaultContext, variables: { ts1, ts2 } };
|
|
98
|
-
|
|
98
|
+
evaluate("DIFFERENCE_IN_HOURS(ts1, ts2)", context); // → 5
|
|
99
99
|
|
|
100
100
|
// Date arithmetic and comparisons
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
101
|
+
evaluate("ADD_DAYS(NOW(), 7)", defaultContext); // → 7 days from now
|
|
102
|
+
evaluate("START_OF_DAY(NOW())", defaultContext); // → today at 00:00:00.000
|
|
103
|
+
evaluate("IS_WEEKEND(NOW())", defaultContext); // → 1 if today is Sat/Sun, else 0
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
### Custom Functions and Variables
|
|
107
107
|
|
|
108
108
|
```typescript
|
|
109
|
-
import {
|
|
109
|
+
import { evaluate } from "littlewing";
|
|
110
110
|
|
|
111
111
|
const context = {
|
|
112
112
|
functions: {
|
|
113
|
-
// Custom functions
|
|
114
|
-
|
|
115
|
-
|
|
113
|
+
// Custom functions should use UPPERCASE naming (like built-in functions)
|
|
114
|
+
FAHRENHEIT: (celsius: number) => (celsius * 9) / 5 + 32,
|
|
115
|
+
DISCOUNT: (price: number, percent: number) => price * (1 - percent / 100),
|
|
116
116
|
},
|
|
117
117
|
variables: {
|
|
118
118
|
pi: 3.14159,
|
|
@@ -120,9 +120,9 @@ const context = {
|
|
|
120
120
|
},
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
evaluate("FAHRENHEIT(20)", context); // → 68
|
|
124
|
+
evaluate("DISCOUNT(100, 15)", context); // → 85
|
|
125
|
+
evaluate("100 * (1 + taxRate)", context); // → 108
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
### External Variables Override Script Defaults
|
|
@@ -132,11 +132,11 @@ execute("100 * (1 + taxRate)", context); // → 108
|
|
|
132
132
|
const formula = "multiplier = 2; value = 100; value * multiplier";
|
|
133
133
|
|
|
134
134
|
// Without external variables: uses script defaults
|
|
135
|
-
|
|
135
|
+
evaluate(formula); // → 200
|
|
136
136
|
|
|
137
137
|
// External variables override script assignments
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
evaluate(formula, { variables: { multiplier: 3 } }); // → 300
|
|
139
|
+
evaluate(formula, { variables: { value: 50 } }); // → 100
|
|
140
140
|
|
|
141
141
|
// Useful for configurable formulas
|
|
142
142
|
const pricing = `
|
|
@@ -146,9 +146,9 @@ const pricing = `
|
|
|
146
146
|
finalPrice = basePrice * (1 - discount) * (1 + taxRate)
|
|
147
147
|
`;
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
evaluate(pricing); // → 108 (uses all defaults)
|
|
150
|
+
evaluate(pricing, { variables: { discount: 0.1 } }); // → 97.2 (10% discount)
|
|
151
|
+
evaluate(pricing, { variables: { basePrice: 200, discount: 0.2 } }); // → 172.8
|
|
152
152
|
```
|
|
153
153
|
|
|
154
154
|
## Language Reference
|
|
@@ -159,51 +159,55 @@ For complete language documentation including all operators, functions, and exam
|
|
|
159
159
|
|
|
160
160
|
### Main Functions
|
|
161
161
|
|
|
162
|
-
#### `
|
|
162
|
+
#### `evaluate(input: string | ASTNode, context?: ExecutionContext): number`
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
Evaluate an expression or AST and return the result. Accepts either a source string or a pre-parsed AST node.
|
|
165
165
|
|
|
166
166
|
```typescript
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
//
|
|
172
|
-
const ast =
|
|
173
|
-
|
|
174
|
-
|
|
167
|
+
// Evaluate source string directly
|
|
168
|
+
evaluate("2 + 2"); // → 4
|
|
169
|
+
evaluate("ABS(-5)", { functions: { ABS: Math.abs } }); // → 5
|
|
170
|
+
|
|
171
|
+
// Evaluate pre-parsed AST (useful for parse-once, evaluate-many scenarios)
|
|
172
|
+
const ast = parse("2 + 2");
|
|
173
|
+
evaluate(ast); // → 4
|
|
174
|
+
evaluate(ast); // → 4 (no re-parsing)
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
#### `
|
|
177
|
+
#### `parse(source: string): ASTNode`
|
|
178
178
|
|
|
179
|
-
Parse source into an Abstract Syntax Tree without
|
|
179
|
+
Parse source into an Abstract Syntax Tree without evaluating. Useful for parse-once, execute-many scenarios.
|
|
180
180
|
|
|
181
181
|
```typescript
|
|
182
|
-
const ast =
|
|
182
|
+
const ast = parse("2 + 3 * 4");
|
|
183
183
|
|
|
184
|
-
//
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
// Evaluate multiple times with different contexts (no re-parsing)
|
|
185
|
+
evaluate(ast); // → 14
|
|
186
|
+
evaluate(ast, {
|
|
187
187
|
variables: {
|
|
188
188
|
/* ... */
|
|
189
189
|
},
|
|
190
190
|
}); // → 14 (with context)
|
|
191
191
|
|
|
192
|
-
// Or use with
|
|
192
|
+
// Or use with optimize() function
|
|
193
193
|
const optimized = optimize(ast);
|
|
194
194
|
```
|
|
195
195
|
|
|
196
196
|
#### `optimize(node: ASTNode): ASTNode`
|
|
197
197
|
|
|
198
|
-
Optimize an AST
|
|
198
|
+
Optimize an AST with constant folding and dead code elimination. Safe for use with external variables.
|
|
199
199
|
|
|
200
200
|
```typescript
|
|
201
|
-
const ast =
|
|
201
|
+
const ast = parse("2 + 3 * 4");
|
|
202
202
|
const optimized = optimize(ast); // → NumberLiteral(14)
|
|
203
203
|
|
|
204
204
|
// Variables are NOT folded (can be overridden by context)
|
|
205
|
-
const ast2 =
|
|
205
|
+
const ast2 = parse("x = 5; x + 10");
|
|
206
206
|
const opt2 = optimize(ast2); // Still has variable reference
|
|
207
|
+
|
|
208
|
+
// Dead code elimination removes unused variables
|
|
209
|
+
const ast3 = parse("x = 10; y = 20; z = x * 20");
|
|
210
|
+
const opt3 = optimize(ast3); // Removes unused y assignment
|
|
207
211
|
```
|
|
208
212
|
|
|
209
213
|
#### `generate(node: ASTNode): string`
|
|
@@ -211,10 +215,36 @@ const opt2 = optimize(ast2); // Still has variable reference
|
|
|
211
215
|
Convert AST back to source code.
|
|
212
216
|
|
|
213
217
|
```typescript
|
|
214
|
-
const ast =
|
|
218
|
+
const ast = parse("2 + 3 * 4");
|
|
215
219
|
generate(ast); // → "2 + 3 * 4"
|
|
216
220
|
```
|
|
217
221
|
|
|
222
|
+
#### `humanize(node: ASTNode, options?: HumanizeOptions): string`
|
|
223
|
+
|
|
224
|
+
Convert AST to human-readable English text.
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
const ast = parse("x + 10");
|
|
228
|
+
humanize(ast); // → "x plus 10"
|
|
229
|
+
|
|
230
|
+
// With HTML output
|
|
231
|
+
humanize(ast, { html: true }); // → "<span class='identifier'>x</span> plus <span class='number'>10</span>"
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### `extractInputVariables(ast: ASTNode): string[]`
|
|
235
|
+
|
|
236
|
+
Extract all variable identifiers that are assigned to constant values (literals, constant expressions, or function calls with constant arguments). Variables computed from other variables are excluded.
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
const ast = parse("price = 100; tax = price * 0.08");
|
|
240
|
+
extractInputVariables(ast); // → ["price"]
|
|
241
|
+
// 'tax' is excluded because it's computed from 'price'
|
|
242
|
+
|
|
243
|
+
const ast2 = parse("x = 10; y = 20; sum = x + y");
|
|
244
|
+
extractInputVariables(ast2); // → ["x", "y"]
|
|
245
|
+
// 'sum' is excluded because it's computed from 'x' and 'y'
|
|
246
|
+
```
|
|
247
|
+
|
|
218
248
|
### AST Visitor Pattern
|
|
219
249
|
|
|
220
250
|
The visitor pattern provides a centralized, type-safe way to traverse and transform ASTs. This is useful for implementing custom analyzers, transformers, or code generators.
|
|
@@ -224,49 +254,70 @@ The visitor pattern provides a centralized, type-safe way to traverse and transf
|
|
|
224
254
|
Exhaustively visit every node in an AST. All node types must be handled.
|
|
225
255
|
|
|
226
256
|
```typescript
|
|
227
|
-
import { visit,
|
|
257
|
+
import { visit, parse } from "littlewing";
|
|
228
258
|
|
|
229
|
-
const ast =
|
|
259
|
+
const ast = parse("x = 5; x + 10");
|
|
230
260
|
|
|
231
261
|
// Count all identifiers in an AST
|
|
232
262
|
const count = visit(ast, {
|
|
233
|
-
|
|
263
|
+
// Tuple: [kind, statements]
|
|
264
|
+
Program: (n, recurse) => {
|
|
265
|
+
const statements = n[1];
|
|
266
|
+
return statements.reduce((sum, stmt) => sum + recurse(stmt), 0);
|
|
267
|
+
},
|
|
234
268
|
NumberLiteral: () => 0,
|
|
235
269
|
Identifier: () => 1,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
270
|
+
// Tuple: [kind, left, operator, right]
|
|
271
|
+
BinaryOp: (n, recurse) => recurse(n[1]) + recurse(n[3]),
|
|
272
|
+
// Tuple: [kind, operator, argument]
|
|
273
|
+
UnaryOp: (n, recurse) => recurse(n[2]),
|
|
274
|
+
// Tuple: [kind, name, value]
|
|
275
|
+
Assignment: (n, recurse) => 1 + recurse(n[2]),
|
|
276
|
+
// Tuple: [kind, name, arguments]
|
|
277
|
+
FunctionCall: (n, recurse) => {
|
|
278
|
+
const args = n[2];
|
|
279
|
+
return 1 + args.reduce((sum, arg) => sum + recurse(arg), 0);
|
|
280
|
+
},
|
|
281
|
+
// Tuple: [kind, condition, consequent, alternate]
|
|
282
|
+
ConditionalExpression: (n, recurse) =>
|
|
283
|
+
recurse(n[1]) + recurse(n[2]) + recurse(n[3]),
|
|
243
284
|
}); // → 3 (x appears 3 times)
|
|
244
285
|
```
|
|
245
286
|
|
|
246
|
-
#### `visitPartial<T>(node: ASTNode, visitor: Partial<Visitor<T
|
|
287
|
+
#### `visitPartial<T>(node: ASTNode, visitor: Partial<Visitor<T>>, defaultHandler: (node: ASTNode, recurse: (n: ASTNode) => T) => T): T`
|
|
247
288
|
|
|
248
|
-
Visit only specific node types.
|
|
289
|
+
Visit only specific node types. The `defaultHandler` is called for unhandled node types.
|
|
249
290
|
|
|
250
291
|
```typescript
|
|
251
|
-
import { visitPartial,
|
|
292
|
+
import { visitPartial, parse } from "littlewing";
|
|
252
293
|
|
|
253
|
-
const ast =
|
|
294
|
+
const ast = parse("x = 5; y = x + 10; y * 2");
|
|
254
295
|
|
|
255
296
|
// Find first assignment to a specific variable
|
|
256
|
-
const result = visitPartial(
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
297
|
+
const result = visitPartial(
|
|
298
|
+
ast,
|
|
299
|
+
{
|
|
300
|
+
// Tuple: [kind, statements]
|
|
301
|
+
Program: (n, recurse) => {
|
|
302
|
+
const statements = n[1];
|
|
303
|
+
for (const stmt of statements) {
|
|
304
|
+
const found = recurse(stmt);
|
|
305
|
+
if (found !== undefined) return found;
|
|
306
|
+
}
|
|
307
|
+
return undefined;
|
|
308
|
+
},
|
|
309
|
+
// Tuple: [kind, name, value]
|
|
310
|
+
Assignment: (n, recurse) => {
|
|
311
|
+
const name = n[1];
|
|
312
|
+
const value = n[2];
|
|
313
|
+
if (name === "y") return n; // Found it!
|
|
314
|
+
return recurse(value); // Keep searching
|
|
315
|
+
},
|
|
316
|
+
// Tuple: [kind, left, operator, right]
|
|
317
|
+
BinaryOp: (n, recurse) => recurse(n[1]) ?? recurse(n[3]),
|
|
267
318
|
},
|
|
268
|
-
|
|
269
|
-
|
|
319
|
+
() => undefined,
|
|
320
|
+
); // → Assignment node for "y = x + 10"
|
|
270
321
|
```
|
|
271
322
|
|
|
272
323
|
#### Transform Pattern
|
|
@@ -274,35 +325,66 @@ const result = visitPartial(ast, {
|
|
|
274
325
|
Use `visit<ASTNode>` to create AST transformers:
|
|
275
326
|
|
|
276
327
|
```typescript
|
|
277
|
-
import { visit,
|
|
328
|
+
import { visit, parse, ast } from "littlewing";
|
|
278
329
|
|
|
279
330
|
// Double all numeric literals
|
|
280
|
-
const transformed = visit<ASTNode>(
|
|
281
|
-
|
|
282
|
-
|
|
331
|
+
const transformed = visit<ASTNode>(parse("2 + 3 * 4"), {
|
|
332
|
+
// Tuple: [kind, statements]
|
|
333
|
+
Program: (n, recurse) => {
|
|
334
|
+
const statements = n[1];
|
|
335
|
+
return ast.program(statements.map((stmt) => recurse(stmt)));
|
|
336
|
+
},
|
|
337
|
+
// Tuple: [kind, value]
|
|
338
|
+
NumberLiteral: (n) => ast.number(n[1] * 2),
|
|
283
339
|
Identifier: (n) => n,
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
340
|
+
// Tuple: [kind, left, operator, right]
|
|
341
|
+
BinaryOp: (n, recurse) => ast.binaryOp(recurse(n[1]), n[2], recurse(n[3])),
|
|
342
|
+
// Tuple: [kind, operator, argument]
|
|
343
|
+
UnaryOp: (n, recurse) => ast.unaryOp(n[1], recurse(n[2])),
|
|
344
|
+
// Tuple: [kind, name, value]
|
|
345
|
+
Assignment: (n, recurse) => ast.assign(n[1], recurse(n[2])),
|
|
346
|
+
// Tuple: [kind, name, arguments]
|
|
347
|
+
FunctionCall: (n, recurse) => {
|
|
348
|
+
const name = n[1];
|
|
349
|
+
const args = n[2];
|
|
350
|
+
return ast.functionCall(name, args.map(recurse));
|
|
351
|
+
},
|
|
352
|
+
// Tuple: [kind, condition, consequent, alternate]
|
|
353
|
+
ConditionalExpression: (n, recurse) =>
|
|
354
|
+
ast.conditional(recurse(n[1]), recurse(n[2]), recurse(n[3])),
|
|
296
355
|
});
|
|
297
356
|
|
|
298
357
|
generate(transformed); // → "4 + 6 * 8"
|
|
299
358
|
```
|
|
300
359
|
|
|
360
|
+
### AST Builder Functions
|
|
361
|
+
|
|
362
|
+
The `ast` namespace provides builder functions for constructing AST nodes manually:
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { ast, generate } from "littlewing";
|
|
366
|
+
|
|
367
|
+
// Core builders
|
|
368
|
+
const node = ast.binaryOp(ast.number(2), "+", ast.number(3));
|
|
369
|
+
generate(node); // → "2 + 3"
|
|
370
|
+
|
|
371
|
+
// Convenience builders
|
|
372
|
+
const expr = ast.add(ast.identifier("x"), ast.number(10));
|
|
373
|
+
generate(expr); // → "x + 10"
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
**Available builders:**
|
|
377
|
+
|
|
378
|
+
- Core: `program()`, `number()`, `identifier()`, `binaryOp()`, `unaryOp()`, `functionCall()`, `assignment()`, `conditional()`
|
|
379
|
+
- Arithmetic: `add()`, `subtract()`, `multiply()`, `divide()`, `modulo()`, `exponentiate()`, `negate()`
|
|
380
|
+
- Comparison: `equals()`, `notEquals()`, `lessThan()`, `greaterThan()`, `lessEqual()`, `greaterEqual()`
|
|
381
|
+
- Logical: `logicalAnd()`, `logicalOr()`, `logicalNot()`
|
|
382
|
+
|
|
301
383
|
### ExecutionContext
|
|
302
384
|
|
|
303
385
|
```typescript
|
|
304
386
|
interface ExecutionContext {
|
|
305
|
-
functions?: Record<string, (...args:
|
|
387
|
+
functions?: Record<string, (...args: number[]) => number>;
|
|
306
388
|
variables?: Record<string, number>;
|
|
307
389
|
}
|
|
308
390
|
```
|
|
@@ -311,42 +393,44 @@ interface ExecutionContext {
|
|
|
311
393
|
|
|
312
394
|
The `defaultContext` includes these built-in functions:
|
|
313
395
|
|
|
314
|
-
**Math:** `ABS`, `CEIL`, `FLOOR`, `ROUND`, `SQRT`, `MIN`, `MAX`, `CLAMP`, `SIN`, `COS`, `TAN`, `LOG`, `LOG10`, `EXP`
|
|
396
|
+
**Math (14 functions):** `ABS`, `CEIL`, `FLOOR`, `ROUND`, `SQRT`, `MIN`, `MAX`, `CLAMP`, `SIN`, `COS`, `TAN`, `LOG`, `LOG10`, `EXP`
|
|
397
|
+
|
|
398
|
+
**Timestamps (2 functions):** `NOW`, `DATE`
|
|
399
|
+
|
|
400
|
+
**Time converters (4 functions, to milliseconds):** `FROM_DAYS`, `FROM_WEEKS`, `FROM_MONTHS`, `FROM_YEARS`
|
|
315
401
|
|
|
316
|
-
**
|
|
402
|
+
**Date component extractors (10 functions):** `GET_YEAR`, `GET_MONTH`, `GET_DAY`, `GET_HOUR`, `GET_MINUTE`, `GET_SECOND`, `GET_WEEKDAY`, `GET_MILLISECOND`, `GET_DAY_OF_YEAR`, `GET_QUARTER`
|
|
317
403
|
|
|
318
|
-
**Time
|
|
404
|
+
**Time differences (7 functions, always positive):** `DIFFERENCE_IN_SECONDS`, `DIFFERENCE_IN_MINUTES`, `DIFFERENCE_IN_HOURS`, `DIFFERENCE_IN_DAYS`, `DIFFERENCE_IN_WEEKS`, `DIFFERENCE_IN_MONTHS`, `DIFFERENCE_IN_YEARS`
|
|
319
405
|
|
|
320
|
-
**
|
|
406
|
+
**Start/End of period (8 functions):** `START_OF_DAY`, `END_OF_DAY`, `START_OF_WEEK`, `START_OF_MONTH`, `END_OF_MONTH`, `START_OF_YEAR`, `END_OF_YEAR`, `START_OF_QUARTER`
|
|
321
407
|
|
|
322
|
-
**
|
|
408
|
+
**Date arithmetic (3 functions):** `ADD_DAYS`, `ADD_MONTHS`, `ADD_YEARS`
|
|
323
409
|
|
|
324
|
-
**
|
|
410
|
+
**Date comparisons (3 functions):** `IS_SAME_DAY`, `IS_WEEKEND`, `IS_LEAP_YEAR`
|
|
325
411
|
|
|
326
|
-
**
|
|
412
|
+
**Note:** For before/after comparisons, use the comparison operators directly: `ts1 < ts2`, `ts1 > ts2`, `ts1 <= ts2`, `ts1 >= ts2`
|
|
327
413
|
|
|
328
|
-
**
|
|
414
|
+
**Total: 54 built-in functions**
|
|
329
415
|
|
|
330
416
|
## Performance Optimization
|
|
331
417
|
|
|
332
418
|
For expressions that are executed multiple times with different contexts, parse once and reuse the AST:
|
|
333
419
|
|
|
334
420
|
```typescript
|
|
335
|
-
import {
|
|
421
|
+
import { evaluate, parse } from "littlewing";
|
|
336
422
|
|
|
337
423
|
// Parse once
|
|
338
|
-
const formula =
|
|
339
|
-
"price * quantity * (1 - discount) * (1 + taxRate)",
|
|
340
|
-
);
|
|
424
|
+
const formula = parse("price * quantity * (1 - discount) * (1 + taxRate)");
|
|
341
425
|
|
|
342
|
-
//
|
|
343
|
-
|
|
426
|
+
// Evaluate many times with different values (no re-parsing)
|
|
427
|
+
evaluate(formula, {
|
|
344
428
|
variables: { price: 10, quantity: 5, discount: 0.1, taxRate: 0.08 },
|
|
345
429
|
});
|
|
346
|
-
|
|
430
|
+
evaluate(formula, {
|
|
347
431
|
variables: { price: 20, quantity: 3, discount: 0.15, taxRate: 0.08 },
|
|
348
432
|
});
|
|
349
|
-
|
|
433
|
+
evaluate(formula, {
|
|
350
434
|
variables: { price: 15, quantity: 10, discount: 0.2, taxRate: 0.08 },
|
|
351
435
|
});
|
|
352
436
|
|
|
@@ -371,7 +455,7 @@ Your app needs to evaluate user-provided formulas or dynamic expressions. Using
|
|
|
371
455
|
|
|
372
456
|
### The Solution
|
|
373
457
|
|
|
374
|
-
Littlewing provides just enough: arithmetic expressions with variables and functions. It's safe (no code execution), fast (linear time), and tiny (
|
|
458
|
+
Littlewing provides just enough: arithmetic expressions with variables and functions. It's safe (no code execution), fast (linear time), and tiny (7.8 KB gzipped).
|
|
375
459
|
|
|
376
460
|
### What Makes It Different
|
|
377
461
|
|