littlewing 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +39 -15
- package/dist/index.d.ts +219 -8
- package/dist/index.js +266 -80
- package/package.json +1 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ execute("2 + 3 * 4"); // → 14
|
|
|
12
12
|
execute("radius = 5; area = 3.14159 * radius ^ 2", defaultContext); // → 78.54
|
|
13
13
|
|
|
14
14
|
// Date arithmetic with timestamps
|
|
15
|
-
execute("deadline =
|
|
15
|
+
execute("deadline = NOW() + FROM_DAYS(7)", defaultContext); // → timestamp 7 days from now
|
|
16
16
|
|
|
17
17
|
// Conditional logic
|
|
18
18
|
execute("score = 85; grade = score >= 90 ? 100 : 90", {
|
|
@@ -23,12 +23,13 @@ execute("score = 85; grade = score >= 90 ? 100 : 90", {
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
25
|
- **Numbers-only** - Every value is a number. Simple, predictable, fast.
|
|
26
|
-
- **Zero dependencies** -
|
|
26
|
+
- **Zero dependencies** - 6.15 KB gzipped, perfect for browser bundles
|
|
27
27
|
- **O(n) performance** - Linear time parsing and execution
|
|
28
28
|
- **Safe evaluation** - No eval(), no code generation, no security risks
|
|
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
|
+
- **99%+ test coverage** - 276 tests with 99.39% function coverage, 99.56% line coverage
|
|
32
33
|
|
|
33
34
|
## Installation
|
|
34
35
|
|
|
@@ -70,22 +71,33 @@ execute("age >= 18 ? 100 : 0", { variables: { age: 21 } }); // → 100
|
|
|
70
71
|
import { execute, defaultContext } from "littlewing";
|
|
71
72
|
|
|
72
73
|
// Math functions
|
|
73
|
-
execute("
|
|
74
|
-
execute("
|
|
75
|
-
execute("
|
|
74
|
+
execute("ABS(-42)", defaultContext); // → 42
|
|
75
|
+
execute("SQRT(16)", defaultContext); // → 4
|
|
76
|
+
execute("MAX(3, 7, 2)", defaultContext); // → 7
|
|
76
77
|
|
|
77
78
|
// Current timestamp
|
|
78
|
-
execute("
|
|
79
|
+
execute("NOW()", defaultContext); // → 1704067200000
|
|
79
80
|
|
|
80
81
|
// Date arithmetic
|
|
81
|
-
execute("
|
|
82
|
-
execute("tomorrow =
|
|
82
|
+
execute("NOW() + FROM_HOURS(2)", defaultContext); // → timestamp 2 hours from now
|
|
83
|
+
execute("tomorrow = NOW() + FROM_DAYS(1)", defaultContext); // → tomorrow's timestamp
|
|
83
84
|
|
|
84
85
|
// Extract date components
|
|
85
86
|
const ctx = { ...defaultContext, variables: { ts: Date.now() } };
|
|
86
|
-
execute("
|
|
87
|
-
execute("
|
|
88
|
-
execute("
|
|
87
|
+
execute("GET_YEAR(ts)", ctx); // → 2024
|
|
88
|
+
execute("GET_MONTH(ts)", ctx); // → 11
|
|
89
|
+
execute("GET_DAY(ts)", ctx); // → 6
|
|
90
|
+
|
|
91
|
+
// Calculate time differences
|
|
92
|
+
const ts1 = Date.now();
|
|
93
|
+
const ts2 = ts1 + 1000 * 60 * 60 * 5; // 5 hours later
|
|
94
|
+
const context = { ...defaultContext, variables: { ts1, ts2 } };
|
|
95
|
+
execute("DIFFERENCE_IN_HOURS(ts1, ts2)", context); // → 5
|
|
96
|
+
|
|
97
|
+
// Date arithmetic and comparisons
|
|
98
|
+
execute("ADD_DAYS(NOW(), 7)", defaultContext); // → 7 days from now
|
|
99
|
+
execute("START_OF_DAY(NOW())", defaultContext); // → today at 00:00:00.000
|
|
100
|
+
execute("IS_WEEKEND(NOW())", defaultContext); // → 1 if today is Sat/Sun, else 0
|
|
89
101
|
```
|
|
90
102
|
|
|
91
103
|
### Custom Functions and Variables
|
|
@@ -150,7 +162,7 @@ Execute an expression and return the result.
|
|
|
150
162
|
|
|
151
163
|
```typescript
|
|
152
164
|
execute("2 + 2"); // → 4
|
|
153
|
-
execute("
|
|
165
|
+
execute("ABS(-5)", { functions: { ABS: Math.abs } }); // → 5
|
|
154
166
|
```
|
|
155
167
|
|
|
156
168
|
#### `parseSource(source: string): ASTNode`
|
|
@@ -197,11 +209,23 @@ interface ExecutionContext {
|
|
|
197
209
|
|
|
198
210
|
The `defaultContext` includes these built-in functions:
|
|
199
211
|
|
|
200
|
-
**Math:** `
|
|
212
|
+
**Math:** `ABS`, `CEIL`, `FLOOR`, `ROUND`, `SQRT`, `MIN`, `MAX`, `SIN`, `COS`, `TAN`, `LOG`, `LOG10`, `EXP`
|
|
201
213
|
|
|
202
|
-
**
|
|
214
|
+
**Timestamps:** `NOW`, `DATE`
|
|
203
215
|
|
|
204
|
-
**
|
|
216
|
+
**Time converters (to milliseconds):** `FROM_SECONDS`, `FROM_MINUTES`, `FROM_HOURS`, `FROM_DAYS`, `FROM_WEEKS`, `FROM_MONTHS`, `FROM_YEARS`
|
|
217
|
+
|
|
218
|
+
**Date component extractors:** `GET_YEAR`, `GET_MONTH`, `GET_DAY`, `GET_HOUR`, `GET_MINUTE`, `GET_SECOND`, `GET_WEEKDAY`, `GET_MILLISECOND`, `GET_DAY_OF_YEAR`, `GET_QUARTER`
|
|
219
|
+
|
|
220
|
+
**Time differences (always positive):** `DIFFERENCE_IN_SECONDS`, `DIFFERENCE_IN_MINUTES`, `DIFFERENCE_IN_HOURS`, `DIFFERENCE_IN_DAYS`, `DIFFERENCE_IN_WEEKS`
|
|
221
|
+
|
|
222
|
+
**Start/End of period:** `START_OF_DAY`, `END_OF_DAY`, `START_OF_WEEK`, `START_OF_MONTH`, `END_OF_MONTH`, `START_OF_YEAR`, `END_OF_YEAR`, `START_OF_QUARTER`
|
|
223
|
+
|
|
224
|
+
**Date arithmetic:** `ADD_DAYS`, `ADD_MONTHS`, `ADD_YEARS`
|
|
225
|
+
|
|
226
|
+
**Date comparisons:** `IS_BEFORE`, `IS_AFTER`, `IS_SAME_DAY`, `IS_WEEKEND`, `IS_LEAP_YEAR`
|
|
227
|
+
|
|
228
|
+
**Unix time:** `TO_UNIX_SECONDS`, `FROM_UNIX_SECONDS`
|
|
205
229
|
|
|
206
230
|
## Use Cases
|
|
207
231
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
declare namespace exports_ast {
|
|
2
|
-
export { unaryOp, subtract, program, number, notEquals, negate, multiply, modulo, logicalOr, logicalAnd, lessThan, lessEqual, identifier, greaterThan, greaterEqual, functionCall, exponentiate, equals, divide, conditional, binaryOp, assign, add };
|
|
3
|
-
}
|
|
4
1
|
/**
|
|
5
2
|
* Runtime value type - only numbers
|
|
6
3
|
*/
|
|
@@ -134,6 +131,31 @@ declare function isAssignment(node: ASTNode): node is Assignment;
|
|
|
134
131
|
declare function isProgram(node: ASTNode): node is Program;
|
|
135
132
|
declare function isConditionalExpression(node: ASTNode): node is ConditionalExpression;
|
|
136
133
|
/**
|
|
134
|
+
* Extracts input variables from an AST.
|
|
135
|
+
*
|
|
136
|
+
* Input variables are those whose values can be determined without knowing
|
|
137
|
+
* the values of other variables in the script. This includes:
|
|
138
|
+
* - Literals (10, -5, 3.14)
|
|
139
|
+
* - Unary minus of literals (-10)
|
|
140
|
+
* - Constant expressions (2 + 3, -5 * 2)
|
|
141
|
+
* - Function calls with constant arguments (MAX(10, 20), NOW())
|
|
142
|
+
*
|
|
143
|
+
* Computed variables (those that reference other variables) are excluded.
|
|
144
|
+
*
|
|
145
|
+
* @param ast - The AST to analyze (can be a single statement or Program node)
|
|
146
|
+
* @returns Array of input variable names
|
|
147
|
+
*
|
|
148
|
+
* @example
|
|
149
|
+
* ```typescript
|
|
150
|
+
* const ast = parseSource('price = 100; tax = price * 0.08')
|
|
151
|
+
* extractInputVariables(ast) // ['price']
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
154
|
+
declare function extractInputVariables(ast: ASTNode): string[];
|
|
155
|
+
declare namespace exports_ast {
|
|
156
|
+
export { unaryOp, subtract, program, number, notEquals, negate, multiply, modulo, logicalOr, logicalAnd, lessThan, lessEqual, identifier, greaterThan, greaterEqual, functionCall, exponentiate, equals, divide, conditional, binaryOp, assign, add };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
137
159
|
* Builder functions for creating AST nodes manually
|
|
138
160
|
*/
|
|
139
161
|
/**
|
|
@@ -294,28 +316,217 @@ declare class CodeGenerator {
|
|
|
294
316
|
* Generate source code from an AST node
|
|
295
317
|
*/
|
|
296
318
|
declare function generate(node: ASTNode): string;
|
|
319
|
+
declare namespace exports_date_utils {
|
|
320
|
+
export { TO_UNIX_SECONDS, START_OF_YEAR, START_OF_WEEK, START_OF_QUARTER, START_OF_MONTH, START_OF_DAY, NOW, IS_WEEKEND, IS_SAME_DAY, IS_LEAP_YEAR, IS_BEFORE, IS_AFTER, GET_YEAR, GET_WEEKDAY, GET_SECOND, GET_QUARTER, GET_MONTH, GET_MINUTE, GET_MILLISECOND, GET_HOUR, GET_DAY_OF_YEAR, GET_DAY, FROM_YEARS, FROM_WEEKS, FROM_UNIX_SECONDS, FROM_SECONDS, FROM_MONTHS, FROM_MINUTES, FROM_HOURS, FROM_DAYS, END_OF_YEAR, END_OF_MONTH, END_OF_DAY, DIFFERENCE_IN_WEEKS, DIFFERENCE_IN_SECONDS, DIFFERENCE_IN_MINUTES, DIFFERENCE_IN_HOURS, DIFFERENCE_IN_DAYS, DATE, ADD_YEARS, ADD_MONTHS, ADD_DAYS };
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Date utility functions for working with timestamps
|
|
324
|
+
* All functions work with milliseconds since Unix epoch (numbers only)
|
|
325
|
+
*/
|
|
326
|
+
/**
|
|
327
|
+
* Get current timestamp (milliseconds since Unix epoch)
|
|
328
|
+
*/
|
|
329
|
+
declare const NOW: () => number;
|
|
330
|
+
/**
|
|
331
|
+
* Create timestamp from date components
|
|
332
|
+
* Year is required, all other parameters default to minimum values
|
|
333
|
+
* Month is 1-based (1 = January, 12 = December)
|
|
334
|
+
*/
|
|
335
|
+
declare const DATE: (year: number, month?: number, day?: number, hour?: number, minute?: number, second?: number) => number;
|
|
336
|
+
/**
|
|
337
|
+
* Convert seconds to milliseconds
|
|
338
|
+
*/
|
|
339
|
+
declare const FROM_SECONDS: (s: number) => number;
|
|
340
|
+
/**
|
|
341
|
+
* Convert minutes to milliseconds
|
|
342
|
+
*/
|
|
343
|
+
declare const FROM_MINUTES: (m: number) => number;
|
|
344
|
+
/**
|
|
345
|
+
* Convert hours to milliseconds
|
|
346
|
+
*/
|
|
347
|
+
declare const FROM_HOURS: (h: number) => number;
|
|
348
|
+
/**
|
|
349
|
+
* Convert days to milliseconds
|
|
350
|
+
*/
|
|
351
|
+
declare const FROM_DAYS: (d: number) => number;
|
|
352
|
+
/**
|
|
353
|
+
* Convert weeks to milliseconds
|
|
354
|
+
*/
|
|
355
|
+
declare const FROM_WEEKS: (w: number) => number;
|
|
356
|
+
/**
|
|
357
|
+
* Convert months to milliseconds (approximate: 30 days per month)
|
|
358
|
+
*/
|
|
359
|
+
declare const FROM_MONTHS: (months: number) => number;
|
|
360
|
+
/**
|
|
361
|
+
* Convert years to milliseconds (approximate: 365 days per year)
|
|
362
|
+
*/
|
|
363
|
+
declare const FROM_YEARS: (years: number) => number;
|
|
364
|
+
/**
|
|
365
|
+
* Get the year from a timestamp
|
|
366
|
+
*/
|
|
367
|
+
declare const GET_YEAR: (timestamp: number) => number;
|
|
368
|
+
/**
|
|
369
|
+
* Get the month from a timestamp (1-based: 1 = January, 12 = December)
|
|
370
|
+
*/
|
|
371
|
+
declare const GET_MONTH: (timestamp: number) => number;
|
|
372
|
+
/**
|
|
373
|
+
* Get the day of month from a timestamp (1-31)
|
|
374
|
+
*/
|
|
375
|
+
declare const GET_DAY: (timestamp: number) => number;
|
|
376
|
+
/**
|
|
377
|
+
* Get the hour from a timestamp (0-23)
|
|
378
|
+
*/
|
|
379
|
+
declare const GET_HOUR: (timestamp: number) => number;
|
|
380
|
+
/**
|
|
381
|
+
* Get the minute from a timestamp (0-59)
|
|
382
|
+
*/
|
|
383
|
+
declare const GET_MINUTE: (timestamp: number) => number;
|
|
384
|
+
/**
|
|
385
|
+
* Get the second from a timestamp (0-59)
|
|
386
|
+
*/
|
|
387
|
+
declare const GET_SECOND: (timestamp: number) => number;
|
|
388
|
+
/**
|
|
389
|
+
* Get the millisecond component from a timestamp (0-999)
|
|
390
|
+
*/
|
|
391
|
+
declare const GET_MILLISECOND: (timestamp: number) => number;
|
|
392
|
+
/**
|
|
393
|
+
* Get the day of week from a timestamp (0 = Sunday, 6 = Saturday)
|
|
394
|
+
*/
|
|
395
|
+
declare const GET_WEEKDAY: (timestamp: number) => number;
|
|
396
|
+
/**
|
|
397
|
+
* Get the day of year (1-366) from a timestamp
|
|
398
|
+
*/
|
|
399
|
+
declare const GET_DAY_OF_YEAR: (timestamp: number) => number;
|
|
400
|
+
/**
|
|
401
|
+
* Get the quarter (1-4) from a timestamp
|
|
402
|
+
*/
|
|
403
|
+
declare const GET_QUARTER: (timestamp: number) => number;
|
|
404
|
+
/**
|
|
405
|
+
* Get the absolute difference between two timestamps in seconds
|
|
406
|
+
*/
|
|
407
|
+
declare const DIFFERENCE_IN_SECONDS: (ts1: number, ts2: number) => number;
|
|
408
|
+
/**
|
|
409
|
+
* Get the absolute difference between two timestamps in minutes
|
|
410
|
+
*/
|
|
411
|
+
declare const DIFFERENCE_IN_MINUTES: (ts1: number, ts2: number) => number;
|
|
412
|
+
/**
|
|
413
|
+
* Get the absolute difference between two timestamps in hours
|
|
414
|
+
*/
|
|
415
|
+
declare const DIFFERENCE_IN_HOURS: (ts1: number, ts2: number) => number;
|
|
416
|
+
/**
|
|
417
|
+
* Get the absolute difference between two timestamps in days
|
|
418
|
+
*/
|
|
419
|
+
declare const DIFFERENCE_IN_DAYS: (ts1: number, ts2: number) => number;
|
|
420
|
+
/**
|
|
421
|
+
* Get the absolute difference between two timestamps in weeks
|
|
422
|
+
*/
|
|
423
|
+
declare const DIFFERENCE_IN_WEEKS: (ts1: number, ts2: number) => number;
|
|
297
424
|
/**
|
|
298
|
-
*
|
|
425
|
+
* Get the start of day (00:00:00.000) for a given timestamp
|
|
426
|
+
*/
|
|
427
|
+
declare const START_OF_DAY: (timestamp: number) => number;
|
|
428
|
+
/**
|
|
429
|
+
* Get the end of day (23:59:59.999) for a given timestamp
|
|
430
|
+
*/
|
|
431
|
+
declare const END_OF_DAY: (timestamp: number) => number;
|
|
432
|
+
/**
|
|
433
|
+
* Get the start of week (Sunday at 00:00:00.000) for a given timestamp
|
|
434
|
+
*/
|
|
435
|
+
declare const START_OF_WEEK: (timestamp: number) => number;
|
|
436
|
+
/**
|
|
437
|
+
* Get the start of month (1st day at 00:00:00.000) for a given timestamp
|
|
438
|
+
*/
|
|
439
|
+
declare const START_OF_MONTH: (timestamp: number) => number;
|
|
440
|
+
/**
|
|
441
|
+
* Get the end of month (last day at 23:59:59.999) for a given timestamp
|
|
442
|
+
*/
|
|
443
|
+
declare const END_OF_MONTH: (timestamp: number) => number;
|
|
444
|
+
/**
|
|
445
|
+
* Get the start of year (Jan 1st at 00:00:00.000) for a given timestamp
|
|
446
|
+
*/
|
|
447
|
+
declare const START_OF_YEAR: (timestamp: number) => number;
|
|
448
|
+
/**
|
|
449
|
+
* Get the end of year (Dec 31st at 23:59:59.999) for a given timestamp
|
|
450
|
+
*/
|
|
451
|
+
declare const END_OF_YEAR: (timestamp: number) => number;
|
|
452
|
+
/**
|
|
453
|
+
* Add days to a timestamp
|
|
454
|
+
*/
|
|
455
|
+
declare const ADD_DAYS: (timestamp: number, days: number) => number;
|
|
456
|
+
/**
|
|
457
|
+
* Add months to a timestamp (handles variable month lengths correctly)
|
|
458
|
+
*/
|
|
459
|
+
declare const ADD_MONTHS: (timestamp: number, months: number) => number;
|
|
460
|
+
/**
|
|
461
|
+
* Add years to a timestamp
|
|
462
|
+
*/
|
|
463
|
+
declare const ADD_YEARS: (timestamp: number, years: number) => number;
|
|
464
|
+
/**
|
|
465
|
+
* Check if ts1 is before ts2
|
|
466
|
+
* Returns 1 if true, 0 if false
|
|
467
|
+
*/
|
|
468
|
+
declare const IS_BEFORE: (ts1: number, ts2: number) => number;
|
|
469
|
+
/**
|
|
470
|
+
* Check if ts1 is after ts2
|
|
471
|
+
* Returns 1 if true, 0 if false
|
|
472
|
+
*/
|
|
473
|
+
declare const IS_AFTER: (ts1: number, ts2: number) => number;
|
|
474
|
+
/**
|
|
475
|
+
* Check if two timestamps are on the same calendar day
|
|
476
|
+
* Returns 1 if true, 0 if false
|
|
477
|
+
*/
|
|
478
|
+
declare const IS_SAME_DAY: (ts1: number, ts2: number) => number;
|
|
479
|
+
/**
|
|
480
|
+
* Check if timestamp falls on a weekend (Saturday or Sunday)
|
|
481
|
+
* Returns 1 if true, 0 if false
|
|
482
|
+
*/
|
|
483
|
+
declare const IS_WEEKEND: (timestamp: number) => number;
|
|
484
|
+
/**
|
|
485
|
+
* Check if timestamp is in a leap year
|
|
486
|
+
* Returns 1 if true, 0 if false
|
|
487
|
+
*/
|
|
488
|
+
declare const IS_LEAP_YEAR: (timestamp: number) => number;
|
|
489
|
+
/**
|
|
490
|
+
* Get the start of quarter for a given timestamp
|
|
491
|
+
*/
|
|
492
|
+
declare const START_OF_QUARTER: (timestamp: number) => number;
|
|
493
|
+
/**
|
|
494
|
+
* Convert millisecond timestamp to Unix seconds
|
|
495
|
+
*/
|
|
496
|
+
declare const TO_UNIX_SECONDS: (timestamp: number) => number;
|
|
497
|
+
/**
|
|
498
|
+
* Convert Unix seconds to millisecond timestamp
|
|
499
|
+
*/
|
|
500
|
+
declare const FROM_UNIX_SECONDS: (seconds: number) => number;
|
|
501
|
+
/**
|
|
502
|
+
* Default execution context with common Math functions and date/time utilities
|
|
299
503
|
* Users can use this as-is or spread it into their own context
|
|
300
504
|
*
|
|
505
|
+
* All functions use UPPERCASE naming convention to avoid collisions with user variables.
|
|
301
506
|
* All date-related functions work with timestamps (milliseconds since Unix epoch)
|
|
302
507
|
* to maintain the language's numbers-only type system.
|
|
303
508
|
*
|
|
304
509
|
* @example
|
|
305
510
|
* // Use as-is
|
|
306
|
-
* execute('
|
|
511
|
+
* execute('ABS(-5)', defaultContext)
|
|
307
512
|
*
|
|
308
513
|
* @example
|
|
309
514
|
* // Spread into custom context
|
|
310
|
-
* execute('
|
|
515
|
+
* execute('NOW() + FROM_MINUTES(5)', {
|
|
311
516
|
* ...defaultContext,
|
|
312
517
|
* variables: { customVar: 42 }
|
|
313
518
|
* })
|
|
314
519
|
*
|
|
315
520
|
* @example
|
|
316
521
|
* // Work with timestamps
|
|
317
|
-
* const result = execute('
|
|
522
|
+
* const result = execute('NOW() + FROM_DAYS(7)', defaultContext)
|
|
318
523
|
* const futureDate = new Date(result) // Convert back to Date if needed
|
|
524
|
+
*
|
|
525
|
+
* @example
|
|
526
|
+
* // Calculate time differences
|
|
527
|
+
* const ts1 = Date.now()
|
|
528
|
+
* const ts2 = ts1 + 1000 * 60 * 60 * 3 // 3 hours later
|
|
529
|
+
* execute('DIFFERENCE_IN_HOURS(ts1, ts2)', { ...defaultContext, variables: { ts1, ts2 } })
|
|
319
530
|
*/
|
|
320
531
|
declare const defaultContext: ExecutionContext;
|
|
321
532
|
/**
|
|
@@ -493,4 +704,4 @@ declare class Parser {
|
|
|
493
704
|
* @returns Parsed AST
|
|
494
705
|
*/
|
|
495
706
|
declare function parseSource(source: string): ASTNode;
|
|
496
|
-
export { parseSource, optimize, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isConditionalExpression, isBinaryOp, isAssignment, generate, execute, defaultContext, exports_ast as ast, TokenType, Token, RuntimeValue, Parser, Lexer, Executor, ExecutionContext, CodeGenerator, ASTNode };
|
|
707
|
+
export { parseSource, optimize, isUnaryOp, isProgram, isNumberLiteral, isIdentifier, isFunctionCall, isConditionalExpression, isBinaryOp, isAssignment, generate, extractInputVariables, execute, defaultContext, exports_date_utils as dateUtils, exports_ast as ast, UnaryOp, TokenType, Token, RuntimeValue, Program, Parser, Operator, NumberLiteral, Lexer, Identifier, FunctionCall, Executor, ExecutionContext, ConditionalExpression, CodeGenerator, BinaryOp, Assignment, ASTNode };
|
package/dist/index.js
CHANGED
|
@@ -10,6 +10,92 @@ var __export = (target, all) => {
|
|
|
10
10
|
});
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
+
// src/types.ts
|
|
14
|
+
var TokenType;
|
|
15
|
+
((TokenType2) => {
|
|
16
|
+
TokenType2["NUMBER"] = "NUMBER";
|
|
17
|
+
TokenType2["IDENTIFIER"] = "IDENTIFIER";
|
|
18
|
+
TokenType2["PLUS"] = "PLUS";
|
|
19
|
+
TokenType2["MINUS"] = "MINUS";
|
|
20
|
+
TokenType2["STAR"] = "STAR";
|
|
21
|
+
TokenType2["SLASH"] = "SLASH";
|
|
22
|
+
TokenType2["PERCENT"] = "PERCENT";
|
|
23
|
+
TokenType2["CARET"] = "CARET";
|
|
24
|
+
TokenType2["DOUBLE_EQUALS"] = "DOUBLE_EQUALS";
|
|
25
|
+
TokenType2["NOT_EQUALS"] = "NOT_EQUALS";
|
|
26
|
+
TokenType2["LESS_THAN"] = "LESS_THAN";
|
|
27
|
+
TokenType2["GREATER_THAN"] = "GREATER_THAN";
|
|
28
|
+
TokenType2["LESS_EQUAL"] = "LESS_EQUAL";
|
|
29
|
+
TokenType2["GREATER_EQUAL"] = "GREATER_EQUAL";
|
|
30
|
+
TokenType2["LOGICAL_AND"] = "LOGICAL_AND";
|
|
31
|
+
TokenType2["LOGICAL_OR"] = "LOGICAL_OR";
|
|
32
|
+
TokenType2["LPAREN"] = "LPAREN";
|
|
33
|
+
TokenType2["RPAREN"] = "RPAREN";
|
|
34
|
+
TokenType2["EQUALS"] = "EQUALS";
|
|
35
|
+
TokenType2["COMMA"] = "COMMA";
|
|
36
|
+
TokenType2["QUESTION"] = "QUESTION";
|
|
37
|
+
TokenType2["COLON"] = "COLON";
|
|
38
|
+
TokenType2["EOF"] = "EOF";
|
|
39
|
+
})(TokenType ||= {});
|
|
40
|
+
function isNumberLiteral(node) {
|
|
41
|
+
return node.type === "NumberLiteral";
|
|
42
|
+
}
|
|
43
|
+
function isIdentifier(node) {
|
|
44
|
+
return node.type === "Identifier";
|
|
45
|
+
}
|
|
46
|
+
function isBinaryOp(node) {
|
|
47
|
+
return node.type === "BinaryOp";
|
|
48
|
+
}
|
|
49
|
+
function isUnaryOp(node) {
|
|
50
|
+
return node.type === "UnaryOp";
|
|
51
|
+
}
|
|
52
|
+
function isFunctionCall(node) {
|
|
53
|
+
return node.type === "FunctionCall";
|
|
54
|
+
}
|
|
55
|
+
function isAssignment(node) {
|
|
56
|
+
return node.type === "Assignment";
|
|
57
|
+
}
|
|
58
|
+
function isProgram(node) {
|
|
59
|
+
return node.type === "Program";
|
|
60
|
+
}
|
|
61
|
+
function isConditionalExpression(node) {
|
|
62
|
+
return node.type === "ConditionalExpression";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// src/analyzer.ts
|
|
66
|
+
function extractInputVariables(ast) {
|
|
67
|
+
const inputVars = new Set;
|
|
68
|
+
const statements = isProgram(ast) ? ast.statements : [ast];
|
|
69
|
+
for (const statement of statements) {
|
|
70
|
+
if (isAssignment(statement)) {
|
|
71
|
+
if (!containsVariableReference(statement.value)) {
|
|
72
|
+
inputVars.add(statement.name);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return Array.from(inputVars);
|
|
77
|
+
}
|
|
78
|
+
function containsVariableReference(node) {
|
|
79
|
+
if (isIdentifier(node)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
if (isNumberLiteral(node)) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (isBinaryOp(node)) {
|
|
86
|
+
return containsVariableReference(node.left) || containsVariableReference(node.right);
|
|
87
|
+
}
|
|
88
|
+
if (isUnaryOp(node)) {
|
|
89
|
+
return containsVariableReference(node.argument);
|
|
90
|
+
}
|
|
91
|
+
if (isFunctionCall(node)) {
|
|
92
|
+
return node.arguments.some((arg) => containsVariableReference(arg));
|
|
93
|
+
}
|
|
94
|
+
if (isConditionalExpression(node)) {
|
|
95
|
+
return containsVariableReference(node.condition) || containsVariableReference(node.consequent) || containsVariableReference(node.alternate);
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
13
99
|
// src/ast.ts
|
|
14
100
|
var exports_ast = {};
|
|
15
101
|
__export(exports_ast, {
|
|
@@ -137,58 +223,6 @@ function logicalAnd(left, right) {
|
|
|
137
223
|
function logicalOr(left, right) {
|
|
138
224
|
return binaryOp(left, "||", right);
|
|
139
225
|
}
|
|
140
|
-
// src/types.ts
|
|
141
|
-
var TokenType;
|
|
142
|
-
((TokenType2) => {
|
|
143
|
-
TokenType2["NUMBER"] = "NUMBER";
|
|
144
|
-
TokenType2["IDENTIFIER"] = "IDENTIFIER";
|
|
145
|
-
TokenType2["PLUS"] = "PLUS";
|
|
146
|
-
TokenType2["MINUS"] = "MINUS";
|
|
147
|
-
TokenType2["STAR"] = "STAR";
|
|
148
|
-
TokenType2["SLASH"] = "SLASH";
|
|
149
|
-
TokenType2["PERCENT"] = "PERCENT";
|
|
150
|
-
TokenType2["CARET"] = "CARET";
|
|
151
|
-
TokenType2["DOUBLE_EQUALS"] = "DOUBLE_EQUALS";
|
|
152
|
-
TokenType2["NOT_EQUALS"] = "NOT_EQUALS";
|
|
153
|
-
TokenType2["LESS_THAN"] = "LESS_THAN";
|
|
154
|
-
TokenType2["GREATER_THAN"] = "GREATER_THAN";
|
|
155
|
-
TokenType2["LESS_EQUAL"] = "LESS_EQUAL";
|
|
156
|
-
TokenType2["GREATER_EQUAL"] = "GREATER_EQUAL";
|
|
157
|
-
TokenType2["LOGICAL_AND"] = "LOGICAL_AND";
|
|
158
|
-
TokenType2["LOGICAL_OR"] = "LOGICAL_OR";
|
|
159
|
-
TokenType2["LPAREN"] = "LPAREN";
|
|
160
|
-
TokenType2["RPAREN"] = "RPAREN";
|
|
161
|
-
TokenType2["EQUALS"] = "EQUALS";
|
|
162
|
-
TokenType2["COMMA"] = "COMMA";
|
|
163
|
-
TokenType2["QUESTION"] = "QUESTION";
|
|
164
|
-
TokenType2["COLON"] = "COLON";
|
|
165
|
-
TokenType2["EOF"] = "EOF";
|
|
166
|
-
})(TokenType ||= {});
|
|
167
|
-
function isNumberLiteral(node) {
|
|
168
|
-
return node.type === "NumberLiteral";
|
|
169
|
-
}
|
|
170
|
-
function isIdentifier(node) {
|
|
171
|
-
return node.type === "Identifier";
|
|
172
|
-
}
|
|
173
|
-
function isBinaryOp(node) {
|
|
174
|
-
return node.type === "BinaryOp";
|
|
175
|
-
}
|
|
176
|
-
function isUnaryOp(node) {
|
|
177
|
-
return node.type === "UnaryOp";
|
|
178
|
-
}
|
|
179
|
-
function isFunctionCall(node) {
|
|
180
|
-
return node.type === "FunctionCall";
|
|
181
|
-
}
|
|
182
|
-
function isAssignment(node) {
|
|
183
|
-
return node.type === "Assignment";
|
|
184
|
-
}
|
|
185
|
-
function isProgram(node) {
|
|
186
|
-
return node.type === "Program";
|
|
187
|
-
}
|
|
188
|
-
function isConditionalExpression(node) {
|
|
189
|
-
return node.type === "ConditionalExpression";
|
|
190
|
-
}
|
|
191
|
-
|
|
192
226
|
// src/utils.ts
|
|
193
227
|
function evaluateBinaryOperation(operator, left, right) {
|
|
194
228
|
switch (operator) {
|
|
@@ -373,37 +407,187 @@ function generate(node) {
|
|
|
373
407
|
const generator = new CodeGenerator;
|
|
374
408
|
return generator.generate(node);
|
|
375
409
|
}
|
|
410
|
+
// src/date-utils.ts
|
|
411
|
+
var exports_date_utils = {};
|
|
412
|
+
__export(exports_date_utils, {
|
|
413
|
+
TO_UNIX_SECONDS: () => TO_UNIX_SECONDS,
|
|
414
|
+
START_OF_YEAR: () => START_OF_YEAR,
|
|
415
|
+
START_OF_WEEK: () => START_OF_WEEK,
|
|
416
|
+
START_OF_QUARTER: () => START_OF_QUARTER,
|
|
417
|
+
START_OF_MONTH: () => START_OF_MONTH,
|
|
418
|
+
START_OF_DAY: () => START_OF_DAY,
|
|
419
|
+
NOW: () => NOW,
|
|
420
|
+
IS_WEEKEND: () => IS_WEEKEND,
|
|
421
|
+
IS_SAME_DAY: () => IS_SAME_DAY,
|
|
422
|
+
IS_LEAP_YEAR: () => IS_LEAP_YEAR,
|
|
423
|
+
IS_BEFORE: () => IS_BEFORE,
|
|
424
|
+
IS_AFTER: () => IS_AFTER,
|
|
425
|
+
GET_YEAR: () => GET_YEAR,
|
|
426
|
+
GET_WEEKDAY: () => GET_WEEKDAY,
|
|
427
|
+
GET_SECOND: () => GET_SECOND,
|
|
428
|
+
GET_QUARTER: () => GET_QUARTER,
|
|
429
|
+
GET_MONTH: () => GET_MONTH,
|
|
430
|
+
GET_MINUTE: () => GET_MINUTE,
|
|
431
|
+
GET_MILLISECOND: () => GET_MILLISECOND,
|
|
432
|
+
GET_HOUR: () => GET_HOUR,
|
|
433
|
+
GET_DAY_OF_YEAR: () => GET_DAY_OF_YEAR,
|
|
434
|
+
GET_DAY: () => GET_DAY,
|
|
435
|
+
FROM_YEARS: () => FROM_YEARS,
|
|
436
|
+
FROM_WEEKS: () => FROM_WEEKS,
|
|
437
|
+
FROM_UNIX_SECONDS: () => FROM_UNIX_SECONDS,
|
|
438
|
+
FROM_SECONDS: () => FROM_SECONDS,
|
|
439
|
+
FROM_MONTHS: () => FROM_MONTHS,
|
|
440
|
+
FROM_MINUTES: () => FROM_MINUTES,
|
|
441
|
+
FROM_HOURS: () => FROM_HOURS,
|
|
442
|
+
FROM_DAYS: () => FROM_DAYS,
|
|
443
|
+
END_OF_YEAR: () => END_OF_YEAR,
|
|
444
|
+
END_OF_MONTH: () => END_OF_MONTH,
|
|
445
|
+
END_OF_DAY: () => END_OF_DAY,
|
|
446
|
+
DIFFERENCE_IN_WEEKS: () => DIFFERENCE_IN_WEEKS,
|
|
447
|
+
DIFFERENCE_IN_SECONDS: () => DIFFERENCE_IN_SECONDS,
|
|
448
|
+
DIFFERENCE_IN_MINUTES: () => DIFFERENCE_IN_MINUTES,
|
|
449
|
+
DIFFERENCE_IN_HOURS: () => DIFFERENCE_IN_HOURS,
|
|
450
|
+
DIFFERENCE_IN_DAYS: () => DIFFERENCE_IN_DAYS,
|
|
451
|
+
DATE: () => DATE,
|
|
452
|
+
ADD_YEARS: () => ADD_YEARS,
|
|
453
|
+
ADD_MONTHS: () => ADD_MONTHS,
|
|
454
|
+
ADD_DAYS: () => ADD_DAYS
|
|
455
|
+
});
|
|
456
|
+
var NOW = () => Date.now();
|
|
457
|
+
var DATE = (year, month = 1, day = 1, hour = 0, minute = 0, second = 0) => new Date(year, month - 1, day, hour, minute, second).getTime();
|
|
458
|
+
var FROM_SECONDS = (s) => s * 1000;
|
|
459
|
+
var FROM_MINUTES = (m) => m * 60 * 1000;
|
|
460
|
+
var FROM_HOURS = (h) => h * 60 * 60 * 1000;
|
|
461
|
+
var FROM_DAYS = (d) => d * 24 * 60 * 60 * 1000;
|
|
462
|
+
var FROM_WEEKS = (w) => w * 7 * 24 * 60 * 60 * 1000;
|
|
463
|
+
var FROM_MONTHS = (months) => months * 30 * 24 * 60 * 60 * 1000;
|
|
464
|
+
var FROM_YEARS = (years) => years * 365 * 24 * 60 * 60 * 1000;
|
|
465
|
+
var GET_YEAR = (timestamp) => new Date(timestamp).getFullYear();
|
|
466
|
+
var GET_MONTH = (timestamp) => new Date(timestamp).getMonth() + 1;
|
|
467
|
+
var GET_DAY = (timestamp) => new Date(timestamp).getDate();
|
|
468
|
+
var GET_HOUR = (timestamp) => new Date(timestamp).getHours();
|
|
469
|
+
var GET_MINUTE = (timestamp) => new Date(timestamp).getMinutes();
|
|
470
|
+
var GET_SECOND = (timestamp) => new Date(timestamp).getSeconds();
|
|
471
|
+
var GET_MILLISECOND = (timestamp) => new Date(timestamp).getMilliseconds();
|
|
472
|
+
var GET_WEEKDAY = (timestamp) => new Date(timestamp).getDay();
|
|
473
|
+
var GET_DAY_OF_YEAR = (timestamp) => {
|
|
474
|
+
const date = new Date(timestamp);
|
|
475
|
+
const start = new Date(date.getFullYear(), 0, 0);
|
|
476
|
+
const diff = date.getTime() - start.getTime();
|
|
477
|
+
const oneDay = 1000 * 60 * 60 * 24;
|
|
478
|
+
return Math.floor(diff / oneDay);
|
|
479
|
+
};
|
|
480
|
+
var GET_QUARTER = (timestamp) => {
|
|
481
|
+
const month = new Date(timestamp).getMonth();
|
|
482
|
+
return Math.floor(month / 3) + 1;
|
|
483
|
+
};
|
|
484
|
+
var DIFFERENCE_IN_SECONDS = (ts1, ts2) => Math.abs(ts1 - ts2) / 1000;
|
|
485
|
+
var DIFFERENCE_IN_MINUTES = (ts1, ts2) => Math.abs(ts1 - ts2) / (60 * 1000);
|
|
486
|
+
var DIFFERENCE_IN_HOURS = (ts1, ts2) => Math.abs(ts1 - ts2) / (60 * 60 * 1000);
|
|
487
|
+
var DIFFERENCE_IN_DAYS = (ts1, ts2) => Math.abs(ts1 - ts2) / (24 * 60 * 60 * 1000);
|
|
488
|
+
var DIFFERENCE_IN_WEEKS = (ts1, ts2) => Math.abs(ts1 - ts2) / (7 * 24 * 60 * 60 * 1000);
|
|
489
|
+
var START_OF_DAY = (timestamp) => {
|
|
490
|
+
const date = new Date(timestamp);
|
|
491
|
+
date.setHours(0, 0, 0, 0);
|
|
492
|
+
return date.getTime();
|
|
493
|
+
};
|
|
494
|
+
var END_OF_DAY = (timestamp) => {
|
|
495
|
+
const date = new Date(timestamp);
|
|
496
|
+
date.setHours(23, 59, 59, 999);
|
|
497
|
+
return date.getTime();
|
|
498
|
+
};
|
|
499
|
+
var START_OF_WEEK = (timestamp) => {
|
|
500
|
+
const date = new Date(timestamp);
|
|
501
|
+
const day = date.getDay();
|
|
502
|
+
const diff = date.getDate() - day;
|
|
503
|
+
date.setDate(diff);
|
|
504
|
+
date.setHours(0, 0, 0, 0);
|
|
505
|
+
return date.getTime();
|
|
506
|
+
};
|
|
507
|
+
var START_OF_MONTH = (timestamp) => {
|
|
508
|
+
const date = new Date(timestamp);
|
|
509
|
+
date.setDate(1);
|
|
510
|
+
date.setHours(0, 0, 0, 0);
|
|
511
|
+
return date.getTime();
|
|
512
|
+
};
|
|
513
|
+
var END_OF_MONTH = (timestamp) => {
|
|
514
|
+
const date = new Date(timestamp);
|
|
515
|
+
date.setMonth(date.getMonth() + 1, 0);
|
|
516
|
+
date.setHours(23, 59, 59, 999);
|
|
517
|
+
return date.getTime();
|
|
518
|
+
};
|
|
519
|
+
var START_OF_YEAR = (timestamp) => {
|
|
520
|
+
const date = new Date(timestamp);
|
|
521
|
+
date.setMonth(0, 1);
|
|
522
|
+
date.setHours(0, 0, 0, 0);
|
|
523
|
+
return date.getTime();
|
|
524
|
+
};
|
|
525
|
+
var END_OF_YEAR = (timestamp) => {
|
|
526
|
+
const date = new Date(timestamp);
|
|
527
|
+
date.setMonth(11, 31);
|
|
528
|
+
date.setHours(23, 59, 59, 999);
|
|
529
|
+
return date.getTime();
|
|
530
|
+
};
|
|
531
|
+
var ADD_DAYS = (timestamp, days) => {
|
|
532
|
+
const date = new Date(timestamp);
|
|
533
|
+
date.setDate(date.getDate() + days);
|
|
534
|
+
return date.getTime();
|
|
535
|
+
};
|
|
536
|
+
var ADD_MONTHS = (timestamp, months) => {
|
|
537
|
+
const date = new Date(timestamp);
|
|
538
|
+
date.setMonth(date.getMonth() + months);
|
|
539
|
+
return date.getTime();
|
|
540
|
+
};
|
|
541
|
+
var ADD_YEARS = (timestamp, years) => {
|
|
542
|
+
const date = new Date(timestamp);
|
|
543
|
+
date.setFullYear(date.getFullYear() + years);
|
|
544
|
+
return date.getTime();
|
|
545
|
+
};
|
|
546
|
+
var IS_BEFORE = (ts1, ts2) => {
|
|
547
|
+
return ts1 < ts2 ? 1 : 0;
|
|
548
|
+
};
|
|
549
|
+
var IS_AFTER = (ts1, ts2) => {
|
|
550
|
+
return ts1 > ts2 ? 1 : 0;
|
|
551
|
+
};
|
|
552
|
+
var IS_SAME_DAY = (ts1, ts2) => {
|
|
553
|
+
const date1 = new Date(ts1);
|
|
554
|
+
const date2 = new Date(ts2);
|
|
555
|
+
return date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() ? 1 : 0;
|
|
556
|
+
};
|
|
557
|
+
var IS_WEEKEND = (timestamp) => {
|
|
558
|
+
const day = new Date(timestamp).getDay();
|
|
559
|
+
return day === 0 || day === 6 ? 1 : 0;
|
|
560
|
+
};
|
|
561
|
+
var IS_LEAP_YEAR = (timestamp) => {
|
|
562
|
+
const year = new Date(timestamp).getFullYear();
|
|
563
|
+
return year % 4 === 0 && year % 100 !== 0 || year % 400 === 0 ? 1 : 0;
|
|
564
|
+
};
|
|
565
|
+
var START_OF_QUARTER = (timestamp) => {
|
|
566
|
+
const date = new Date(timestamp);
|
|
567
|
+
const quarter = Math.floor(date.getMonth() / 3);
|
|
568
|
+
date.setMonth(quarter * 3, 1);
|
|
569
|
+
date.setHours(0, 0, 0, 0);
|
|
570
|
+
return date.getTime();
|
|
571
|
+
};
|
|
572
|
+
var TO_UNIX_SECONDS = (timestamp) => Math.floor(timestamp / 1000);
|
|
573
|
+
var FROM_UNIX_SECONDS = (seconds) => seconds * 1000;
|
|
376
574
|
// src/defaults.ts
|
|
377
575
|
var defaultContext = {
|
|
378
576
|
functions: {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
timestamp: (year, month, day, hour = 0, minute = 0, second = 0) => new Date(year, month - 1, day, hour, minute, second).getTime(),
|
|
394
|
-
milliseconds: (ms) => ms,
|
|
395
|
-
seconds: (s) => s * 1000,
|
|
396
|
-
minutes: (m) => m * 60 * 1000,
|
|
397
|
-
hours: (h) => h * 60 * 60 * 1000,
|
|
398
|
-
days: (d) => d * 24 * 60 * 60 * 1000,
|
|
399
|
-
weeks: (w) => w * 7 * 24 * 60 * 60 * 1000,
|
|
400
|
-
year: (timestamp) => new Date(timestamp).getFullYear(),
|
|
401
|
-
month: (timestamp) => new Date(timestamp).getMonth() + 1,
|
|
402
|
-
day: (timestamp) => new Date(timestamp).getDate(),
|
|
403
|
-
hour: (timestamp) => new Date(timestamp).getHours(),
|
|
404
|
-
minute: (timestamp) => new Date(timestamp).getMinutes(),
|
|
405
|
-
second: (timestamp) => new Date(timestamp).getSeconds(),
|
|
406
|
-
weekday: (timestamp) => new Date(timestamp).getDay()
|
|
577
|
+
ABS: Math.abs,
|
|
578
|
+
CEIL: Math.ceil,
|
|
579
|
+
FLOOR: Math.floor,
|
|
580
|
+
ROUND: Math.round,
|
|
581
|
+
SQRT: Math.sqrt,
|
|
582
|
+
MIN: Math.min,
|
|
583
|
+
MAX: Math.max,
|
|
584
|
+
SIN: Math.sin,
|
|
585
|
+
COS: Math.cos,
|
|
586
|
+
TAN: Math.tan,
|
|
587
|
+
LOG: Math.log,
|
|
588
|
+
LOG10: Math.log10,
|
|
589
|
+
EXP: Math.exp,
|
|
590
|
+
...exports_date_utils
|
|
407
591
|
}
|
|
408
592
|
};
|
|
409
593
|
// src/lexer.ts
|
|
@@ -908,8 +1092,10 @@ export {
|
|
|
908
1092
|
isBinaryOp,
|
|
909
1093
|
isAssignment,
|
|
910
1094
|
generate,
|
|
1095
|
+
extractInputVariables,
|
|
911
1096
|
execute,
|
|
912
1097
|
defaultContext,
|
|
1098
|
+
exports_date_utils as dateUtils,
|
|
913
1099
|
exports_ast as ast,
|
|
914
1100
|
TokenType,
|
|
915
1101
|
Parser,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "littlewing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "A minimal, high-performance arithmetic expression language with lexer, parser, and executor. Optimized for browsers with zero dependencies and type-safe execution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"arithmetic",
|