formulaParser property

Parser formulaParser

Construct the parser, with caching.

Implementation

Parser get formulaParser {
  if (_formulaParser != null) return _formulaParser!;

  // Start the arithmetic parser.
  // The following is taken from `package:petitparser` README.
  final builder = ExpressionBuilder<AstNode>();

  // Variables have the highest priority.
  // Take every variable and make a string parser from it.
  final variableParsers = variables.keys
      .map((key) => stringIgnoreCase(key)
          .trim()
          .map((value) => VariableNode(key, variables[key]!)))
      .toList(growable: false);

  // Create a choice (OR) parser from all the variable parsers.
  // variableParsers =
  //     variableParsers.toChoiceParser(failureJoiner: selectFarthestJoined);

  // Numbers and parenthesis are just a tad down from variables.
  final Parser<AstNode> numberParser = digit()
      .plus()
      .seq(char('.').seq(digit().plus()).optional())
      .flatten()
      .trim()
      .map((a) => NumberNode(double.parse(a)));

  // Top group
  final topGroup = builder.group();
  topGroup.primitive(numberParser);
  for (final parser in variableParsers) {
    topGroup.primitive(parser);
  }
  topGroup.wrapper<String, String>(
    char('(').trim(),
    char(')').trim(),
    (l, a, r) => ParensNode(a),
  );

  // negation is a prefix operator
  builder.group().prefix<String>(
        char('-').trim(),
        (op, a) => NegativeNode(a),
      );

  // The range operator is left associative and very high-priority.
  builder.group().left<String>(char('~').trim(), (a, op, b) {
    if (a.isStochastic) {
      throw ArgumentError('Cannot create a range with stochastic start: $a');
    }
    if (b.isStochastic) {
      throw ArgumentError('Cannot create a range with stochastic end: $b');
    }
    final aValue = a.emit();
    final bValue = b.emit();
    if (aValue == bValue) {
      print('Warning: range $aValue~$bValue is auto-converted to just '
          'the value $aValue.');
      return NumberNode(aValue);
    }
    final range = Range(aValue, bValue);
    return RangeNode(range);
  });

  // The % postfix (just divides by 100).
  builder.group()
    ..postfix(char('%').trim(), (a, op) => ConstantMultipleNode(a, 1 / 100))
    ..postfix(stringIgnoreCase('K').trim(),
        (a, op) => ConstantMultipleNode(a, 1000))
    ..postfix(stringIgnoreCase('M').trim(),
        (a, op) => ConstantMultipleNode(a, 1000000));

  // TODO: add a +- parser

  // Math functions.
  // TODO: take more from Excel / Google Spreadsheets: MOD(), POWER(),
  //       CEILING(), FLOOR(), LOG(), LN(), LOG10() EXP(), SIGN(), POW(),
  //       ABS(), CLAMP()
  //       FV(), PV(), PMT(), PPMT(), NPER(), RATE(), EFFECT(),
  //       NOMINAL(), SLN()
  //       PI, E, TAU
  builder.group()
    ..wrapper(stringIgnoreCase('sqrt(').trim(), char(')').trim(),
        (l, a, r) => SquareRootFunctionNode(a))
    ..wrapper(stringIgnoreCase('sin(').trim(), char(')').trim(),
        (l, a, r) => SineFunctionNode(a))
    ..wrapper(stringIgnoreCase('cos(').trim(), char(')').trim(),
        (l, a, r) => CosineFunctionNode(a))
    ..wrapper(stringIgnoreCase('tan(').trim(), char(')').trim(),
        (l, a, r) => TangentFunctionNode(a));

  // power is right-associative
  builder.group().right<String>(
        [char('^'), string('**')]
            .toChoiceParser(failureJoiner: selectFarthestJoined)
            .trim(),
        (a, op, b) => MathPowerNode(a, b),
      );

  // multiplication and addition are left-associative
  builder.group()
    ..left<String>(
      [char('*'), char('×'), char('·')]
          .toChoiceParser(failureJoiner: selectFarthestJoined)
          .trim(),
      (a, op, b) => MultiplicationNode(a, b),
    )
    ..left<String>(
      [char('/'), char('÷')]
          .toChoiceParser(failureJoiner: selectFarthestJoined)
          .trim(),
      (a, op, b) => DivisionNode(a, b),
    );
  builder.group()
    ..left<String>(
      char('+').trim(),
      (a, op, b) => AdditionNode(a, b),
    )
    ..left<String>(
      char('-').trim(),
      (a, op, b) => SubtractionNode(a, b),
    );

  _formulaParser = builder.build().end();
  return _formulaParser!;
}