Merge feature/formula-evaluation into master

Adds comprehensive formula evaluation capabilities using d4rt interpreter:

- FormulaEvaluator class for executing mathematical formulas
- Variable injection mechanism for d4rt compatibility
- Single output variable constraint with validation
- Comprehensive test suite with 15 test cases
- Working examples demonstrating physics and financial calculations
- Proper error handling for missing variables and invalid formulas

This enables the execution of user-defined mathematical formulas
with type-safe input validation and d4rt-based computation.
This commit is contained in:
Álvaro González 2025-08-21 18:36:08 +02:00
commit eb38cfcb0e
5 changed files with 639 additions and 1 deletions

View file

@ -0,0 +1,190 @@
/// Example demonstrating formula evaluation using the d4rt interpreter.
///
/// This example shows how to:
/// 1. Create formulas with input/output specifications
/// 2. Evaluate formulas with different input values
/// 3. Handle evaluation errors
import 'package:d4rt_formulas/d4rt_formulas.dart';
void main() {
print('=== Formula Evaluation Example ===\n');
// Create a formula evaluator
final evaluator = FormulaEvaluator();
// Example 1: Newton's Second Law (F = m * a)
print('1. Newton\'s Second Law of Motion');
final newtonFormula = Formula(
name: "Newton's Second Law",
input: {
'm': VariableSpec(magnitude: 'mass'),
'a': VariableSpec(magnitude: 'acceleration'),
},
output: {
'F': VariableSpec(magnitude: 'force'),
},
d4rtCode: '''
main() {
return m * a;
}
''',
);
try {
final force = evaluator.evaluate(newtonFormula, {
'm': 10.0, // 10 kg
'a': 9.8, // 9.8 m/s²
});
print(' Mass: 10.0 kg');
print(' Acceleration: 9.8 m/s²');
print(' Calculated Force: $force N');
print(' Output variable: ${evaluator.getOutputVariableName(newtonFormula)}');
print(' Output magnitude: ${evaluator.getOutputVariableMagnitude(newtonFormula)}');
} catch (e) {
print(' Error: $e');
}
print('');
// Example 2: Quadratic Formula Discriminant
print('2. Quadratic Formula Discriminant (Δ = b² - 4ac)');
final discriminantFormula = Formula(
name: 'Quadratic Discriminant',
input: {
'a': VariableSpec(magnitude: 'coefficient'),
'b': VariableSpec(magnitude: 'coefficient'),
'c': VariableSpec(magnitude: 'coefficient'),
},
output: {
'discriminant': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: '''
main() {
return b * b - 4 * a * c;
}
''',
);
try {
final discriminant = evaluator.evaluate(discriminantFormula, {
'a': 1,
'b': 5,
'c': 6,
});
print(' Equation: 1x² + 5x + 6 = 0');
print(' a = 1, b = 5, c = 6');
print(' Discriminant: $discriminant');
if (discriminant > 0) {
print(' → Two real solutions');
} else if (discriminant == 0) {
print(' → One real solution');
} else {
print(' → No real solutions');
}
} catch (e) {
print(' Error: $e');
}
print('');
// Example 3: Circle Area
print('3. Circle Area (A = π * r²)');
final circleAreaFormula = Formula(
name: 'Circle Area',
input: {
'r': VariableSpec(magnitude: 'length'),
},
output: {
'A': VariableSpec(magnitude: 'area'),
},
d4rtCode: '''
main() {
var pi = 3.14159265359;
return pi * r * r;
}
''',
);
try {
final area = evaluator.evaluate(circleAreaFormula, {
'r': 5.0, // radius = 5 units
});
print(' Radius: 5.0 units');
print(' Calculated Area: $area square units');
} catch (e) {
print(' Error: $e');
}
print('');
// Example 4: Error handling
print('4. Error Handling Example');
try {
// Try to evaluate with missing input variable
evaluator.evaluate(newtonFormula, {
'm': 10.0,
// Missing 'a' variable
});
} catch (e) {
print(' Expected error when missing input variable:');
print(' $e');
}
print('');
// Example 5: Complex calculation
print('5. Compound Interest Formula');
final compoundInterestFormula = Formula(
name: 'Compound Interest',
input: {
'P': VariableSpec(magnitude: 'currency'), // Principal
'r': VariableSpec(magnitude: 'rate'), // Annual interest rate
'n': VariableSpec(magnitude: 'count'), // Times compounded per year
't': VariableSpec(magnitude: 'time'), // Time in years
},
output: {
'A': VariableSpec(magnitude: 'currency'), // Final amount
},
d4rtCode: '''
main() {
// A = P * (1 + r/n)^(n*t)
var rate_per_period = r / n;
var base = 1 + rate_per_period;
var exponent = n * t;
// Calculate base^exponent using repeated multiplication
// (d4rt may not have built-in pow function)
var result = P;
for (var i = 0; i < exponent; i++) {
result = result * base;
}
return result;
}
''',
);
try {
final finalAmount = evaluator.evaluate(compoundInterestFormula, {
'P': 1000.0, // \$1000 principal
'r': 0.05, // 5% annual interest rate
'n': 12, // Compounded monthly
't': 2, // 2 years
});
print(' Principal: \$1000');
print(' Annual interest rate: 5%');
print(' Compounded: 12 times per year (monthly)');
print(' Time: 2 years');
print(' Final amount: \$$finalAmount');
} catch (e) {
print(' Error: $e');
}
print('\n=== Example Complete ===');
}

8
lib/d4rt_formulas.dart Normal file
View file

@ -0,0 +1,8 @@
/// A library for working with mathematical formulas using the d4rt interpreter.
///
/// This library provides data models for representing formulas and an evaluator
/// for executing them using the d4rt Dart interpreter.
library d4rt_formulas;
export 'formula_models.dart';
export 'formula_evaluator.dart';

148
lib/formula_evaluator.dart Normal file
View file

@ -0,0 +1,148 @@
/// Formula evaluator that executes d4rt code with input variables and
/// returns the value of the single output variable.
///
/// The evaluator assumes that:
/// - A formula has exactly one output variable
/// - The d4rt code defines a main function with parameters matching input variables
/// - The d4rt code when executed returns the value of that output variable
/// - Input variables are provided as a Map<String, dynamic>
import 'package:d4rt/d4rt.dart';
import 'formula_models.dart';
/// Exception thrown when formula evaluation fails
class FormulaEvaluationException implements Exception {
final String message;
final Object? cause;
const FormulaEvaluationException(this.message, [this.cause]);
@override
String toString() => 'FormulaEvaluationException: $message'
'${cause != null ? ' (caused by: $cause)' : ''}';
}
/// Evaluates formulas using the d4rt interpreter
class FormulaEvaluator {
final D4rt _interpreter;
/// Creates a new formula evaluator with an optional d4rt interpreter instance.
/// If no interpreter is provided, a new one will be created.
FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? D4rt();
/// Evaluates a formula with the given input values.
///
/// The [formula] must have exactly one output variable.
/// The [inputValues] map must contain values for all input variables defined
/// in the formula.
///
/// The formula's d4rt_code should define a main function that uses the input
/// variable names directly. The evaluator will inject variable declarations
/// before the formula code. For example:
/// ```
/// main() {
/// return m * a; // Returns Force = mass * acceleration
/// }
/// ```
///
/// Returns the computed value of the single output variable.
///
/// Throws [FormulaEvaluationException] if:
/// - The formula has zero or more than one output variable
/// - Required input variables are missing
/// - The d4rt code execution fails
dynamic evaluate(Formula formula, Map<String, dynamic> inputValues) {
_validateFormula(formula);
_validateInputValues(formula, inputValues);
try {
// Build the complete d4rt source code with variable declarations
final completeSource = _buildCompleteSource(formula, inputValues);
// Execute the code using d4rt (no args needed since variables are in source)
final result = _interpreter.execute(source: completeSource);
return result;
} catch (e) {
throw FormulaEvaluationException(
'Failed to execute formula "${formula.name}"',
e,
);
}
}
/// Validates that the formula has exactly one output variable
void _validateFormula(Formula formula) {
if (formula.output.length != 1) {
throw FormulaEvaluationException(
'Formula "${formula.name}" must have exactly one output variable, '
'but has ${formula.output.length}',
);
}
}
/// Validates that all required input variables are provided
void _validateInputValues(Formula formula, Map<String, dynamic> inputValues) {
final missingVars = <String>[];
for (final inputVar in formula.input.keys) {
if (!inputValues.containsKey(inputVar)) {
missingVars.add(inputVar);
}
}
if (missingVars.isNotEmpty) {
throw FormulaEvaluationException(
'Missing required input variables for formula "${formula.name}": '
'${missingVars.join(', ')}',
);
}
}
/// Gets the name of the single output variable from the formula
String getOutputVariableName(Formula formula) {
_validateFormula(formula);
return formula.output.keys.first;
}
/// Gets the magnitude of the single output variable from the formula
String getOutputVariableMagnitude(Formula formula) {
_validateFormula(formula);
return formula.output.values.first.magnitude;
}
/// Gets the ordered list of input variable names (alphabetically sorted)
List<String> getInputVariableOrder(Formula formula) {
return formula.input.keys.toList()..sort();
}
/// Builds the complete d4rt source code by injecting variable declarations
/// before the formula's d4rt code
String _buildCompleteSource(Formula formula, Map<String, dynamic> inputValues) {
final buffer = StringBuffer();
// Add variable declarations for all input variables
for (final entry in inputValues.entries) {
final varName = entry.key;
final value = entry.value;
// Handle different value types appropriately for d4rt
if (value is String) {
// Escape quotes in string values
final escapedValue = value.replaceAll('"', '\\"');
buffer.writeln('var $varName = "$escapedValue";');
} else {
// For numbers and other types, use direct representation
buffer.writeln('var $varName = $value;');
}
}
// Add a blank line for readability
buffer.writeln();
// Add the formula's d4rt code
buffer.write(formula.d4rtCode);
return buffer.toString();
}
}

View file

@ -1 +0,0 @@
import 'package:d4rt/d4rt.dart';

View file

@ -0,0 +1,293 @@
import 'package:test/test.dart';
import 'package:d4rt_formulas/formula_models.dart';
import 'package:d4rt_formulas/formula_evaluator.dart';
void main() {
group('FormulaEvaluator', () {
late FormulaEvaluator evaluator;
setUp(() {
evaluator = FormulaEvaluator();
});
group('Basic evaluation', () {
test('evaluates Newton\'s second law formula', () {
final formula = Formula(
name: "Newton's second law",
input: {
'm': VariableSpec(magnitude: 'mass'),
'a': VariableSpec(magnitude: 'acceleration'),
},
output: {
'F': VariableSpec(magnitude: 'force'),
},
d4rtCode: '''
main() {
return a * m;
}
''',
);
final result = evaluator.evaluate(formula, {
'm': 10.0, // 10 kg
'a': 9.8, // 9.8 m/s²
});
expect(result, 98.0); // F = m * a = 10 * 9.8 = 98 N
});
test('evaluates simple arithmetic formula', () {
final formula = Formula(
name: 'Simple addition',
input: {
'x': VariableSpec(magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'),
},
output: {
'result': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: '''
main() {
return x + y;
}
''',
);
final result = evaluator.evaluate(formula, {
'x': 5,
'y': 3,
});
expect(result, 8);
});
test('handles single input variable', () {
final formula = Formula(
name: 'Square function',
input: {
'n': VariableSpec(magnitude: 'scalar'),
},
output: {
'result': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: '''
main() {
return n * n;
}
''',
);
final result = evaluator.evaluate(formula, {'n': 7});
expect(result, 49);
});
test('handles complex mathematical operations', () {
final formula = Formula(
name: 'Quadratic formula discriminant',
input: {
'a': VariableSpec(magnitude: 'scalar'),
'b': VariableSpec(magnitude: 'scalar'),
'c': VariableSpec(magnitude: 'scalar'),
},
output: {
'discriminant': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: '''
main() {
return b * b - 4 * a * c;
}
''',
);
final result = evaluator.evaluate(formula, {
'a': 1,
'b': 5,
'c': 6,
});
expect(result, 1); // b² - 4ac = 25 - 24 = 1
});
});
group('Input variable order', () {
test('maintains consistent alphabetical order for input variables', () {
final formula = Formula(
name: 'Test order',
input: {
'z': VariableSpec(magnitude: 'scalar'),
'a': VariableSpec(magnitude: 'scalar'),
'b': VariableSpec(magnitude: 'scalar'),
},
output: {
'result': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: 'main() { return a + b + z; }',
);
final order = evaluator.getInputVariableOrder(formula);
expect(order, ['a', 'b', 'z']);
});
test('passes arguments in correct alphabetical order', () {
final formula = Formula(
name: 'Test argument order',
input: {
'z': VariableSpec(magnitude: 'scalar'),
'a': VariableSpec(magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'),
},
output: {
'result': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: '''
main() {
// Variables: a=1, y=2, z=3
return a * 100 + y * 10 + z;
}
''',
);
final result = evaluator.evaluate(formula, {
'z': 3,
'a': 1,
'y': 2,
});
expect(result, 123); // 1*100 + 2*10 + 3 = 123
});
});
group('Error handling', () {
test('throws exception for formula with no output variables', () {
final formula = Formula(
name: 'Invalid formula',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {}, // No output variables
d4rtCode: 'main() { return x; }',
);
expect(
() => evaluator.evaluate(formula, {'x': 1}),
throwsA(isA<FormulaEvaluationException>()),
);
});
test('throws exception for formula with multiple output variables', () {
final formula = Formula(
name: 'Invalid formula',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {
'y': VariableSpec(magnitude: 'scalar'),
'z': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: 'main(x) { return x; }',
);
expect(
() => evaluator.evaluate(formula, {'x': 1}),
throwsA(isA<FormulaEvaluationException>()),
);
});
test('throws exception for missing input variables', () {
final formula = Formula(
name: 'Test formula',
input: {
'x': VariableSpec(magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'),
},
output: {'result': VariableSpec(magnitude: 'scalar')},
d4rtCode: 'main() { return x + y; }',
);
expect(
() => evaluator.evaluate(formula, {'x': 1}), // Missing 'y'
throwsA(isA<FormulaEvaluationException>()),
);
});
test('throws exception for invalid d4rt code', () {
final formula = Formula(
name: 'Invalid code formula',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {'result': VariableSpec(magnitude: 'scalar')},
d4rtCode: 'invalid dart code here!',
);
expect(
() => evaluator.evaluate(formula, {'x': 1}),
throwsA(isA<FormulaEvaluationException>()),
);
});
});
group('Utility methods', () {
test('getOutputVariableName returns the single output variable name', () {
final formula = Formula(
name: 'Test',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {'force': VariableSpec(magnitude: 'Newton')},
d4rtCode: 'main() { return x; }',
);
expect(evaluator.getOutputVariableName(formula), 'force');
});
test('getOutputVariableMagnitude returns the output variable magnitude', () {
final formula = Formula(
name: 'Test',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {'force': VariableSpec(magnitude: 'Newton')},
d4rtCode: 'main() { return x; }',
);
expect(evaluator.getOutputVariableMagnitude(formula), 'Newton');
});
test('utility methods throw exception for invalid formulas', () {
final invalidFormula = Formula(
name: 'Invalid',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {}, // No output variables
d4rtCode: 'main() { return x; }',
);
expect(
() => evaluator.getOutputVariableName(invalidFormula),
throwsA(isA<FormulaEvaluationException>()),
);
expect(
() => evaluator.getOutputVariableMagnitude(invalidFormula),
throwsA(isA<FormulaEvaluationException>()),
);
});
});
group('Data types', () {
test('handles integer values', () {
final formula = Formula(
name: 'Integer test',
input: {'n': VariableSpec(magnitude: 'count')},
output: {'result': VariableSpec(magnitude: 'count')},
d4rtCode: 'main() { return n + 1; }',
);
final result = evaluator.evaluate(formula, {'n': 42});
expect(result, 43);
});
test('handles double values', () {
final formula = Formula(
name: 'Double test',
input: {'x': VariableSpec(magnitude: 'length')},
output: {'result': VariableSpec(magnitude: 'area')},
d4rtCode: 'main() { return x * x; }',
);
final result = evaluator.evaluate(formula, {'x': 3.14});
expect(result, closeTo(9.8596, 0.0001));
});
});
});
}