Merge branch 'feature/validation-refactor'

This commit is contained in:
Álvaro González 2025-08-22 19:57:14 +02:00
commit 2973dfd3ff
5 changed files with 103 additions and 102 deletions

2
.gitignore vendored
View file

@ -11,3 +11,5 @@
.local/
.zcompdump
.zshrc
.idea
/d4rt-formulas.iml

View file

@ -5,7 +5,6 @@ A comprehensive command-line application for managing and computing mathematical
This project uses dart language, and flutter framework. It leverages d4rt library to execute formulas.
# Development guidelines
If you are a contributor or an agent, please follow [CLAUDE.md](./CLAUDE.md) for development guidelines.

View file

@ -32,7 +32,7 @@ class FormulaEvaluator {
/// Evaluates a formula with the given input values.
///
/// The [formula] must have exactly one output variable.
/// The [formula] must have exactly one output variable (validated during construction).
/// The [inputValues] map must contain values for all input variables defined
/// in the formula.
///
@ -48,11 +48,9 @@ class FormulaEvaluator {
/// 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 {
@ -71,16 +69,6 @@ class FormulaEvaluator {
}
}
/// 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>[];
@ -101,13 +89,13 @@ class FormulaEvaluator {
/// Gets the name of the single output variable from the formula
String getOutputVariableName(Formula formula) {
_validateFormula(formula);
// Formula construction already ensures exactly one output variable
return formula.output.keys.first;
}
/// Gets the magnitude of the single output variable from the formula
String getOutputVariableMagnitude(Formula formula) {
_validateFormula(formula);
// Formula construction already ensures exactly one output variable
return formula.output.values.first.magnitude;
}

View file

@ -46,12 +46,26 @@ class Formula {
final Map<String, VariableSpec> output; // Supports multiple output variables
final String d4rtCode;
const Formula({
Formula({
required this.name,
required this.input,
required this.output,
required this.d4rtCode,
});
}){
validate();
}
validate() {
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<String, dynamic> json) {
final name = json['name'];

View file

@ -18,9 +18,7 @@ void main() {
'm': VariableSpec(magnitude: 'mass'),
'a': VariableSpec(magnitude: 'acceleration'),
},
output: {
'F': VariableSpec(magnitude: 'force'),
},
output: {'F': VariableSpec(magnitude: 'force')},
d4rtCode: '''
main() {
return a * m;
@ -43,9 +41,7 @@ void main() {
'x': VariableSpec(magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'),
},
output: {
'result': VariableSpec(magnitude: 'scalar'),
},
output: {'result': VariableSpec(magnitude: 'scalar')},
d4rtCode: '''
main() {
return x + y;
@ -53,10 +49,7 @@ void main() {
''',
);
final result = evaluator.evaluate(formula, {
'x': 5,
'y': 3,
});
final result = evaluator.evaluate(formula, {'x': 5, 'y': 3});
expect(result, 8);
});
@ -64,12 +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: {'n': VariableSpec(magnitude: 'scalar')},
output: {'result': VariableSpec(magnitude: 'scalar')},
d4rtCode: '''
main() {
return n * n;
@ -89,9 +78,7 @@ void main() {
'b': VariableSpec(magnitude: 'scalar'),
'c': VariableSpec(magnitude: 'scalar'),
},
output: {
'discriminant': VariableSpec(magnitude: 'scalar'),
},
output: {'discriminant': VariableSpec(magnitude: 'scalar')},
d4rtCode: '''
main() {
return b * b - 4 * a * c;
@ -99,11 +86,7 @@ void main() {
''',
);
final result = evaluator.evaluate(formula, {
'a': 1,
'b': 5,
'c': 6,
});
final result = evaluator.evaluate(formula, {'a': 1, 'b': 5, 'c': 6});
expect(result, 1); // b² - 4ac = 25 - 24 = 1
});
@ -118,9 +101,7 @@ void main() {
'a': VariableSpec(magnitude: 'scalar'),
'b': VariableSpec(magnitude: 'scalar'),
},
output: {
'result': VariableSpec(magnitude: 'scalar'),
},
output: {'result': VariableSpec(magnitude: 'scalar')},
d4rtCode: 'main() { return a + b + z; }',
);
@ -136,9 +117,7 @@ void main() {
'a': VariableSpec(magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'),
},
output: {
'result': VariableSpec(magnitude: 'scalar'),
},
output: {'result': VariableSpec(magnitude: 'scalar')},
d4rtCode: '''
main() {
// Variables: a=1, y=2, z=3
@ -147,47 +126,44 @@ void main() {
''',
);
final result = evaluator.evaluate(formula, {
'z': 3,
'a': 1,
'y': 2,
});
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(
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; }',
);
expect(
() => evaluator.evaluate(formula, {'x': 1}),
throwsA(isA<FormulaEvaluationException>()),
}, throwsA(isA<ArgumentError>()));
},
);
});
test('throws exception for formula with multiple output variables', () {
final formula = Formula(
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(x) { return x; }',
d4rtCode: 'main() { return x; }',
),
throwsA(isA<ArgumentError>()),
);
expect(
() => evaluator.evaluate(formula, {'x': 1}),
throwsA(isA<FormulaEvaluationException>()),
},
);
});
test('throws exception for missing input variables', () {
final formula = Formula(
@ -233,7 +209,9 @@ void main() {
expect(evaluator.getOutputVariableName(formula), 'force');
});
test('getOutputVariableMagnitude returns the output variable magnitude', () {
test(
'getOutputVariableMagnitude returns the output variable magnitude',
() {
final formula = Formula(
name: 'Test',
input: {'x': VariableSpec(magnitude: 'scalar')},
@ -242,24 +220,44 @@ void main() {
);
expect(evaluator.getOutputVariableMagnitude(formula), 'Newton');
},
);
test('utility methods work correctly with valid formulas', () {
final validFormula = Formula(
name: 'Valid Formula',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {'result': VariableSpec(magnitude: 'Newton')},
d4rtCode: 'main() { return x; }',
);
expect(evaluator.getOutputVariableName(validFormula), 'result');
expect(evaluator.getOutputVariableMagnitude(validFormula), 'Newton');
});
test('utility methods throw exception for invalid formulas', () {
final invalidFormula = Formula(
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<ArgumentError>()),
);
expect(
() => evaluator.getOutputVariableName(invalidFormula),
throwsA(isA<FormulaEvaluationException>()),
);
expect(
() => evaluator.getOutputVariableMagnitude(invalidFormula),
throwsA(isA<FormulaEvaluationException>()),
() => Formula(
name: 'Invalid',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {
'y': VariableSpec(magnitude: 'scalar'),
'z': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: 'main() { return x; }',
),
throwsA(isA<ArgumentError>()),
);
});
});