@fincity/kirun-js 2.16.2 → 3.0.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.
@@ -7,6 +7,7 @@ import { ExpressionEvaluationException } from './exception/ExpressionEvaluationE
7
7
  import { ExpressionToken } from './ExpressionToken';
8
8
  import { ExpressionTokenValue } from './ExpressionTokenValue';
9
9
  import { Operation } from './Operation';
10
+ import { ExpressionParser } from './ExpressionParser';
10
11
 
11
12
  export class Expression extends ExpressionToken {
12
13
  // Data structure for storing tokens
@@ -18,27 +19,88 @@ export class Expression extends ExpressionToken {
18
19
  private cachedTokensArray?: ExpressionToken[];
19
20
  private cachedOpsArray?: Operation[];
20
21
 
22
+ /**
23
+ * Create a ternary expression with condition, trueExpr, and falseExpr.
24
+ * Using push() to maintain compatibility with evaluator (push adds to head).
25
+ * Tokens will be in order: [falseExpr, trueExpr, condition] from head to tail.
26
+ */
27
+ public static createTernary(condition: ExpressionToken, trueExpr: ExpressionToken, falseExpr: ExpressionToken): Expression {
28
+ const expr = new Expression('', undefined, undefined, undefined, true);
29
+ // Push in reverse order so get() returns them correctly
30
+ // push(condition) -> head=condition
31
+ // push(trueExpr) -> head=trueExpr, trueExpr.next=condition
32
+ // push(falseExpr) -> head=falseExpr, falseExpr.next=trueExpr.next=condition
33
+ // get(0)=falseExpr, get(1)=trueExpr, get(2)=condition
34
+ expr.tokens.push(condition);
35
+ expr.tokens.push(trueExpr);
36
+ expr.tokens.push(falseExpr);
37
+ expr.ops.push(Operation.CONDITIONAL_TERNARY_OPERATOR);
38
+ return expr;
39
+ }
40
+
41
+ /**
42
+ * Create a leaf expression (identifier/number) without re-parsing.
43
+ * Used by the parser for leaf nodes.
44
+ */
45
+ public static createLeaf(value: string): Expression {
46
+ const expr = new Expression('', undefined, undefined, undefined, true);
47
+ expr.expression = value;
48
+ // Push a token for the leaf value - the evaluator needs at least one token
49
+ expr.tokens.push(new ExpressionToken(value));
50
+ return expr;
51
+ }
52
+
21
53
  public constructor(
22
54
  expression?: string,
23
55
  l?: ExpressionToken,
24
56
  r?: ExpressionToken,
25
57
  op?: Operation,
58
+ skipParsing?: boolean,
26
59
  ) {
27
60
  super(expression ? expression : '');
28
- if (op?.getOperator() == '..') {
29
- if (!l) l = new ExpressionTokenValue('', '');
30
- else if (!r) r = new ExpressionTokenValue('', '');
31
- }
32
- if (l) this.tokens.push(l);
33
- if (r) this.tokens.push(r);
34
- if (op) this.ops.push(op);
35
- this.evaluate();
36
- if (
37
- !this.ops.isEmpty() &&
38
- this.ops.peekLast().getOperator() == '..' &&
39
- this.tokens.length == 1
40
- ) {
41
- this.tokens.push(new ExpressionToken(''));
61
+
62
+ // If skipParsing is true, don't do anything (used by createLeaf/createTernary)
63
+ if (skipParsing) {
64
+ return;
65
+ }
66
+
67
+ // If we have left/right tokens and operation, construct directly (for AST building)
68
+ // Using push() to maintain compatibility with evaluator (push adds to head).
69
+ // For binary ops: push(left), push(right) -> get(0)=right, get(1)=left
70
+ if (l !== undefined || r !== undefined || op !== undefined) {
71
+ if (op?.getOperator() == '..') {
72
+ if (!l) l = new ExpressionTokenValue('', '');
73
+ else if (!r) r = new ExpressionTokenValue('', '');
74
+ }
75
+ if (l) this.tokens.push(l);
76
+ if (r) this.tokens.push(r);
77
+ if (op) this.ops.push(op);
78
+ // For constructed expressions, don't re-parse
79
+ return;
80
+ }
81
+
82
+ // If we have an expression string, use the new parser
83
+ if (expression && expression.trim().length > 0) {
84
+ try {
85
+ const parser = new ExpressionParser(expression);
86
+ const parsed = parser.parse();
87
+ // Copy tokens and operations from parsed expression
88
+ this.tokens = parsed.getTokens();
89
+ this.ops = parsed.getOperations();
90
+ // Invalidate cache
91
+ this.cachedTokensArray = undefined;
92
+ this.cachedOpsArray = undefined;
93
+ } catch (error) {
94
+ // Fall back to old parser if new one fails (for backward compatibility during migration)
95
+ this.evaluate();
96
+ if (
97
+ !this.ops.isEmpty() &&
98
+ this.ops.peekLast().getOperator() == '..' &&
99
+ this.tokens.length == 1
100
+ ) {
101
+ this.tokens.push(new ExpressionToken(''));
102
+ }
103
+ }
42
104
  }
43
105
  }
44
106
 
@@ -442,6 +504,17 @@ export class Expression extends ExpressionToken {
442
504
  if (!pre1 || !pre2) {
443
505
  throw new Error('Unknown operators provided');
444
506
  }
507
+ // For left-associative operators with same precedence, combine the previous
508
+ // operation before adding the new one. This ensures 5 - 1 - 2 is parsed as
509
+ // (5-1) - 2, not 5 - (1-2).
510
+ // Exception: OBJECT_OPERATOR and ARRAY_OPERATOR should NOT combine -
511
+ // they need to stay flat for path building (e.g., a.b.c).
512
+ if (pre2 === pre1) {
513
+ return op2 !== Operation.OBJECT_OPERATOR &&
514
+ op2 !== Operation.ARRAY_OPERATOR &&
515
+ op1 !== Operation.OBJECT_OPERATOR &&
516
+ op1 !== Operation.ARRAY_OPERATOR;
517
+ }
445
518
  return pre2 < pre1;
446
519
  }
447
520
 
@@ -461,72 +534,195 @@ export class Expression extends ExpressionToken {
461
534
  return level === 1; // Should be 1 just before the last ')'
462
535
  }
463
536
 
537
+ /**
538
+ * Simple recursive toString() that walks the AST tree.
539
+ * The tree structure is in the tokens LinkedList:
540
+ * - For Expression('', left, right, op): tokens[0] = left, tokens[1] = right
541
+ * - For leaf nodes: just the expression string
542
+ */
464
543
  public toString(): string {
544
+ // Leaf node: no operations, just return the token
465
545
  if (this.ops.isEmpty()) {
466
- if (this.tokens.size() == 1) return this.tokens.get(0).toString();
546
+ if (this.tokens.size() == 1) {
547
+ return this.tokenToString(this.tokens.get(0));
548
+ }
549
+ // Handle special case: expression string without parsed structure
550
+ if (this.expression && this.expression.length > 0) {
551
+ return this.expression;
552
+ }
467
553
  return 'Error: No tokens';
468
554
  }
469
555
 
470
- let sb: StringBuilder = new StringBuilder();
471
- let ind: number = 0;
472
-
473
- const ops: Operation[] = this.ops.toArray();
474
- const tokens: ExpressionToken[] = this.tokens.toArray();
475
-
476
- for (let i = 0; i < ops.length; i++) {
477
- if (ops[i].getOperator().startsWith('UN: ')) {
478
- sb.append('(')
479
- .append(ops[i].getOperator().substring(4))
480
- .append(
481
- tokens[ind] instanceof Expression
482
- ? (tokens[ind] as Expression).toString()
483
- : tokens[ind],
484
- )
485
- .append(')');
486
- ind++;
487
- } else if (ops[i] == Operation.CONDITIONAL_TERNARY_OPERATOR) {
488
- let temp: ExpressionToken = tokens[ind++];
489
- sb.insert(
490
- 0,
491
- temp instanceof Expression ? (temp as Expression).toString() : temp.toString(),
492
- );
493
- sb.insert(0, ':');
494
- temp = tokens[ind++];
495
- sb.insert(
496
- 0,
497
- temp instanceof Expression ? (temp as Expression).toString() : temp.toString(),
498
- );
499
- sb.insert(0, '?');
500
- temp = tokens[ind++];
501
- sb.insert(
502
- 0,
503
- temp instanceof Expression ? (temp as Expression).toString() : temp.toString(),
504
- ).append(')');
505
- sb.insert(0, '(');
506
- } else {
507
- if (ind == 0) {
508
- const temp: ExpressionToken = tokens[ind++];
509
- sb.insert(
510
- 0,
511
- temp instanceof Expression
512
- ? (temp as Expression).toString()
513
- : temp.toString(),
514
- );
556
+ // Get the single operation and its operands
557
+ const op = this.ops.get(0);
558
+
559
+ // Unary operation: (operator operand)
560
+ if (op.getOperator().startsWith('UN: ')) {
561
+ return this.formatUnaryOperation(op);
562
+ }
563
+
564
+ // Ternary operation: (condition ? trueExpr : falseExpr)
565
+ if (op == Operation.CONDITIONAL_TERNARY_OPERATOR) {
566
+ return this.formatTernaryOperation();
567
+ }
568
+
569
+ // Binary operation
570
+ return this.formatBinaryOperation(op);
571
+ }
572
+
573
+ /**
574
+ * Format a unary operation: (operator operand)
575
+ */
576
+ private formatUnaryOperation(op: Operation): string {
577
+ const operand = this.tokens.get(0);
578
+ const operandStr = this.tokenToString(operand);
579
+ return '(' + op.getOperator().substring(4) + operandStr + ')';
580
+ }
581
+
582
+ /**
583
+ * Format a ternary operation: (condition ? trueExpr : falseExpr)
584
+ * Note: With push() order, tokens are [falseExpr, trueExpr, condition]
585
+ */
586
+ private formatTernaryOperation(): string {
587
+ // With push() order: get(0)=falseExpr, get(1)=trueExpr, get(2)=condition
588
+ const falseExpr = this.tokens.get(0);
589
+ const trueExpr = this.tokens.get(1);
590
+ const condition = this.tokens.get(2);
591
+
592
+ const conditionStr = this.tokenToString(condition);
593
+ const trueStr = this.tokenToString(trueExpr);
594
+ const falseStr = this.tokenToString(falseExpr);
595
+
596
+ return '(' + conditionStr + '?' + trueStr + ':' + falseStr + ')';
597
+ }
598
+
599
+ /**
600
+ * Format a binary operation based on operator type
601
+ * Note: With push() order, tokens are [right, left] (push(left), push(right) -> get(0)=right)
602
+ */
603
+ private formatBinaryOperation(op: Operation): string {
604
+ // Safety check: ensure we have at least 2 tokens for binary operation
605
+ if (this.tokens.size() < 2) {
606
+ // Fall back to expression string if available
607
+ if (this.expression && this.expression.length > 0) {
608
+ return this.expression;
609
+ }
610
+ // Single token case - just return the token
611
+ if (this.tokens.size() === 1) {
612
+ return this.tokenToString(this.tokens.get(0));
613
+ }
614
+ return 'Error: Invalid binary expression';
615
+ }
616
+
617
+ // With push() order: get(0)=right, get(1)=left
618
+ const right = this.tokens.get(0);
619
+ const left = this.tokens.get(1);
620
+
621
+ // ARRAY_OPERATOR: left[index] - NO outer parens, brackets are enough
622
+ if (op == Operation.ARRAY_OPERATOR) {
623
+ const leftStr = this.tokenToString(left);
624
+ const indexStr = this.formatArrayIndex(right);
625
+ return leftStr + '[' + indexStr + ']';
626
+ }
627
+
628
+ // OBJECT_OPERATOR: left.right or left.(right) if right has operations
629
+ if (op == Operation.OBJECT_OPERATOR) {
630
+ const leftStr = this.tokenToString(left);
631
+ const rightStr = this.tokenToString(right);
632
+
633
+ // Check what operation the right side has
634
+ if (right instanceof Expression) {
635
+ const rightOps = right.getOperations();
636
+ if (!rightOps.isEmpty()) {
637
+ const rightOp = rightOps.get(0);
638
+ // ARRAY_OPERATOR doesn't add outer parens, so we need to wrap
639
+ // OBJECT_OPERATOR and other ops already add parens in their toString()
640
+ if (rightOp == Operation.ARRAY_OPERATOR) {
641
+ return '(' + leftStr + '.(' + rightStr + '))';
642
+ } else {
643
+ // Other ops (like OBJECT_OPERATOR) already include parens
644
+ return '(' + leftStr + '.' + rightStr + ')';
645
+ }
646
+ }
647
+ }
648
+
649
+ // No operations - simple identifier
650
+ return '(' + leftStr + '.' + rightStr + ')';
651
+ }
652
+
653
+ // ARRAY_RANGE_INDEX_OPERATOR: (start..end)
654
+ if (op == Operation.ARRAY_RANGE_INDEX_OPERATOR) {
655
+ const leftStr = this.tokenToString(left);
656
+ const rightStr = this.tokenToString(right);
657
+ return '(' + leftStr + '..' + rightStr + ')';
658
+ }
659
+
660
+ // Other binary operators: (left op right)
661
+ const leftStr = this.tokenToString(left);
662
+ const rightStr = this.tokenToString(right);
663
+ return '(' + leftStr + op.getOperator() + rightStr + ')';
664
+ }
665
+
666
+ /**
667
+ * Convert a token to string, handling nested expressions recursively
668
+ */
669
+ private tokenToString(token: ExpressionToken): string {
670
+ if (token instanceof Expression) {
671
+ return token.toString();
672
+ }
673
+
674
+ // Check if token is an ExpressionTokenValue (use duck typing for minified code)
675
+ if (token && typeof (token as any).getExpression === 'function' &&
676
+ typeof (token as any).getTokenValue === 'function') {
677
+ const originalExpr = (token as any).getExpression();
678
+ // If it's a quoted string, use the original expression with quotes
679
+ if (originalExpr && (originalExpr.startsWith('"') || originalExpr.startsWith("'"))) {
680
+ return originalExpr;
681
+ }
682
+ }
683
+
684
+ return token.toString();
685
+ }
686
+
687
+ /**
688
+ * Format array index, preserving quotes for bracket notation
689
+ */
690
+ private formatArrayIndex(token: ExpressionToken): string {
691
+ if (token instanceof Expression) {
692
+ // Check if this is a simple quoted string
693
+ const expr = token as Expression;
694
+ if (expr.getOperations().isEmpty() && expr.getTokens().size() == 1) {
695
+ const innerToken = expr.getTokens().get(0);
696
+ // Check if innerToken is an ExpressionTokenValue with quoted expression
697
+ if (innerToken && typeof (innerToken as any).getExpression === 'function') {
698
+ const originalExpr = (innerToken as any).getExpression();
699
+ if (originalExpr && (originalExpr.startsWith('"') || originalExpr.startsWith("'"))) {
700
+ return originalExpr;
701
+ }
515
702
  }
516
- const temp: ExpressionToken = tokens[ind++];
517
- sb.insert(0, ops[i].getOperator())
518
- .insert(
519
- 0,
520
- temp instanceof Expression
521
- ? (temp as Expression).toString()
522
- : temp?.toString(),
523
- )
524
- .insert(0, '(')
525
- .append(')');
703
+ }
704
+ return expr.toString();
705
+ }
706
+
707
+ // Check if token is an ExpressionTokenValue with quoted expression
708
+ if (token && typeof (token as any).getExpression === 'function') {
709
+ const originalExpr = (token as any).getExpression();
710
+ if (originalExpr && (originalExpr.startsWith('"') || originalExpr.startsWith("'"))) {
711
+ return originalExpr;
526
712
  }
527
713
  }
714
+
715
+ return token.toString();
716
+ }
528
717
 
529
- return sb.toString();
718
+ /**
719
+ * Check if a token is an Expression with operations
720
+ */
721
+ private tokenHasOperations(token: ExpressionToken): boolean {
722
+ if (token instanceof Expression) {
723
+ return !token.getOperations().isEmpty();
724
+ }
725
+ return false;
530
726
  }
531
727
 
532
728
  public equals(o: Expression): boolean {