diff --git a/example/formula_evaluation_example.dart b/example/formula_evaluation_example.dart new file mode 100644 index 0000000..4f0ce55 --- /dev/null +++ b/example/formula_evaluation_example.dart @@ -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 ==='); +} diff --git a/lib/d4rt_formulas.dart b/lib/d4rt_formulas.dart new file mode 100644 index 0000000..96d691f --- /dev/null +++ b/lib/d4rt_formulas.dart @@ -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'; diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart new file mode 100644 index 0000000..617279c --- /dev/null +++ b/lib/formula_evaluator.dart @@ -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 + +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 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 inputValues) { + final missingVars = []; + + 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 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 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(); + } +} diff --git a/lib/pruebas_d4rt.dart b/lib/pruebas_d4rt.dart deleted file mode 100644 index 8d9e6a5..0000000 --- a/lib/pruebas_d4rt.dart +++ /dev/null @@ -1 +0,0 @@ -import 'package:d4rt/d4rt.dart'; diff --git a/test/formula_evaluator_test.dart b/test/formula_evaluator_test.dart new file mode 100644 index 0000000..0665c2d --- /dev/null +++ b/test/formula_evaluator_test.dart @@ -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()), + ); + }); + + 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()), + ); + }); + + 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()), + ); + }); + + 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()), + ); + }); + }); + + 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()), + ); + + expect( + () => evaluator.getOutputVariableMagnitude(invalidFormula), + throwsA(isA()), + ); + }); + }); + + 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)); + }); + }); + }); +}