diff --git a/README.md b/README.md index e7685f0..67c244b 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,8 @@ The file is a json array of formulas. "magnitude" : "Force" } }, - "d4rt_code":{ - "return m*a;" - } + "d4rt_code": "return m*a;" + } ] diff --git a/bin/pruebas_d4rt.dart b/bin/pruebas_d4rt.dart index 15cc375..f9da874 100644 --- a/bin/pruebas_d4rt.dart +++ b/bin/pruebas_d4rt.dart @@ -1,4 +1,4 @@ -import 'package:pruebas_d4rt/pruebas_d4rt.dart' as pruebas_d4rt; +import 'package:d4rt_formulas/d4rt_formulas.dart' as pruebas_d4rt; import 'package:d4rt/d4rt.dart'; void main() { diff --git a/example/formula_evaluation_example.dart b/example/formula_evaluation_example.dart index 4f0ce55..333a3ce 100644 --- a/example/formula_evaluation_example.dart +++ b/example/formula_evaluation_example.dart @@ -17,13 +17,11 @@ void main() { 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'), - }, + input: [ + VariableSpec(name: 'm', magnitude: 'mass'), + VariableSpec(name: 'a', magnitude: 'acceleration'), + ], + output: VariableSpec(name: 'F', magnitude: 'force'), d4rtCode: ''' main() { return m * a; @@ -52,14 +50,12 @@ void main() { 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'), - }, + input: [ + VariableSpec(name: 'a', magnitude: 'coefficient'), + VariableSpec(name: 'b', magnitude: 'coefficient'), + VariableSpec(name: 'c', magnitude: 'coefficient'), + ], + output : VariableSpec(name: 'discriminant', magnitude: 'scalar'), d4rtCode: ''' main() { return b * b - 4 * a * c; @@ -95,12 +91,11 @@ void main() { print('3. Circle Area (A = π * r²)'); final circleAreaFormula = Formula( name: 'Circle Area', - input: { - 'r': VariableSpec(magnitude: 'length'), - }, - output: { - 'A': VariableSpec(magnitude: 'area'), - }, + input: [ + VariableSpec(name: 'r', magnitude: 'length'), + ], + output: VariableSpec(name: 'A', magnitude: 'area'), + d4rtCode: ''' main() { var pi = 3.14159265359; @@ -141,15 +136,13 @@ void main() { 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 - }, + input: [ + VariableSpec(name: 'P', magnitude: 'currency'), // Principal + VariableSpec(name: 'r', magnitude: 'rate'), // Annual interest rate + VariableSpec(name: 'n', magnitude: 'count'), // Times compounded per year + VariableSpec(name: 't', magnitude: 'time'), // Time in years + ], + output: VariableSpec(name: 'A', magnitude: 'currency'), // Final amount d4rtCode: ''' main() { // A = P * (1 + r/n)^(n*t) diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart index d1bd957..65e3a89 100644 --- a/lib/formula_evaluator.dart +++ b/lib/formula_evaluator.dart @@ -1,11 +1,3 @@ -/// 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'; @@ -73,7 +65,7 @@ class FormulaEvaluator { void _validateInputValues(Formula formula, Map inputValues) { final missingVars = []; - for (final inputVar in formula.input.keys) { + for (final inputVar in formula.inputVarNames()) { if (!inputValues.containsKey(inputVar)) { missingVars.add(inputVar); } @@ -89,19 +81,18 @@ class FormulaEvaluator { /// Gets the name of the single output variable from the formula String getOutputVariableName(Formula formula) { - // Formula construction already ensures exactly one output variable - return formula.output.keys.first; + return formula.output.name; } /// Gets the magnitude of the single output variable from the formula String getOutputVariableMagnitude(Formula formula) { // Formula construction already ensures exactly one output variable - return formula.output.values.first.magnitude; + return formula.output.magnitude; } /// Gets the ordered list of input variable names (alphabetically sorted) List getInputVariableOrder(Formula formula) { - return formula.input.keys.toList()..sort(); + return formula.inputVarNames()..sort(); } /// Builds the complete d4rt source code by injecting variable declarations diff --git a/lib/formula_models.dart b/lib/formula_models.dart index a7f0f5e..8d48cc8 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -1,49 +1,33 @@ -/// Data classes to represent a formula and its I/O specs. -/// -/// Structure (from README.md): -/// - name: String -/// - input: { varName: { magnitude: String } } -/// - output: { varName: { magnitude: String } } -/// - d4rt_code: { code: String } or String -/// -/// - Accept d4rt_code as either a plain string or an object { code: "..." }. + +import 'package:d4rt/d4rt.dart'; class VariableSpec { + + final String name; final String magnitude; + static final MAGNITUDELESS = "magnitudeless"; - const VariableSpec({required this.magnitude}); - - factory VariableSpec.fromJson(Map json) { - final mag = json['magnitude']; - if (mag is! String || mag.trim().isEmpty) { - throw FormatException("'magnitude' must be a non-empty string"); - } - return VariableSpec(magnitude: mag); - } - - Map toJson() => {'magnitude': magnitude}; + VariableSpec({required this.name, required this.magnitude}); @override - String toString() => 'VariableSpec(magnitude: $magnitude)'; - - VariableSpec copyWith({String? magnitude}) => - VariableSpec(magnitude: magnitude ?? this.magnitude); + String toString() => 'var($name: $magnitude)'; @override bool operator ==(Object other) => identical(this, other) || other is VariableSpec && runtimeType == other.runtimeType && - magnitude == other.magnitude; + magnitude == other.magnitude && + name == other.name; @override - int get hashCode => magnitude.hashCode; + int get hashCode => Object.hash(magnitude, name); } class Formula { final String name; - final Map input; // Supports multiple input variables - final Map output; // Supports multiple output variables + final List input; + final VariableSpec output; final String d4rtCode; Formula({ @@ -59,116 +43,27 @@ class Formula { if (name.trim().isEmpty) { throw ArgumentError('Formula name cannot be empty'); } - if (output.length != 1) { - throw ArgumentError( - 'Formula "$name" must have exactly one output variable, ' - 'but has ${output.length}', - ); - } } - factory Formula.fromJson(Map json) { - final name = json['name']; - if (name is! String || name.trim().isEmpty) { - throw FormatException('Formula requires a non-empty name string'); - } - - Map parseVars(dynamic obj, String fieldName) { - if (obj == null) return {}; - if (obj is! Map) { - throw FormatException('$fieldName must be an object map'); - } - return obj.map((k, v) { - if (v is! Map) { - throw FormatException( - 'Variable "$k" in $fieldName must be an object', - ); - } - return MapEntry(k, VariableSpec.fromJson(v)); - }); - } - - String parseCode(dynamic code) { - if (code == null) { - throw FormatException('d4rt_code is required'); - } - if (code is String) return code; - if (code is Map) { - final explicit = code['code']; - if (explicit is String) return explicit; - for (final entry in code.entries) { - if (entry.value is String) return entry.value as String; - } - } - throw FormatException( - 'd4rt_code must be a string or an object containing a code string', - ); - } - - final input = parseVars(json['input'], 'input'); - final output = parseVars(json['output'], 'output'); - final d4rtCode = parseCode(json['d4rt_code']); - - return Formula( - name: name, - input: input, - output: output, - d4rtCode: d4rtCode, - ); - } - - Map toJson() => { - 'name': name, - 'input': input.map((k, v) => MapEntry(k, v.toJson())), - 'output': output.map((k, v) => MapEntry(k, v.toJson())), - 'd4rt_code': {'code': d4rtCode}, - }; @override String toString() => 'Formula(name: $name, input: $input, output: $output, d4rtCode: $d4rtCode)'; - Formula copyWith({ - String? name, - Map? input, - Map? output, - String? d4rtCode, - }) => Formula( - name: name ?? this.name, - input: input ?? this.input, - output: output ?? this.output, - d4rtCode: d4rtCode ?? this.d4rtCode, - ); - @override bool operator ==(Object other) => identical(this, other) || other is Formula && runtimeType == other.runtimeType && name == other.name && - _mapEquals(input, other.input) && - _mapEquals(output, other.output) && + output == other.output && + ListEquality().equals(input, other.input) && d4rtCode == other.d4rtCode; @override int get hashCode => - Object.hash(name, _mapHash(input), _mapHash(output), d4rtCode); + Object.hash(name, ListEquality().hash(input), output, d4rtCode); + + List inputVarNames() => input.map( (v) => v.name ).toList(growable: false); } -bool _mapEquals(Map a, Map b) { - if (identical(a, b)) return true; - if (a.length != b.length) return false; - for (final entry in a.entries) { - if (!b.containsKey(entry.key)) return false; - if (b[entry.key] != entry.value) return false; - } - return true; -} - -int _mapHash(Map m) { - var h = 0; - for (final e in m.entries) { - h = h ^ Object.hash(e.key, e.value); - } - return h; -} diff --git a/test/formula_evaluator_test.dart b/test/formula_evaluator_test.dart index b06bafa..8988853 100644 --- a/test/formula_evaluator_test.dart +++ b/test/formula_evaluator_test.dart @@ -14,11 +14,11 @@ void main() { 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')}, + input: [ + VariableSpec(name: 'm', magnitude: 'mass'), + VariableSpec(name: 'a', magnitude: 'acceleration'), + ], + output: VariableSpec(name: 'F', magnitude: 'force'), d4rtCode: ''' main() { return a * m; @@ -37,11 +37,11 @@ void main() { 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')}, + input: [ + VariableSpec(name: 'x', magnitude: 'scalar'), + VariableSpec(name: 'y', magnitude: 'scalar'), + ], + output: VariableSpec(name: 'result', magnitude: 'scalar'), d4rtCode: ''' main() { return x + y; @@ -57,8 +57,8 @@ void main() { test('handles single input variable', () { final formula = Formula( name: 'Square function', - input: {'n': VariableSpec(magnitude: 'scalar')}, - output: {'result': VariableSpec(magnitude: 'scalar')}, + input: [VariableSpec(name: 'n', magnitude: 'scalar')], + output: VariableSpec(name: 'result', magnitude: 'scalar'), d4rtCode: ''' main() { return n * n; @@ -73,12 +73,12 @@ void main() { 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')}, + input: [ + VariableSpec(name: 'a', magnitude: 'scalar'), + VariableSpec(name: 'b', magnitude: 'scalar'), + VariableSpec(name: 'c', magnitude: 'scalar'), + ], + output: VariableSpec(name: 'discriminant', magnitude: 'scalar'), d4rtCode: ''' main() { return b * b - 4 * a * c; @@ -96,12 +96,12 @@ void main() { 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')}, + input: [ + VariableSpec(name: 'z', magnitude: 'scalar'), + VariableSpec(name: 'a', magnitude: 'scalar'), + VariableSpec(name: 'b', magnitude: 'scalar'), + ], + output: VariableSpec(name: 'result', magnitude: 'scalar'), d4rtCode: 'main() { return a + b + z; }', ); @@ -112,12 +112,12 @@ void main() { 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')}, + input: [ + VariableSpec(name: 'z', magnitude: 'scalar'), + VariableSpec(name: 'a', magnitude: 'scalar'), + VariableSpec(name: 'y', magnitude: 'scalar'), + ], + output: VariableSpec(name: 'result', magnitude: 'scalar'), d4rtCode: ''' main() { // Variables: a=1, y=2, z=3 @@ -133,46 +133,15 @@ void main() { }); group('Error handling', () { - test( - 'throws exception for formula with no output variables during construction', - () { - expect(() { - return Formula( - name: 'Invalid formula', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: {}, // No output variables - d4rtCode: 'main() { return x; }', - ); - }, throwsA(isA())); - }, - ); - - test( - 'throws exception for formula with multiple output variables during construction', - () { - expect( - () => Formula( - name: 'Invalid formula', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: { - 'y': VariableSpec(magnitude: 'scalar'), - 'z': VariableSpec(magnitude: 'scalar'), - }, - d4rtCode: 'main() { return x; }', - ), - 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')}, + input: [ + VariableSpec(name: 'x', magnitude: 'scalar'), + VariableSpec(name: 'y', magnitude: 'scalar'), + ], + output: VariableSpec(name: 'result', magnitude: 'scalar'), d4rtCode: 'main() { return x + y; }', ); @@ -185,8 +154,8 @@ void main() { test('throws exception for invalid d4rt code', () { final formula = Formula( name: 'Invalid code formula', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: {'result': VariableSpec(magnitude: 'scalar')}, + input: [VariableSpec(name: 'x', magnitude: 'scalar')], + output: VariableSpec(name: 'result', magnitude: 'scalar'), d4rtCode: 'invalid dart code here!', ); @@ -201,8 +170,8 @@ void main() { test('getOutputVariableName returns the single output variable name', () { final formula = Formula( name: 'Test', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: {'force': VariableSpec(magnitude: 'Newton')}, + input: [VariableSpec(name: 'x', magnitude: 'scalar')], + output: VariableSpec(name: 'force', magnitude: 'Newton'), d4rtCode: 'main() { return x; }', ); @@ -214,8 +183,8 @@ void main() { () { final formula = Formula( name: 'Test', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: {'force': VariableSpec(magnitude: 'Newton')}, + input: [VariableSpec(name: 'x', magnitude: 'scalar')], + output: VariableSpec(name: 'force', magnitude: 'Newton'), d4rtCode: 'main() { return x; }', ); @@ -226,8 +195,8 @@ void main() { test('utility methods work correctly with valid formulas', () { final validFormula = Formula( name: 'Valid Formula', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: {'result': VariableSpec(magnitude: 'Newton')}, + input: [VariableSpec(name: 'x', magnitude: 'scalar')], + output: VariableSpec(name: 'result', magnitude: 'Newton'), d4rtCode: 'main() { return x; }', ); @@ -235,39 +204,14 @@ void main() { expect(evaluator.getOutputVariableMagnitude(validFormula), 'Newton'); }); - test('validates formula construction with factory method', () { - // Test the factory method validation - expect( - () => Formula( - name: 'Invalid', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: {}, // No output variables - d4rtCode: 'main() { return x; }', - ), - throwsA(isA()), - ); - - expect( - () => Formula( - name: 'Invalid', - input: {'x': VariableSpec(magnitude: 'scalar')}, - output: { - 'y': VariableSpec(magnitude: 'scalar'), - 'z': VariableSpec(magnitude: 'scalar'), - }, - d4rtCode: 'main() { return x; }', - ), - 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')}, + input: [VariableSpec(name: 'n', magnitude: 'count')], + output: VariableSpec(name: 'result', magnitude: 'count'), d4rtCode: 'main() { return n + 1; }', ); @@ -278,8 +222,8 @@ void main() { test('handles double values', () { final formula = Formula( name: 'Double test', - input: {'x': VariableSpec(magnitude: 'length')}, - output: {'result': VariableSpec(magnitude: 'area')}, + input: [VariableSpec(name: 'x', magnitude: 'length')], + output: VariableSpec(name: 'result', magnitude: 'area'), d4rtCode: 'main() { return x * x; }', ); diff --git a/test/formula_models_test.dart b/test/formula_models_test.dart index a77fdcd..95ce74a 100644 --- a/test/formula_models_test.dart +++ b/test/formula_models_test.dart @@ -4,71 +4,4 @@ import 'package:test/test.dart'; import 'package:d4rt_formulas/formula_models.dart'; void main() { - group('Formula models', () { - test('parses a formula with multiple input variables', () { - const jsonStr = ''' - { - "name": "Newton's second law (scalar)", - "input": { - "m": {"magnitude": "mass"}, - "a": {"magnitude": "acceleration"} - }, - "output": { - "F": {"magnitude": "Force"} - }, - "d4rt_code": {"code": "return m*a;"} - } - '''; - - final map = jsonDecode(jsonStr) as Map; - final f = Formula.fromJson(map); - - expect(f.name, "Newton's second law (scalar)"); - expect(f.input.length, 2); - expect(f.input['m']!.magnitude, 'mass'); - expect(f.input['a']!.magnitude, 'acceleration'); - expect(f.output.length, 1); - expect(f.output['F']!.magnitude, 'Force'); - expect(f.d4rtCode, 'return m*a;'); - - final back = f.toJson(); - expect(back['name'], f.name); - expect((back['input'] as Map)['m']['magnitude'], 'mass'); - expect((back['input'] as Map)['a']['magnitude'], 'acceleration'); - expect((back['output'] as Map)['F']['magnitude'], 'Force'); - expect((back['d4rt_code'] as Map)['code'], 'return m*a;'); - }); - - test('rejects typo magitude (must be magnitude)', () { - const badJson = ''' - { - "name": "Bad formula", - "input": { - "x": {"magitude": "oops"} - }, - "output": { - "y": {"magnitude": "ok"} - }, - "d4rt_code": "return x;" - } - '''; - - final map = jsonDecode(badJson) as Map; - expect(() => Formula.fromJson(map), throwsFormatException); - }); - - test('accepts d4rt_code as a plain string', () { - const jsonStr = ''' - { - "name": "Simple", - "input": {"x": {"magnitude": "scalar"}}, - "output": {"y": {"magnitude": "scalar"}}, - "d4rt_code": "return x;" - } - '''; - final map = jsonDecode(jsonStr) as Map; - final f = Formula.fromJson(map); - expect(f.d4rtCode, 'return x;'); - }); - }); }