VariableSpec includes var name

This commit is contained in:
Álvaro González 2025-08-24 11:52:34 +02:00
parent 2973dfd3ff
commit 52590cd2fb
7 changed files with 94 additions and 339 deletions

View file

@ -29,9 +29,8 @@ The file is a json array of formulas.
"magnitude" : "Force" "magnitude" : "Force"
} }
}, },
"d4rt_code":{ "d4rt_code": "return m*a;"
"return m*a;"
}
} }
] ]

View file

@ -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'; import 'package:d4rt/d4rt.dart';
void main() { void main() {

View file

@ -17,13 +17,11 @@ void main() {
print('1. Newton\'s Second Law of Motion'); print('1. Newton\'s Second Law of Motion');
final newtonFormula = Formula( final newtonFormula = Formula(
name: "Newton's Second Law", name: "Newton's Second Law",
input: { input: [
'm': VariableSpec(magnitude: 'mass'), VariableSpec(name: 'm', magnitude: 'mass'),
'a': VariableSpec(magnitude: 'acceleration'), VariableSpec(name: 'a', magnitude: 'acceleration'),
}, ],
output: { output: VariableSpec(name: 'F', magnitude: 'force'),
'F': VariableSpec(magnitude: 'force'),
},
d4rtCode: ''' d4rtCode: '''
main() { main() {
return m * a; return m * a;
@ -52,14 +50,12 @@ void main() {
print('2. Quadratic Formula Discriminant (Δ = b² - 4ac)'); print('2. Quadratic Formula Discriminant (Δ = b² - 4ac)');
final discriminantFormula = Formula( final discriminantFormula = Formula(
name: 'Quadratic Discriminant', name: 'Quadratic Discriminant',
input: { input: [
'a': VariableSpec(magnitude: 'coefficient'), VariableSpec(name: 'a', magnitude: 'coefficient'),
'b': VariableSpec(magnitude: 'coefficient'), VariableSpec(name: 'b', magnitude: 'coefficient'),
'c': VariableSpec(magnitude: 'coefficient'), VariableSpec(name: 'c', magnitude: 'coefficient'),
}, ],
output: { output : VariableSpec(name: 'discriminant', magnitude: 'scalar'),
'discriminant': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: ''' d4rtCode: '''
main() { main() {
return b * b - 4 * a * c; return b * b - 4 * a * c;
@ -95,12 +91,11 @@ void main() {
print('3. Circle Area (A = π * r²)'); print('3. Circle Area (A = π * r²)');
final circleAreaFormula = Formula( final circleAreaFormula = Formula(
name: 'Circle Area', name: 'Circle Area',
input: { input: [
'r': VariableSpec(magnitude: 'length'), VariableSpec(name: 'r', magnitude: 'length'),
}, ],
output: { output: VariableSpec(name: 'A', magnitude: 'area'),
'A': VariableSpec(magnitude: 'area'),
},
d4rtCode: ''' d4rtCode: '''
main() { main() {
var pi = 3.14159265359; var pi = 3.14159265359;
@ -141,15 +136,13 @@ void main() {
print('5. Compound Interest Formula'); print('5. Compound Interest Formula');
final compoundInterestFormula = Formula( final compoundInterestFormula = Formula(
name: 'Compound Interest', name: 'Compound Interest',
input: { input: [
'P': VariableSpec(magnitude: 'currency'), // Principal VariableSpec(name: 'P', magnitude: 'currency'), // Principal
'r': VariableSpec(magnitude: 'rate'), // Annual interest rate VariableSpec(name: 'r', magnitude: 'rate'), // Annual interest rate
'n': VariableSpec(magnitude: 'count'), // Times compounded per year VariableSpec(name: 'n', magnitude: 'count'), // Times compounded per year
't': VariableSpec(magnitude: 'time'), // Time in years VariableSpec(name: 't', magnitude: 'time'), // Time in years
}, ],
output: { output: VariableSpec(name: 'A', magnitude: 'currency'), // Final amount
'A': VariableSpec(magnitude: 'currency'), // Final amount
},
d4rtCode: ''' d4rtCode: '''
main() { main() {
// A = P * (1 + r/n)^(n*t) // A = P * (1 + r/n)^(n*t)

View file

@ -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<String, dynamic>
import 'package:d4rt/d4rt.dart'; import 'package:d4rt/d4rt.dart';
import 'formula_models.dart'; import 'formula_models.dart';
@ -73,7 +65,7 @@ class FormulaEvaluator {
void _validateInputValues(Formula formula, Map<String, dynamic> inputValues) { void _validateInputValues(Formula formula, Map<String, dynamic> inputValues) {
final missingVars = <String>[]; final missingVars = <String>[];
for (final inputVar in formula.input.keys) { for (final inputVar in formula.inputVarNames()) {
if (!inputValues.containsKey(inputVar)) { if (!inputValues.containsKey(inputVar)) {
missingVars.add(inputVar); missingVars.add(inputVar);
} }
@ -89,19 +81,18 @@ class FormulaEvaluator {
/// Gets the name of the single output variable from the formula /// Gets the name of the single output variable from the formula
String getOutputVariableName(Formula formula) { String getOutputVariableName(Formula formula) {
// Formula construction already ensures exactly one output variable return formula.output.name;
return formula.output.keys.first;
} }
/// Gets the magnitude of the single output variable from the formula /// Gets the magnitude of the single output variable from the formula
String getOutputVariableMagnitude(Formula formula) { String getOutputVariableMagnitude(Formula formula) {
// Formula construction already ensures exactly one output variable // 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) /// Gets the ordered list of input variable names (alphabetically sorted)
List<String> getInputVariableOrder(Formula formula) { List<String> getInputVariableOrder(Formula formula) {
return formula.input.keys.toList()..sort(); return formula.inputVarNames()..sort();
} }
/// Builds the complete d4rt source code by injecting variable declarations /// Builds the complete d4rt source code by injecting variable declarations

View file

@ -1,49 +1,33 @@
/// Data classes to represent a formula and its I/O specs.
/// import 'package:d4rt/d4rt.dart';
/// 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: "..." }.
class VariableSpec { class VariableSpec {
final String name;
final String magnitude; final String magnitude;
static final MAGNITUDELESS = "magnitudeless";
const VariableSpec({required this.magnitude}); VariableSpec({required this.name, required this.magnitude});
factory VariableSpec.fromJson(Map<String, dynamic> 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<String, dynamic> toJson() => {'magnitude': magnitude};
@override @override
String toString() => 'VariableSpec(magnitude: $magnitude)'; String toString() => 'var($name: $magnitude)';
VariableSpec copyWith({String? magnitude}) =>
VariableSpec(magnitude: magnitude ?? this.magnitude);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is VariableSpec && other is VariableSpec &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
magnitude == other.magnitude; magnitude == other.magnitude &&
name == other.name;
@override @override
int get hashCode => magnitude.hashCode; int get hashCode => Object.hash(magnitude, name);
} }
class Formula { class Formula {
final String name; final String name;
final Map<String, VariableSpec> input; // Supports multiple input variables final List<VariableSpec> input;
final Map<String, VariableSpec> output; // Supports multiple output variables final VariableSpec output;
final String d4rtCode; final String d4rtCode;
Formula({ Formula({
@ -59,116 +43,27 @@ class Formula {
if (name.trim().isEmpty) { if (name.trim().isEmpty) {
throw ArgumentError('Formula name cannot be empty'); 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'];
if (name is! String || name.trim().isEmpty) {
throw FormatException('Formula requires a non-empty name string');
}
Map<String, VariableSpec> parseVars(dynamic obj, String fieldName) {
if (obj == null) return <String, VariableSpec>{};
if (obj is! Map<String, dynamic>) {
throw FormatException('$fieldName must be an object map');
}
return obj.map((k, v) {
if (v is! Map<String, dynamic>) {
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<String, dynamic>) {
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<String, dynamic> 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 @override
String toString() => String toString() =>
'Formula(name: $name, input: $input, output: $output, d4rtCode: $d4rtCode)'; 'Formula(name: $name, input: $input, output: $output, d4rtCode: $d4rtCode)';
Formula copyWith({
String? name,
Map<String, VariableSpec>? input,
Map<String, VariableSpec>? output,
String? d4rtCode,
}) => Formula(
name: name ?? this.name,
input: input ?? this.input,
output: output ?? this.output,
d4rtCode: d4rtCode ?? this.d4rtCode,
);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is Formula && other is Formula &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
name == other.name && name == other.name &&
_mapEquals(input, other.input) && output == other.output &&
_mapEquals(output, other.output) && ListEquality().equals(input, other.input) &&
d4rtCode == other.d4rtCode; d4rtCode == other.d4rtCode;
@override @override
int get hashCode => int get hashCode =>
Object.hash(name, _mapHash(input), _mapHash(output), d4rtCode); Object.hash(name, ListEquality().hash(input), output, d4rtCode);
List<String> inputVarNames() => input.map( (v) => v.name ).toList(growable: false);
} }
bool _mapEquals<K, V>(Map<K, V> a, Map<K, V> 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<K, V>(Map<K, V> m) {
var h = 0;
for (final e in m.entries) {
h = h ^ Object.hash(e.key, e.value);
}
return h;
}

View file

@ -14,11 +14,11 @@ void main() {
test('evaluates Newton\'s second law formula', () { test('evaluates Newton\'s second law formula', () {
final formula = Formula( final formula = Formula(
name: "Newton's second law", name: "Newton's second law",
input: { input: [
'm': VariableSpec(magnitude: 'mass'), VariableSpec(name: 'm', magnitude: 'mass'),
'a': VariableSpec(magnitude: 'acceleration'), VariableSpec(name: 'a', magnitude: 'acceleration'),
}, ],
output: {'F': VariableSpec(magnitude: 'force')}, output: VariableSpec(name: 'F', magnitude: 'force'),
d4rtCode: ''' d4rtCode: '''
main() { main() {
return a * m; return a * m;
@ -37,11 +37,11 @@ void main() {
test('evaluates simple arithmetic formula', () { test('evaluates simple arithmetic formula', () {
final formula = Formula( final formula = Formula(
name: 'Simple addition', name: 'Simple addition',
input: { input: [
'x': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'x', magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'y', magnitude: 'scalar'),
}, ],
output: {'result': VariableSpec(magnitude: 'scalar')}, output: VariableSpec(name: 'result', magnitude: 'scalar'),
d4rtCode: ''' d4rtCode: '''
main() { main() {
return x + y; return x + y;
@ -57,8 +57,8 @@ void main() {
test('handles single input variable', () { test('handles single input variable', () {
final formula = Formula( final formula = Formula(
name: 'Square function', name: 'Square function',
input: {'n': VariableSpec(magnitude: 'scalar')}, input: [VariableSpec(name: 'n', magnitude: 'scalar')],
output: {'result': VariableSpec(magnitude: 'scalar')}, output: VariableSpec(name: 'result', magnitude: 'scalar'),
d4rtCode: ''' d4rtCode: '''
main() { main() {
return n * n; return n * n;
@ -73,12 +73,12 @@ void main() {
test('handles complex mathematical operations', () { test('handles complex mathematical operations', () {
final formula = Formula( final formula = Formula(
name: 'Quadratic formula discriminant', name: 'Quadratic formula discriminant',
input: { input: [
'a': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'a', magnitude: 'scalar'),
'b': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'b', magnitude: 'scalar'),
'c': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'c', magnitude: 'scalar'),
}, ],
output: {'discriminant': VariableSpec(magnitude: 'scalar')}, output: VariableSpec(name: 'discriminant', magnitude: 'scalar'),
d4rtCode: ''' d4rtCode: '''
main() { main() {
return b * b - 4 * a * c; return b * b - 4 * a * c;
@ -96,12 +96,12 @@ void main() {
test('maintains consistent alphabetical order for input variables', () { test('maintains consistent alphabetical order for input variables', () {
final formula = Formula( final formula = Formula(
name: 'Test order', name: 'Test order',
input: { input: [
'z': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'z', magnitude: 'scalar'),
'a': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'a', magnitude: 'scalar'),
'b': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'b', magnitude: 'scalar'),
}, ],
output: {'result': VariableSpec(magnitude: 'scalar')}, output: VariableSpec(name: 'result', magnitude: 'scalar'),
d4rtCode: 'main() { return a + b + z; }', d4rtCode: 'main() { return a + b + z; }',
); );
@ -112,12 +112,12 @@ void main() {
test('passes arguments in correct alphabetical order', () { test('passes arguments in correct alphabetical order', () {
final formula = Formula( final formula = Formula(
name: 'Test argument order', name: 'Test argument order',
input: { input: [
'z': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'z', magnitude: 'scalar'),
'a': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'a', magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'y', magnitude: 'scalar'),
}, ],
output: {'result': VariableSpec(magnitude: 'scalar')}, output: VariableSpec(name: 'result', magnitude: 'scalar'),
d4rtCode: ''' d4rtCode: '''
main() { main() {
// Variables: a=1, y=2, z=3 // Variables: a=1, y=2, z=3
@ -133,46 +133,15 @@ void main() {
}); });
group('Error handling', () { 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<ArgumentError>()));
},
);
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<ArgumentError>()),
);
},
);
test('throws exception for missing input variables', () { test('throws exception for missing input variables', () {
final formula = Formula( final formula = Formula(
name: 'Test formula', name: 'Test formula',
input: { input: [
'x': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'x', magnitude: 'scalar'),
'y': VariableSpec(magnitude: 'scalar'), VariableSpec(name: 'y', magnitude: 'scalar'),
}, ],
output: {'result': VariableSpec(magnitude: 'scalar')}, output: VariableSpec(name: 'result', magnitude: 'scalar'),
d4rtCode: 'main() { return x + y; }', d4rtCode: 'main() { return x + y; }',
); );
@ -185,8 +154,8 @@ void main() {
test('throws exception for invalid d4rt code', () { test('throws exception for invalid d4rt code', () {
final formula = Formula( final formula = Formula(
name: 'Invalid code formula', name: 'Invalid code formula',
input: {'x': VariableSpec(magnitude: 'scalar')}, input: [VariableSpec(name: 'x', magnitude: 'scalar')],
output: {'result': VariableSpec(magnitude: 'scalar')}, output: VariableSpec(name: 'result', magnitude: 'scalar'),
d4rtCode: 'invalid dart code here!', d4rtCode: 'invalid dart code here!',
); );
@ -201,8 +170,8 @@ void main() {
test('getOutputVariableName returns the single output variable name', () { test('getOutputVariableName returns the single output variable name', () {
final formula = Formula( final formula = Formula(
name: 'Test', name: 'Test',
input: {'x': VariableSpec(magnitude: 'scalar')}, input: [VariableSpec(name: 'x', magnitude: 'scalar')],
output: {'force': VariableSpec(magnitude: 'Newton')}, output: VariableSpec(name: 'force', magnitude: 'Newton'),
d4rtCode: 'main() { return x; }', d4rtCode: 'main() { return x; }',
); );
@ -214,8 +183,8 @@ void main() {
() { () {
final formula = Formula( final formula = Formula(
name: 'Test', name: 'Test',
input: {'x': VariableSpec(magnitude: 'scalar')}, input: [VariableSpec(name: 'x', magnitude: 'scalar')],
output: {'force': VariableSpec(magnitude: 'Newton')}, output: VariableSpec(name: 'force', magnitude: 'Newton'),
d4rtCode: 'main() { return x; }', d4rtCode: 'main() { return x; }',
); );
@ -226,8 +195,8 @@ void main() {
test('utility methods work correctly with valid formulas', () { test('utility methods work correctly with valid formulas', () {
final validFormula = Formula( final validFormula = Formula(
name: 'Valid Formula', name: 'Valid Formula',
input: {'x': VariableSpec(magnitude: 'scalar')}, input: [VariableSpec(name: 'x', magnitude: 'scalar')],
output: {'result': VariableSpec(magnitude: 'Newton')}, output: VariableSpec(name: 'result', magnitude: 'Newton'),
d4rtCode: 'main() { return x; }', d4rtCode: 'main() { return x; }',
); );
@ -235,39 +204,14 @@ void main() {
expect(evaluator.getOutputVariableMagnitude(validFormula), 'Newton'); 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<ArgumentError>()),
);
expect(
() => Formula(
name: 'Invalid',
input: {'x': VariableSpec(magnitude: 'scalar')},
output: {
'y': VariableSpec(magnitude: 'scalar'),
'z': VariableSpec(magnitude: 'scalar'),
},
d4rtCode: 'main() { return x; }',
),
throwsA(isA<ArgumentError>()),
);
});
}); });
group('Data types', () { group('Data types', () {
test('handles integer values', () { test('handles integer values', () {
final formula = Formula( final formula = Formula(
name: 'Integer test', name: 'Integer test',
input: {'n': VariableSpec(magnitude: 'count')}, input: [VariableSpec(name: 'n', magnitude: 'count')],
output: {'result': VariableSpec(magnitude: 'count')}, output: VariableSpec(name: 'result', magnitude: 'count'),
d4rtCode: 'main() { return n + 1; }', d4rtCode: 'main() { return n + 1; }',
); );
@ -278,8 +222,8 @@ void main() {
test('handles double values', () { test('handles double values', () {
final formula = Formula( final formula = Formula(
name: 'Double test', name: 'Double test',
input: {'x': VariableSpec(magnitude: 'length')}, input: [VariableSpec(name: 'x', magnitude: 'length')],
output: {'result': VariableSpec(magnitude: 'area')}, output: VariableSpec(name: 'result', magnitude: 'area'),
d4rtCode: 'main() { return x * x; }', d4rtCode: 'main() { return x * x; }',
); );

View file

@ -4,71 +4,4 @@ import 'package:test/test.dart';
import 'package:d4rt_formulas/formula_models.dart'; import 'package:d4rt_formulas/formula_models.dart';
void main() { 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<String, dynamic>;
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<String, dynamic>;
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<String, dynamic>;
final f = Formula.fromJson(map);
expect(f.d4rtCode, 'return x;');
});
});
} }