diff --git a/lib/defaults/formulas.d4rt b/assets/formulas/formulas.d4rt similarity index 100% rename from lib/defaults/formulas.d4rt rename to assets/formulas/formulas.d4rt diff --git a/lib/defaults/units/angle.d4rt.units b/assets/units/angle.d4rt.units similarity index 100% rename from lib/defaults/units/angle.d4rt.units rename to assets/units/angle.d4rt.units diff --git a/lib/defaults/units/area.d4rt.units b/assets/units/area.d4rt.units similarity index 100% rename from lib/defaults/units/area.d4rt.units rename to assets/units/area.d4rt.units diff --git a/lib/defaults/units/distance.d4rt.units b/assets/units/distance.d4rt.units similarity index 100% rename from lib/defaults/units/distance.d4rt.units rename to assets/units/distance.d4rt.units diff --git a/lib/defaults/units/energy.d4rt.units b/assets/units/energy.d4rt.units similarity index 100% rename from lib/defaults/units/energy.d4rt.units rename to assets/units/energy.d4rt.units diff --git a/lib/defaults/units/force.d4rt.units b/assets/units/force.d4rt.units similarity index 100% rename from lib/defaults/units/force.d4rt.units rename to assets/units/force.d4rt.units diff --git a/lib/defaults/units/mass.d4rt.units b/assets/units/mass.d4rt.units similarity index 100% rename from lib/defaults/units/mass.d4rt.units rename to assets/units/mass.d4rt.units diff --git a/lib/defaults/units/pressure.d4rt.units b/assets/units/pressure.d4rt.units similarity index 100% rename from lib/defaults/units/pressure.d4rt.units rename to assets/units/pressure.d4rt.units diff --git a/assets/units/scalar.d4rt.units b/assets/units/scalar.d4rt.units new file mode 100644 index 0000000..0533d87 --- /dev/null +++ b/assets/units/scalar.d4rt.units @@ -0,0 +1,3 @@ +[ + {"name": "scalar", "symbol": "", "isBase": true}, +] \ No newline at end of file diff --git a/lib/defaults/units/temperature.d4rt.units b/assets/units/temperature.d4rt.units similarity index 100% rename from lib/defaults/units/temperature.d4rt.units rename to assets/units/temperature.d4rt.units diff --git a/lib/defaults/units/time.d4rt.units b/assets/units/time.d4rt.units similarity index 100% rename from lib/defaults/units/time.d4rt.units rename to assets/units/time.d4rt.units diff --git a/lib/defaults/units/velocity.d4rt.units b/assets/units/velocity.d4rt.units similarity index 100% rename from lib/defaults/units/velocity.d4rt.units rename to assets/units/velocity.d4rt.units diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 7b12262..4039f5a 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -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 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(); @@ -97,27 +90,37 @@ class _FormulaScreenState extends State { final inputValues = {}; for (final input in widget.formula.input) { final controller = _inputControllers[input.name]!; - if( controller.d4rtValue == null ){ - throw FormulaEvaluationException( "Field ${input.name} is invalid" ); + if (controller.d4rtValue == null) { + //throw FormulaEvaluationException("Field ${input.name} is invalid"); + _result = ""; + return; } - final value = controller.d4rtValue.value; - // 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 ) { - convertedValue = widget.corpus.convert( - value, - _selectedUnits[input.name]!, - input.unit as String, - ); - } - else{ - convertedValue = 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 + if (input.unit != null) { + convertedValue = widget.corpus.convert( + nr.value, + _selectedUnits[input.name]!, + input.unit as String, + ); + } else { + convertedValue = nr.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(); @@ -125,14 +128,11 @@ class _FormulaScreenState extends State { // 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{ + if (unit != null) { + _result = widget.corpus + .convert(result, unit, _selectedOutputUnit!) + .toStringAsFixed(2); + } else { _result = result; } @@ -142,7 +142,7 @@ class _FormulaScreenState extends State { } catch (e, stack) { debugPrint('Formula evaluation error: $e'); debugPrint('Stack trace: $stack'); - + ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Error: ${e.toString()}\n${stack.toString()}'), @@ -155,9 +155,7 @@ class _FormulaScreenState extends State { @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( @@ -176,7 +174,7 @@ class _FormulaScreenState extends State { } Widget _buildDescriptionSection() { - if (widget.formula.description == null || + if (widget.formula.description == null || widget.formula.description!.isEmpty) { return const SizedBox.shrink(); } @@ -186,9 +184,9 @@ class _FormulaScreenState extends State { 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 { 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 { 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 { decoration: const InputDecoration( border: UnderlineInputBorder(), ), + autovalidateMode: AutovalidateMode.always, validator: (value) { if (value == null || value.isEmpty) { return 'Required'; diff --git a/lib/corpus.dart b/lib/corpus.dart index 0710d8b..f6b37e9 100644 --- a/lib/corpus.dart +++ b/lib/corpus.dart @@ -73,7 +73,7 @@ class Corpus{ List unitsOfSameMagnitude(String? unit){ if( unit == null ){ - return ["unitless"]; + return ["scalar"]; } final base = getUnit(unit).baseUnit; return _baseToUnits[base] as List; @@ -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; diff --git a/lib/defaults/default_corpus.dart b/lib/defaults/default_corpus.dart index d740ba8..af7f983 100644 --- a/lib/defaults/default_corpus.dart +++ b/lib/defaults/default_corpus.dart @@ -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 createDefaultCorpus() async{ final corpus = Corpus(); + Future loadResourceAsString(String path) async { + return await rootBundle.loadString(path, cache: false); + } + + Future 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 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); } diff --git a/lib/defaults/units/scalar.d4rt.units b/lib/defaults/units/scalar.d4rt.units deleted file mode 100644 index c314f66..0000000 --- a/lib/defaults/units/scalar.d4rt.units +++ /dev/null @@ -1,3 +0,0 @@ -[ - {"name": "scalar", "symbol": "", "isBase": true} -] \ No newline at end of file diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart index 9a844b9..1f6d961 100644 --- a/lib/formula_evaluator.dart +++ b/lib/formula_evaluator.dart @@ -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: diff --git a/lib/formula_models.dart b/lib/formula_models.dart index 05bb6a8..9a0c5f6 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -102,17 +102,17 @@ class UnitSpec { class VariableSpec { final String name; final String? unit; - final List? allowedValues; + final List? 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), ); } diff --git a/lib/main.dart b/lib/main.dart index f21f76c..0f44288 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import 'corpus.dart'; import 'defaults/default_corpus.dart'; void main() { + WidgetsFlutterBinding.ensureInitialized(); runApp(MaterialApp( home: FutureBuilder( future: createDefaultCorpus(), diff --git a/pubspec.yaml b/pubspec.yaml index 7a0cea1..c143b38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/d4rt_test.dart b/test/d4rt_test.dart index db0aeb4..0e948a8 100644 --- a/test/d4rt_test.dart +++ b/test/d4rt_test.dart @@ -3,7 +3,7 @@ import 'package:d4rt/d4rt.dart'; import 'dart:math' as Math; -main(){ +void main(){ test('Access to Math', () { final completeSource = """ diff --git a/test/dart_test.dart b/test/dart_test.dart index 9777d46..9e78acc 100644 --- a/test/dart_test.dart +++ b/test/dart_test.dart @@ -3,7 +3,7 @@ import 'package:d4rt/d4rt.dart'; import 'dart:math' as Math; -main(){ +void main(){ test('for dart grammar tests', () { }); diff --git a/test/formula_evaluator_test.dart b/test/formula_evaluator_test.dart index 0b4c04d..7c553ce 100644 --- a/test/formula_evaluator_test.dart +++ b/test/formula_evaluator_test.dart @@ -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'); + }); + }); }); }