161 lines
4.5 KiB
Dart
161 lines
4.5 KiB
Dart
|
|
/// 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: "..." }.
|
||
|
|
|
||
|
|
class VariableSpec {
|
||
|
|
final String magnitude;
|
||
|
|
|
||
|
|
const VariableSpec({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
|
||
|
|
String toString() => 'VariableSpec(magnitude: $magnitude)';
|
||
|
|
|
||
|
|
VariableSpec copyWith({String? magnitude}) =>
|
||
|
|
VariableSpec(magnitude: magnitude ?? this.magnitude);
|
||
|
|
|
||
|
|
@override
|
||
|
|
bool operator ==(Object other) =>
|
||
|
|
identical(this, other) ||
|
||
|
|
other is VariableSpec &&
|
||
|
|
runtimeType == other.runtimeType &&
|
||
|
|
magnitude == other.magnitude;
|
||
|
|
|
||
|
|
@override
|
||
|
|
int get hashCode => magnitude.hashCode;
|
||
|
|
}
|
||
|
|
|
||
|
|
class Formula {
|
||
|
|
final String name;
|
||
|
|
final Map<String, VariableSpec> input; // Supports multiple input variables
|
||
|
|
final Map<String, VariableSpec> output; // Supports multiple output variables
|
||
|
|
final String d4rtCode;
|
||
|
|
|
||
|
|
const Formula({
|
||
|
|
required this.name,
|
||
|
|
required this.input,
|
||
|
|
required this.output,
|
||
|
|
required this.d4rtCode,
|
||
|
|
});
|
||
|
|
|
||
|
|
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
|
||
|
|
String toString() =>
|
||
|
|
'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
|
||
|
|
bool operator ==(Object other) =>
|
||
|
|
identical(this, other) ||
|
||
|
|
other is Formula &&
|
||
|
|
runtimeType == other.runtimeType &&
|
||
|
|
name == other.name &&
|
||
|
|
_mapEquals(input, other.input) &&
|
||
|
|
_mapEquals(output, other.output) &&
|
||
|
|
d4rtCode == other.d4rtCode;
|
||
|
|
|
||
|
|
@override
|
||
|
|
int get hashCode =>
|
||
|
|
Object.hash(name, _mapHash(input), _mapHash(output), d4rtCode);
|
||
|
|
}
|
||
|
|
|
||
|
|
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;
|
||
|
|
}
|