can't make valiation on each user interaction

This commit is contained in:
Álvaro González 2026-01-21 08:49:56 +01:00
parent 49962b95d6
commit 76769973f3
23 changed files with 198 additions and 82 deletions

View file

@ -0,0 +1,3 @@
[
{"name": "scalar", "symbol": "", "isBase": true},
]

View file

@ -1,6 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:flutter_markdown/flutter_markdown.dart';
import '../formula_models.dart'; import '../formula_models.dart';
import '../formula_evaluator.dart'; import '../formula_evaluator.dart';
@ -11,11 +9,7 @@ class FormulaScreen extends StatefulWidget {
final Formula formula; final Formula formula;
final Corpus corpus; final Corpus corpus;
const FormulaScreen({ const FormulaScreen({super.key, required this.formula, required this.corpus});
super.key,
required this.formula,
required this.corpus,
});
@override @override
State<FormulaScreen> createState() => _FormulaScreenState(); State<FormulaScreen> createState() => _FormulaScreenState();
@ -24,7 +18,7 @@ class FormulaScreen extends StatefulWidget {
//// Start of D4rtEditingController class //// //// Start of D4rtEditingController class ////
class D4rtEditingController extends TextEditingController { class D4rtEditingController extends TextEditingController {
String? _lastError; String? _lastError;
String _text = "";
String? get lastError => _lastError; String? get lastError => _lastError;
FormulaResult? _lastValue; FormulaResult? _lastValue;
@ -46,7 +40,6 @@ class D4rtEditingController extends TextEditingController {
get d4rtValue => _lastValue; get d4rtValue => _lastValue;
@override
set text(String newText) { set text(String newText) {
super.text = newText; super.text = newText;
validate(); validate();
@ -97,27 +90,37 @@ class _FormulaScreenState extends State<FormulaScreen> {
final inputValues = <String, dynamic>{}; final inputValues = <String, dynamic>{};
for (final input in widget.formula.input) { for (final input in widget.formula.input) {
final controller = _inputControllers[input.name]!; final controller = _inputControllers[input.name]!;
if( controller.d4rtValue == null ){ if (controller.d4rtValue == null) {
throw FormulaEvaluationException( "Field ${input.name} is invalid" ); //throw FormulaEvaluationException("Field ${input.name} is invalid");
_result = "";
return;
} }
final value = controller.d4rtValue.value;
late final dynamic convertedValue;
switch (controller.d4rtValue) {
case NumberResult nr:
// Convert input to base unit if needed // Convert input to base unit if needed
// Always convert from dropdown unit to variable's base unit // Always convert from dropdown unit to variable's base unit
late final convertedValue; if (input.unit != null) {
if( value is Number && input.unit != null ) {
convertedValue = widget.corpus.convert( convertedValue = widget.corpus.convert(
value, nr.value,
_selectedUnits[input.name]!, _selectedUnits[input.name]!,
input.unit as String, input.unit as String,
); );
} else {
convertedValue = nr.value;
} }
else{
convertedValue = value; case StringResult sr:
convertedValue = sr.value;
default:
throw FormulaEvaluationException(
"Field ${input.name} has unsupported type ${controller.d4rtValue!.runtimeType}",
);
} }
inputValues[input.name] = convertedValue; inputValues[input.name] = convertedValue;
} }
final evaluator = FormulaEvaluator(); final evaluator = FormulaEvaluator();
@ -125,14 +128,11 @@ class _FormulaScreenState extends State<FormulaScreen> {
// Convert output to selected unit if needed // Convert output to selected unit if needed
String? unit = widget.formula.output.unit; String? unit = widget.formula.output.unit;
if( unit != null ) { if (unit != null) {
_result = widget.corpus.convert( _result = widget.corpus
result, .convert(result, unit, _selectedOutputUnit!)
unit, .toStringAsFixed(2);
_selectedOutputUnit!, } else {
).toStringAsFixed(2);
}
else{
_result = result; _result = result;
} }
@ -155,9 +155,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(title: Text(widget.formula.name)),
title: Text(widget.formula.name),
),
body: Form( body: Form(
key: _formKey, key: _formKey,
child: Padding( child: Padding(
@ -186,9 +184,9 @@ class _FormulaScreenState extends State<FormulaScreen> {
children: [ children: [
Text( Text(
'Description', 'Description',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(
fontWeight: FontWeight.bold, context,
), ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Container( Container(
@ -213,9 +211,9 @@ class _FormulaScreenState extends State<FormulaScreen> {
children: [ children: [
Text( Text(
'Input Variables', 'Input Variables',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(
fontWeight: FontWeight.bold, context,
), ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
...widget.formula.input.map((variable) => _buildVariableRow(variable)), ...widget.formula.input.map((variable) => _buildVariableRow(variable)),
@ -229,9 +227,9 @@ class _FormulaScreenState extends State<FormulaScreen> {
children: [ children: [
Text( Text(
'Result', 'Result',
style: Theme.of(context).textTheme.titleMedium?.copyWith( style: Theme.of(
fontWeight: FontWeight.bold, context,
), ).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Row( Row(
@ -286,6 +284,7 @@ class _FormulaScreenState extends State<FormulaScreen> {
decoration: const InputDecoration( decoration: const InputDecoration(
border: UnderlineInputBorder(), border: UnderlineInputBorder(),
), ),
autovalidateMode: AutovalidateMode.always,
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Required'; return 'Required';

View file

@ -73,7 +73,7 @@ class Corpus{
List<String> unitsOfSameMagnitude(String? unit){ List<String> unitsOfSameMagnitude(String? unit){
if( unit == null ){ if( unit == null ){
return ["unitless"]; return ["scalar"];
} }
final base = getUnit(unit).baseUnit; final base = getUnit(unit).baseUnit;
return _baseToUnits[base] as List<String>; return _baseToUnits[base] as List<String>;
@ -90,7 +90,7 @@ class Corpus{
String _converterFromCodeStringAsExpression(Number x, String codeString) { String _converterFromCodeStringAsExpression(Number x, String codeString) {
final buffer = StringBuffer(); final buffer = StringBuffer();
buffer.writeln("final x = ${x};"); buffer.writeln("final x = $x;");
buffer.writeln("main(){return $codeString;}"); buffer.writeln("main(){return $codeString;}");
final code = buffer.toString(); final code = buffer.toString();
return code; return code;
@ -98,7 +98,7 @@ class Corpus{
String _converterFromCodeStringAsStatement(Number x, String codeString) { String _converterFromCodeStringAsStatement(Number x, String codeString) {
final buffer = StringBuffer(); final buffer = StringBuffer();
buffer.writeln("final x = ${x};"); buffer.writeln("final x = $x;");
buffer.writeln("main(){ $codeString; return x; }"); buffer.writeln("main(){ $codeString; return x; }");
final code = buffer.toString(); final code = buffer.toString();
return code; return code;

View file

@ -1,42 +1,48 @@
import 'dart:async';
import 'dart:convert' show utf8; import 'dart:convert' show utf8;
import 'package:flutter/services.dart' show rootBundle;
import 'package:resource_portable/resource_portable.dart' show Resource; import 'package:resource_portable/resource_portable.dart' show Resource;
import '../corpus.dart'; import '../corpus.dart';
import '../formula_models.dart'; import '../formula_models.dart';
Future<Corpus> createDefaultCorpus() async{ Future<Corpus> createDefaultCorpus() async{
final corpus = Corpus(); final corpus = Corpus();
Future<String> loadResourceAsString(String path) async {
return await rootBundle.loadString(path, cache: false);
}
Future<void> loadUnits() async { Future<void> loadUnits() async {
final unitResources = [ final unitResources = [
"lib/defaults/units/angle.d4rt.units", "assets/units/angle.d4rt.units",
"lib/defaults/units/area.d4rt.units", "assets/units/area.d4rt.units",
"lib/defaults/units/distance.d4rt.units", "assets/units/distance.d4rt.units",
"lib/defaults/units/energy.d4rt.units", "assets/units/energy.d4rt.units",
"lib/defaults/units/force.d4rt.units", "assets/units/force.d4rt.units",
"lib/defaults/units/mass.d4rt.units", "assets/units/mass.d4rt.units",
"lib/defaults/units/pressure.d4rt.units", "assets/units/pressure.d4rt.units",
"lib/defaults/units/scalar.d4rt.units", "assets/units/scalar.d4rt.units",
"lib/defaults/units/temperature.d4rt.units", "assets/units/temperature.d4rt.units",
"lib/defaults/units/time.d4rt.units", "assets/units/time.d4rt.units",
"lib/defaults/units/velocity.d4rt.units", "assets/units/velocity.d4rt.units",
]; ];
for (final unitRes in unitResources) { for (final unitRes in unitResources) {
final resource = Resource(unitRes); final literal = await loadResourceAsString(unitRes);
final literal = await resource.readAsString(encoding: utf8);
final units = UnitSpec.fromArrayStringLiteral(literal); final units = UnitSpec.fromArrayStringLiteral(literal);
corpus.loadUnits(units); corpus.loadUnits(units);
} }
} }
Future<void> loadFormulas() async { Future<void> loadFormulas() async {
final formulaResources = ["lib/defaults/formulas.d4rt"]; final formulaResources = ["assets/formulas/formulas.d4rt"];
for (final formRes in formulaResources) { for (final formRes in formulaResources) {
final resource = Resource(formRes); final literal = await loadResourceAsString(formRes);
final literal = await resource.readAsString(encoding: utf8);
final formulas = Formula.fromArrayStringLiteral(literal); final formulas = Formula.fromArrayStringLiteral(literal);
corpus.loadFormulas(formulas); corpus.loadFormulas(formulas);
} }

View file

@ -1,3 +0,0 @@
[
{"name": "scalar", "symbol": "", "isBase": true}
]

View file

@ -76,14 +76,14 @@ class FormulaEvaluator {
final d4rtInterpreter = interpreter ?? createDefaultInterpreter(); final d4rtInterpreter = interpreter ?? createDefaultInterpreter();
prepareInterpreter(d4rtInterpreter); prepareInterpreter(d4rtInterpreter);
final d4rtCode = """ final d4rtCode = """
${d4rtImports} $d4rtImports
main() main()
{ {
late var result; late var result;
result = $code; result = $code;
return result; return result;
}"""; }""";
//print("evaluateExpression:\n$d4rtCode"); print("evaluateExpression:\n$d4rtCode");
final result = d4rtInterpreter.execute(source: d4rtCode); final result = d4rtInterpreter.execute(source: d4rtCode);
switch ( result ){ switch ( result ){
case int value: case int value:

View file

@ -102,17 +102,17 @@ class UnitSpec {
class VariableSpec { class VariableSpec {
final String name; final String name;
final String? unit; final String? unit;
final List<dynamic>? allowedValues; final List<dynamic>? values;
VariableSpec({required this.name, this.unit, this.allowedValues}){ VariableSpec({required this.name, this.unit, this.values}){
final valuesValid = allowedValues != null && allowedValues?.isNotEmpty == true; final valuesValid = values != null && values?.isNotEmpty == true;
if( unit == null && !valuesValid ){ if( unit == null && !valuesValid ){
throw new ArgumentError("$name: at least unit or allowedValues should be valid"); throw ArgumentError("$name: at least unit or allowedValues should be valid");
} }
} }
@override @override
String toString() => 'var($name: $unit${allowedValues != null ? ' allowed: $allowedValues' : ''})'; String toString() => 'var($name: $unit${values != null ? ' allowed: $values' : ''})';
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
@ -121,10 +121,10 @@ class VariableSpec {
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
unit == other.unit && unit == other.unit &&
name == other.name && name == other.name &&
const DeepCollectionEquality().equals(allowedValues, other.allowedValues); const DeepCollectionEquality().equals(values, other.values);
@override @override
int get hashCode => Object.hash(unit, name, allowedValues != null ? const DeepCollectionEquality().hash(allowedValues!) : 0); int get hashCode => Object.hash(unit, name, values != null ? const DeepCollectionEquality().hash(values!) : 0);
} }
class Formula { class Formula {
@ -146,7 +146,7 @@ class Formula {
validate(); validate();
} }
validate() { void validate() {
if (name.trim().isEmpty) { if (name.trim().isEmpty) {
throw ArgumentError('Formula name cannot be empty'); throw ArgumentError('Formula name cannot be empty');
} }
@ -220,7 +220,7 @@ class Formula {
return VariableSpec( return VariableSpec(
name: name, name: name,
unit: unit, unit: unit,
allowedValues: allowed?.toList(growable: false), values: allowed?.toList(growable: false),
); );
} }

View file

@ -6,6 +6,7 @@ import 'corpus.dart';
import 'defaults/default_corpus.dart'; import 'defaults/default_corpus.dart';
void main() { void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp( runApp(MaterialApp(
home: FutureBuilder<Corpus>( home: FutureBuilder<Corpus>(
future: createDefaultCorpus(), future: createDefaultCorpus(),

View file

@ -68,6 +68,9 @@ flutter:
# assets: # assets:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
assets:
- assets/units/
- assets/formulas/
# An image asset can refer to one or more resolution-specific "variants", see # An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images # https://flutter.dev/to/resolution-aware-images

View file

@ -3,7 +3,7 @@ import 'package:d4rt/d4rt.dart';
import 'dart:math' as Math; import 'dart:math' as Math;
main(){ void main(){
test('Access to Math', () { test('Access to Math', () {
final completeSource = """ final completeSource = """

View file

@ -3,7 +3,7 @@ import 'package:d4rt/d4rt.dart';
import 'dart:math' as Math; import 'dart:math' as Math;
main(){ void main(){
test('for dart grammar tests', () { test('for dart grammar tests', () {
}); });

View file

@ -208,5 +208,112 @@ void main() {
expect(result, closeTo(9.8596, 0.0001)); expect(result, closeTo(9.8596, 0.0001));
}); });
}); });
group('APGAR Score', () {
test('evaluates APGAR score formula - Normal case', () {
final formula = Formula(
name: "Apgar Score",
description: "Newborn health assessment scoring system",
input: [
VariableSpec(name: "HeartRate", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "Breathing", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "MuscleTone", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "Reflexes", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "SkinColor", values: ["hr1", "hr2", "hr3"])
],
output: VariableSpec(name: "Result", unit: "stringscalar"),
d4rtCode: """
var total = HeartRate + Breathing + MuscleTone + Reflexes + SkinColor;
var interpretation = switch (total) {
>= 7 => 'Normal',
4-6 => 'Requires attention',
_ => 'Emergency care needed'
};
Result = 'Score: \$total - \$interpretation';
""",
);
// Test normal case (score 7-10)
final result = evaluator.evaluate(formula, {
'HeartRate': 2,
'Breathing': 2,
'MuscleTone': 2,
'Reflexes': 2,
'SkinColor': 2
});
expect(result, 'Score: 10 - Normal');
});
test('evaluates APGAR score formula - Requires attention case', () {
final formula = Formula(
name: "Apgar Score",
description: "Newborn health assessment scoring system",
input: [
VariableSpec(name: "HeartRate", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "Breathing", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "MuscleTone", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "Reflexes", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "SkinColor", values: ["hr1", "hr2", "hr3"])
],
output: VariableSpec(name: "Result", unit: "stringscalar"),
d4rtCode: """
var total = HeartRate + Breathing + MuscleTone + Reflexes + SkinColor;
var interpretation = switch (total) {
>= 7 => 'Normal',
4-6 => 'Requires attention',
_ => 'Emergency care needed'
};
Result = 'Score: \$total - \$interpretation';
""",
);
// Test requires attention case (score 4-6)
final result = evaluator.evaluate(formula, {
'HeartRate': 1,
'Breathing': 1,
'MuscleTone': 1,
'Reflexes': 1,
'SkinColor': 2
});
expect(result, 'Score: 6 - Requires attention');
});
test('evaluates APGAR score formula - Emergency case', () {
final formula = Formula(
name: "Apgar Score",
description: "Newborn health assessment scoring system",
input: [
VariableSpec(name: "HeartRate", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "Breathing", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "MuscleTone", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "Reflexes", values: ["hr1", "hr2", "hr3"]),
VariableSpec(name: "SkinColor", values: ["hr1", "hr2", "hr3"])
],
output: VariableSpec(name: "Result", unit: "stringscalar"),
d4rtCode: """
var total = HeartRate + Breathing + MuscleTone + Reflexes + SkinColor;
var interpretation = switch (total) {
>= 7 => 'Normal',
4-6 => 'Requires attention',
_ => 'Emergency care needed'
};
Result = 'Score: \$total - \$interpretation';
""",
);
// Test emergency case (score 0-3)
final result = evaluator.evaluate(formula, {
'HeartRate': 0,
'Breathing': 0,
'MuscleTone': 1,
'Reflexes': 0,
'SkinColor': 1
});
expect(result, 'Score: 2 - Emergency care needed');
});
});
}); });
} }