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

View file

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

View file

@ -1,42 +1,48 @@
import 'dart:async';
import 'dart:convert' show utf8;
import 'package:flutter/services.dart' show rootBundle;
import 'package:resource_portable/resource_portable.dart' show Resource;
import '../corpus.dart';
import '../formula_models.dart';
Future<Corpus> createDefaultCorpus() async{
final corpus = Corpus();
Future<String> loadResourceAsString(String path) async {
return await rootBundle.loadString(path, cache: false);
}
Future<void> loadUnits() async {
final unitResources = [
"lib/defaults/units/angle.d4rt.units",
"lib/defaults/units/area.d4rt.units",
"lib/defaults/units/distance.d4rt.units",
"lib/defaults/units/energy.d4rt.units",
"lib/defaults/units/force.d4rt.units",
"lib/defaults/units/mass.d4rt.units",
"lib/defaults/units/pressure.d4rt.units",
"lib/defaults/units/scalar.d4rt.units",
"lib/defaults/units/temperature.d4rt.units",
"lib/defaults/units/time.d4rt.units",
"lib/defaults/units/velocity.d4rt.units",
"assets/units/angle.d4rt.units",
"assets/units/area.d4rt.units",
"assets/units/distance.d4rt.units",
"assets/units/energy.d4rt.units",
"assets/units/force.d4rt.units",
"assets/units/mass.d4rt.units",
"assets/units/pressure.d4rt.units",
"assets/units/scalar.d4rt.units",
"assets/units/temperature.d4rt.units",
"assets/units/time.d4rt.units",
"assets/units/velocity.d4rt.units",
];
for (final unitRes in unitResources) {
final resource = Resource(unitRes);
final literal = await resource.readAsString(encoding: utf8);
final literal = await loadResourceAsString(unitRes);
final units = UnitSpec.fromArrayStringLiteral(literal);
corpus.loadUnits(units);
}
}
Future<void> loadFormulas() async {
final formulaResources = ["lib/defaults/formulas.d4rt"];
final formulaResources = ["assets/formulas/formulas.d4rt"];
for (final formRes in formulaResources) {
final resource = Resource(formRes);
final literal = await resource.readAsString(encoding: utf8);
final literal = await loadResourceAsString(formRes);
final formulas = Formula.fromArrayStringLiteral(literal);
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();
prepareInterpreter(d4rtInterpreter);
final d4rtCode = """
${d4rtImports}
$d4rtImports
main()
{
late var result;
result = $code;
return result;
}""";
//print("evaluateExpression:\n$d4rtCode");
print("evaluateExpression:\n$d4rtCode");
final result = d4rtInterpreter.execute(source: d4rtCode);
switch ( result ){
case int value:

View file

@ -102,17 +102,17 @@ class UnitSpec {
class VariableSpec {
final String name;
final String? unit;
final List<dynamic>? allowedValues;
final List<dynamic>? values;
VariableSpec({required this.name, this.unit, this.allowedValues}){
final valuesValid = allowedValues != null && allowedValues?.isNotEmpty == true;
VariableSpec({required this.name, this.unit, this.values}){
final valuesValid = values != null && values?.isNotEmpty == true;
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
String toString() => 'var($name: $unit${allowedValues != null ? ' allowed: $allowedValues' : ''})';
String toString() => 'var($name: $unit${values != null ? ' allowed: $values' : ''})';
@override
bool operator ==(Object other) =>
@ -121,10 +121,10 @@ class VariableSpec {
runtimeType == other.runtimeType &&
unit == other.unit &&
name == other.name &&
const DeepCollectionEquality().equals(allowedValues, other.allowedValues);
const DeepCollectionEquality().equals(values, other.values);
@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 {
@ -146,7 +146,7 @@ class Formula {
validate();
}
validate() {
void validate() {
if (name.trim().isEmpty) {
throw ArgumentError('Formula name cannot be empty');
}
@ -220,7 +220,7 @@ class Formula {
return VariableSpec(
name: name,
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';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MaterialApp(
home: FutureBuilder<Corpus>(
future: createDefaultCorpus(),

View file

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

View file

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

View file

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

View file

@ -208,5 +208,112 @@ void main() {
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');
});
});
});
}