From 23d895737792821e670e0cf314e26ac4c8298601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sun, 1 Mar 2026 10:49:46 +0100 Subject: [PATCH 1/9] refactor shared formula --- lib/ai/formula_list.dart | 29 +++++++++++++---------------- lib/formula_models.dart | 2 +- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/ai/formula_list.dart b/lib/ai/formula_list.dart index a9f6675..fce2c1d 100644 --- a/lib/ai/formula_list.dart +++ b/lib/ai/formula_list.dart @@ -53,16 +53,20 @@ class _FormulaListState extends State { }).toList(); } + String _formulaAndDependenciesToStringLiteral(Formula formula) { + // Get the formula and its dependencies + final dependencies = widget.corpus.withDependencies(formula); + + // Convert each dependency to its string literal representation + final literals = dependencies.map((element) => element.toStringLiteral()).toList(); + + // Create an array string literal containing all the elements + return '[${literals.join(', ')}]'; + } + void _shareFormula(Formula formula) async { try { - // Get the formula and its dependencies - final dependencies = widget.corpus.withDependencies(formula); - - // Convert each dependency to its string literal representation - final literals = dependencies.map((element) => element.toStringLiteral()).toList(); - - // Create an array string literal containing all the elements - final exportString = '[${literals.join(', ')}]'; + final exportString = _formulaAndDependenciesToStringLiteral(formula); // Share the string await share_plus.SharePlus.instance.share( @@ -90,14 +94,7 @@ class _FormulaListState extends State { void _copyFormula(Formula formula) async { try { - // Get the formula and its dependencies - final dependencies = widget.corpus.withDependencies(formula); - - // Convert each dependency to its string literal representation - final literals = dependencies.map((element) => element.toStringLiteral()).toList(); - - // Create an array string literal containing all the elements - final exportString = '[${literals.join(', ')}]'; + final exportString = _formulaAndDependenciesToStringLiteral(formula); // Copy to clipboard await Clipboard.setData(ClipboardData(text: exportString)); diff --git a/lib/formula_models.dart b/lib/formula_models.dart index 12ceaba..7c79574 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -358,7 +358,7 @@ class Formula implements FormulaElement { buffer.write(', "input": [${inputStrings.join(", ")}]'); buffer.write(', "output": ${output.toStringLiteral()}'); - + buffer.write(', "d4rtCode": r"""$d4rtCode"""'); if (tags.isNotEmpty) { From aadfc9dac5db7b1c7f9fc8c7d6eb28a549882ca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sun, 1 Mar 2026 11:03:13 +0100 Subject: [PATCH 2/9] refactor shared formula --- lib/ai/formula_screen.dart | 4 ++-- test/dart_test.dart | 1 - test/database_test.dart | 1 - test/physics_trigonometry_formulas_test.dart | 1 - 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/ai/formula_screen.dart b/lib/ai/formula_screen.dart index dc1089f..6535ae5 100644 --- a/lib/ai/formula_screen.dart +++ b/lib/ai/formula_screen.dart @@ -63,10 +63,10 @@ class D4rtEditingController extends TextEditingController { if( _validateAsD4rtExpression(text) && _lastValue is StringResult ){ return true; } - if( _validateAsD4rtExpression('"' + text + '"') && _lastValue is StringResult ){ + if( _validateAsD4rtExpression('"$text"') && _lastValue is StringResult ){ return true; } - if( _validateAsD4rtExpression("'" + text + "'") && _lastValue is StringResult ){ + if( _validateAsD4rtExpression("'$text'") && _lastValue is StringResult ){ return true; } return false; diff --git a/test/dart_test.dart b/test/dart_test.dart index 9e78acc..ccedad0 100644 --- a/test/dart_test.dart +++ b/test/dart_test.dart @@ -1,6 +1,5 @@ import 'package:test/test.dart'; import 'package:d4rt/d4rt.dart'; -import 'dart:math' as Math; void main(){ diff --git a/test/database_test.dart b/test/database_test.dart index e3541e7..c38e774 100644 --- a/test/database_test.dart +++ b/test/database_test.dart @@ -1,5 +1,4 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:d4rt_formulas/database/database_service.dart'; import 'package:d4rt_formulas/service_locator.dart'; void main() { diff --git a/test/physics_trigonometry_formulas_test.dart b/test/physics_trigonometry_formulas_test.dart index 2b606f8..d3e3171 100644 --- a/test/physics_trigonometry_formulas_test.dart +++ b/test/physics_trigonometry_formulas_test.dart @@ -2,7 +2,6 @@ import 'package:d4rt_formulas/corpus.dart'; import 'package:d4rt_formulas/defaults/default_corpus.dart'; import 'package:d4rt_formulas/formula_evaluator.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:d4rt_formulas/formula_models.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); From bb468ff60131c27eae8728080e575c4fbcba3273 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Sun, 1 Mar 2026 13:51:14 +0100 Subject: [PATCH 3/9] setutils: static util methods --- bin/pruebas_d4rt.dart | 17 --- lib/database/database_service.dart | 3 +- lib/formula_models.dart | 167 +++++++++++++---------------- lib/main.dart | 1 - test/dart_test.dart | 2 +- test/formula_models_test.dart | 2 +- 6 files changed, 80 insertions(+), 112 deletions(-) delete mode 100644 bin/pruebas_d4rt.dart diff --git a/bin/pruebas_d4rt.dart b/bin/pruebas_d4rt.dart deleted file mode 100644 index 90f6e8a..0000000 --- a/bin/pruebas_d4rt.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:d4rt/d4rt.dart'; - -void main() { - final code = ''' - int fib(int n) { - if (n <= 1) return n; - return fib(n - 1) + fib(n - 2); - } - main() { - return fib(6); - } - '''; - - final interpreter = D4rt(); - final result = interpreter.execute(source: code); - print('Result: $result'); // Result: 8 -} diff --git a/lib/database/database_service.dart b/lib/database/database_service.dart index 06dc034..54033d2 100644 --- a/lib/database/database_service.dart +++ b/lib/database/database_service.dart @@ -1,3 +1,4 @@ +import '../formula_models.dart'; import 'corpus_database_interface.dart'; import 'formulas_database.dart'; import 'package:d4rt_formulas/formula_models.dart' as models; @@ -11,7 +12,7 @@ extension CorpusDatabaseExtension on FormulasDatabase { for (final element in elements) { try { - final parsed = models.parseCorpusElements('[${element.elementText}]'); + final parsed = SetUtils.parseCorpusElements('[${element.elementText}]'); print("PARSED:$element"); parsedElements.addAll(parsed); } catch (e) { diff --git a/lib/formula_models.dart b/lib/formula_models.dart index 7c79574..faeab2e 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -2,6 +2,8 @@ import 'package:d4rt/d4rt.dart'; import 'package:collection/collection.dart'; import 'package:d4rt_formulas/d4rt_formulas.dart'; +typedef Number = double; + abstract class SetUtils { static Object safeGet(Map map, String key) { if (!map.containsKey(key)) { @@ -21,60 +23,55 @@ abstract class SetUtils { static Number numberValue(Map map, String key) { return double.parse(stringValue(map, key)); } -} -/// Parses a d4rt array literal (containing maps and arrays) to a List -/// using d4rt -List parseD4rtLiteral(String arrayStringLiteral) { - var d4rt = D4rt(); - final buffer = StringBuffer(); - buffer.write("main(){ return $arrayStringLiteral; }"); - final code = buffer.toString(); + /// Parses a d4rt array literal (containing maps and arrays) to a List + /// using d4rt + static List parseD4rtLiteral(String arrayStringLiteral) { + var d4rt = D4rt(); + final buffer = StringBuffer(); + buffer.write("main(){ return $arrayStringLiteral; }"); + final code = buffer.toString(); - final List list = d4rt.execute(source: code); + final List list = d4rt.execute(source: code); - return list; -} - -/// Escapes special characters in a string for use in D4RT literals -String escapeD4rtString(String input) { - return input - .replaceAll(r'\', r'\\') // Escape backslashes first - .replaceAll('\n', r'\n') // Escape newlines - .replaceAll('\r', r'\r') // Escape carriage returns - .replaceAll('\t', r'\t') // Escape tabs - .replaceAll('"', r'\"'); // Escape quotes last -} - -/// Parses corpus elements from an array string literal. -/// Determines if each element is a formula or a unit and converts accordingly. -List parseCorpusElements(String arrayStringLiteral) { - final List elements = parseD4rtLiteral(arrayStringLiteral); - - final List result = []; - for (final element in elements) { - if (element is Map) { - // Check if it's a formula by looking for required formula properties - // Formulas typically have 'd4rtCode' and 'input'/'output' properties - if (element.containsKey('d4rtCode')) { - result.add(Formula.fromSet(element)); - } - // Units typically have 'name', 'symbol', and 'baseUnit' properties - else if (element.containsKey('name') && element.containsKey('symbol')) { - result.add(UnitSpec.fromSet(element)); - } - else { - throw ArgumentError('Unknown element type: $element'); - } - } else { - throw ArgumentError('Element must be a Map: $element'); - } + return list; } - return result; + /// Escapes special characters in a string for use in D4RT literals + static String escapeD4rtString(String input) { + return input + .replaceAll(r'\\', r'\\\\') // escape backslashes first + .replaceAll('\n', r'\\n') + .replaceAll('\r', r'\\r') + .replaceAll('\t', r'\\t') + .replaceAll('"', r'\\"'); + } + + /// Parses corpus elements from an array string literal. + /// Determines if each element is a formula or a unit and converts accordingly. + static List parseCorpusElements(String arrayStringLiteral) { + final List elements = parseD4rtLiteral(arrayStringLiteral); + + final List result = []; + for (final element in elements) { + if (element is Map) { + if (element.containsKey('d4rtCode')) { + result.add(Formula.fromSet(element)); + } else + if (element.containsKey('name') && element.containsKey('symbol')) { + result.add(UnitSpec.fromSet(element)); + } else { + throw ArgumentError('Unknown element type: $element'); + } + } else { + throw ArgumentError('Element must be a Map: $element'); + } + } + + return result; + } } -typedef Number = double; /// Abstract base class for formula elements abstract class FormulaElement { @@ -104,7 +101,7 @@ class UnitSpec implements FormulaElement { String name = SetUtils.stringValue(theSet, "name"); String symbol = SetUtils.stringValue(theSet, "symbol"); - if( theSet.containsKey("isBase") ){ + if (theSet.containsKey("isBase")) { return UnitSpec(name: name, baseUnit: name, symbol: symbol, factorFromUnitToBase: 1); } @@ -118,32 +115,24 @@ class UnitSpec implements FormulaElement { symbol: symbol, factorFromUnitToBase: factorFromUnitToBase, ); - } - else if( theSet.containsKey("toBase")) { - String codeFromBaseToUnit = SetUtils.stringValue( - theSet, - "fromBase", + } else if (theSet.containsKey("toBase")) { + String codeFromBaseToUnit = SetUtils.stringValue(theSet, "fromBase"); + String codeFromUnitToBase = SetUtils.stringValue(theSet, "toBase"); + + return UnitSpec( + name: name, + baseUnit: baseUnit, + symbol: symbol, + codeFromBaseToUnit: codeFromBaseToUnit, + codeFromUnitToBase: codeFromUnitToBase, ); - String codeFromUnitToBase = SetUtils.stringValue( - theSet, - "toBase", - ); - - return UnitSpec(name: name, - baseUnit: baseUnit, - symbol: symbol, - codeFromBaseToUnit: codeFromBaseToUnit, - codeFromUnitToBase: codeFromUnitToBase); + } else { + throw ArgumentError("Need factor or toBase/fromBase"); } - else{ - throw ArgumentError( "Need factor or toBase/fromBase"); - } - - } static List fromArrayStringLiteral(String arrayStringLiteral) { - final List list = parseD4rtLiteral(arrayStringLiteral); + final List list = SetUtils.parseD4rtLiteral(arrayStringLiteral); final units = list.map((set) => UnitSpec.fromSet(set as Map)); @@ -153,18 +142,17 @@ class UnitSpec implements FormulaElement { @override String toStringLiteral() { final buffer = StringBuffer('{'); - buffer.write('"name": "${escapeD4rtString(name)}", "symbol": "${escapeD4rtString(symbol)}"'); + buffer.write('"name": "${SetUtils.escapeD4rtString(name)}", "symbol": "${SetUtils.escapeD4rtString(symbol)}"'); if (name == baseUnit && factorFromUnitToBase == 1) { - // This is a base unit buffer.write(', "isBase": true'); } else { - buffer.write(', "baseUnit": "${escapeD4rtString(baseUnit)}"'); + buffer.write(', "baseUnit": "${SetUtils.escapeD4rtString(baseUnit)}"'); if (factorFromUnitToBase != null) { buffer.write(', "factor": $factorFromUnitToBase'); } else if (codeFromUnitToBase != null && codeFromBaseToUnit != null) { - buffer.write(', "toBase": "${escapeD4rtString(codeFromUnitToBase!)}", "fromBase": "${escapeD4rtString(codeFromBaseToUnit!)}"'); + buffer.write(', "toBase": "${SetUtils.escapeD4rtString(codeFromUnitToBase!)}", "fromBase": "${SetUtils.escapeD4rtString(codeFromBaseToUnit!)}"'); } } @@ -178,16 +166,16 @@ class VariableSpec { final String? unit; final List? values; - VariableSpec({required this.name, this.unit, this.values}){ + VariableSpec({required this.name, this.unit, this.values}) { validate(); } - void validate(){ - if( FormulaEvaluator.reservedVariableNames.contains(name) ){ + void validate() { + if (FormulaEvaluator.reservedVariableNames.contains(name)) { throw ArgumentError("$name: is a reserved variable name for FormulaEvaluator"); } final valuesValid = values != null && values?.isNotEmpty == true; - if( unit == null && !valuesValid ){ + if (unit == null && !valuesValid) { throw ArgumentError("$name: at least unit or allowedValues should be valid"); } } @@ -210,20 +198,20 @@ class VariableSpec { @override String toStringLiteral() { final buffer = StringBuffer('{'); - buffer.write('"name": "${escapeD4rtString(name)}"'); + buffer.write('"name": "${SetUtils.escapeD4rtString(name)}"'); if (unit != null) { - buffer.write(', "unit": "${escapeD4rtString(unit!)}"'); + buffer.write(', "unit": "${SetUtils.escapeD4rtString(unit!)}"'); } if (values != null && values!.isNotEmpty) { buffer.write(', "values": [${values!.map((value) { if (value is String) { - return '"${escapeD4rtString(value)}"'; + return '"${SetUtils.escapeD4rtString(value)}"'; } else { return value.toString(); } - }).join(", ")}]'); + }).join(", ")} ]'); } buffer.write('}'); @@ -276,8 +264,7 @@ class Formula implements FormulaElement { int get hashCode => Object.hash(name, description, ListEquality().hash(input), output, d4rtCode, ListEquality().hash(tags)); - List inputVarNames() => - input.map((v) => v.name).toList(growable: false); + List inputVarNames() => input.map((v) => v.name).toList(growable: false); factory Formula.fromStringLiteral(String setStringLiteral) { var d4rt = D4rt(); @@ -291,7 +278,7 @@ class Formula implements FormulaElement { } static List fromArrayStringLiteral(String arrayStringLiteral) { - final List list = parseD4rtLiteral(arrayStringLiteral); + final List list = SetUtils.parseD4rtLiteral(arrayStringLiteral); final formulas = list.map((set) => Formula.fromSet(set as Map)); @@ -323,13 +310,11 @@ class Formula implements FormulaElement { } String name = SetUtils.stringValue(theSet, "name"); - String? description = theSet ["description"] as String?; + String? description = theSet["description"] as String?; List tags = (theSet["tags"] as List? ?? []).map((t) => t.toString()).toList(); final List inputSet = SetUtils.listValue(theSet, "input"); - List input = inputSet - .map((v) => parseVar(v as Map)) - .toList(growable: false); - Map outputSet = theSet.get("output"); + List input = inputSet.map((v) => parseVar(v as Map)).toList(growable: false); + Map outputSet = theSet['output'] as Map; VariableSpec output = parseVar(outputSet); String d4rtCode = SetUtils.stringValue(theSet, "d4rtCode"); @@ -350,7 +335,7 @@ class Formula implements FormulaElement { final inputStrings = input.map((varSpec) => varSpec.toStringLiteral()).toList(); final buffer = StringBuffer('{'); - buffer.write('"name": "$name"'); + buffer.write('"name": "${SetUtils.escapeD4rtString(name)}"'); if (description != null) { buffer.write(', "description": r"""${description!}"""'); @@ -362,7 +347,7 @@ class Formula implements FormulaElement { buffer.write(', "d4rtCode": r"""$d4rtCode"""'); if (tags.isNotEmpty) { - buffer.write(', "tags": [${tags.map((tag) => '"${escapeD4rtString(tag)}"').join(", ")}]'); + buffer.write(', "tags": [${tags.map((tag) => '"${SetUtils.escapeD4rtString(tag)}"').join(", ")}]'); } buffer.write('}'); diff --git a/lib/main.dart b/lib/main.dart index a1e6684..bb2421a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,7 +2,6 @@ import 'package:d4rt_formulas/d4rt_formulas.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; import 'database/database_service.dart'; -import 'package:drift/drift.dart' as drift; import 'service_locator.dart'; import 'ai/formula_list.dart'; diff --git a/test/dart_test.dart b/test/dart_test.dart index ccedad0..64802b5 100644 --- a/test/dart_test.dart +++ b/test/dart_test.dart @@ -1,5 +1,5 @@ import 'package:test/test.dart'; -import 'package:d4rt/d4rt.dart'; + void main(){ diff --git a/test/formula_models_test.dart b/test/formula_models_test.dart index 01a9953..af71609 100644 --- a/test/formula_models_test.dart +++ b/test/formula_models_test.dart @@ -154,7 +154,7 @@ void main() { ); final literal = originalUnit.toStringLiteral(); - final parsedList = parseD4rtLiteral('[${literal}]'); + final parsedList = SetUtils.parseD4rtLiteral('[${literal}]'); final parsedMap = parsedList[0] as Map; final parsedUnit = UnitSpec.fromSet(parsedMap); From 803b98d7aca3642c01d667fefd1c9ae87efd0fe6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Wed, 4 Mar 2026 19:09:48 +0100 Subject: [PATCH 4/9] prettyPrint done --- TODO.md | 6 + lib/ai/formula_list.dart | 7 +- lib/formula_models.dart | 249 ++++++++++++++++++++++------------ test/formula_models_test.dart | 17 +-- 4 files changed, 177 insertions(+), 102 deletions(-) diff --git a/TODO.md b/TODO.md index 5a4f019..fbcac80 100644 --- a/TODO.md +++ b/TODO.md @@ -47,5 +47,11 @@ - [R] There is one row for the ouput variable, similar to the row for the input variable - [R] d4rtCode is a text area with dart syntax highligthing - [R] At the botton, a button allows to test the edited Formula, launching a FormulaScreen +- [X] Create SetUtils.prettyPrint(): receives a dynamic Set, Array, string or number. Convert to a dart representation of than value (a set/array literal), json-like, but for dart language. Do it recursivelly on local functions to that method: + - _prettyPrintString(String s, int indent): Only for simple strings + - _prettyPrintNumber(Number n, int indent) + - _prettyPrintSet(Set s, int indent) + - _prettyPrintArray(dynamic[] a, int indent) + - _prettyPrintRawString(String s, int indent): Use _prettyPrintRawString when the string contains newlines, $, backlash... - [ ] When _FormulaScreenState._evaluateFormula() detect an error, instead of show an SnackBar, show a ExpansionTile with "⚠️ There were an error. Show details..." with the details of the exception. The ExpansionTile will be invisible if there is no error. - [ ] Investigate https://pub.dev/packages/quantity diff --git a/lib/ai/formula_list.dart b/lib/ai/formula_list.dart index fce2c1d..bd99cac 100644 --- a/lib/ai/formula_list.dart +++ b/lib/ai/formula_list.dart @@ -56,12 +56,7 @@ class _FormulaListState extends State { String _formulaAndDependenciesToStringLiteral(Formula formula) { // Get the formula and its dependencies final dependencies = widget.corpus.withDependencies(formula); - - // Convert each dependency to its string literal representation - final literals = dependencies.map((element) => element.toStringLiteral()).toList(); - - // Create an array string literal containing all the elements - return '[${literals.join(', ')}]'; + return SetUtils.prettyPrint(dependencies.map((f) => f.toMap()).toList()); } void _shareFormula(Formula formula) async { diff --git a/lib/formula_models.dart b/lib/formula_models.dart index faeab2e..5f80397 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -38,6 +38,7 @@ abstract class SetUtils { } /// Escapes special characters in a string for use in D4RT literals + @deprecated static String escapeD4rtString(String input) { return input .replaceAll(r'\\', r'\\\\') // escape backslashes first @@ -70,17 +71,112 @@ abstract class SetUtils { return result; } + + /// Pretty prints a dynamic value (Set, Array, string or number) as a Dart literal. + /// Uses JSON-like formatting but for Dart language, with proper indentation. + static String prettyPrint(dynamic value, {int indent = 0}) { + if (value is String) { + return _prettyPrintString(value, indent); + } else if (value is num) { + return _prettyPrintNumber(value, indent); + } else if (value is Set) { + return _prettyPrintSet(value, indent); + } else if (value is List) { + return _prettyPrintArray(value, indent); + } else if (value is Map) { + return _prettyPrintMap(value, indent); + } else { + return value.toString(); + } + } + + /// Pretty prints a simple string, escaping special characters if needed. + static String _prettyPrintString(String s, int indent) { + // Check if the string needs raw string formatting (newlines, $, backslashes, quotes) + final needsRawString = s.contains('\n') || + s.contains(r'$') || + s.contains(r'\') || + s.contains('"'); + + if (needsRawString) { + return _prettyPrintRawString(s, indent); + } + + // Simple string with escaped quotes + return '"${s.replaceAll('"', r'\"')}"'; + //' + } + + /// Pretty prints a number. + static String _prettyPrintNumber(num n, int indent) { + return n.toString(); + } + + /// Pretty prints a Set as a Dart set literal. + static String _prettyPrintSet(Set s, int indent) { + if (s.isEmpty) { + return '{}'; + } + + final indentStr = ' ' * indent; + final innerIndent = ' ' * (indent + 1); + + final elements = s.map((e) => '$innerIndent${prettyPrint(e, indent: indent + 1)}').join(',\n'); + return '{$elements\n$indentStr}'; + } + + /// Pretty prints an Array/List as a Dart list literal. + static String _prettyPrintArray(List a, int indent) { + if (a.isEmpty) { + return '[]'; + } + + final indentStr = ' ' * indent; + final innerIndent = ' ' * (indent + 1); + + final elements = a.map((e) => '$innerIndent${prettyPrint(e, indent: indent + 1)}').join(',\n'); + return '[\n$elements\n$indentStr]'; + } + + /// Pretty prints a Map as a Dart map literal. + static String _prettyPrintMap(Map m, int indent) { + if (m.isEmpty) { + return '{}'; + } + + final indentStr = ' ' * indent; + final innerIndent = ' ' * (indent + 1); + + final entries = m.entries.map((e) { + final key = prettyPrint(e.key, indent: indent + 1); + final value = prettyPrint(e.value, indent: indent + 1); + return '$innerIndent$key: $value'; + }).join(',\n'); + + return '{\n$entries\n$indentStr}'; + } + + /// Pretty prints a raw string (for strings containing newlines, $, backslashes, etc.) + /// Uses Dart's raw string syntax r"""...""" + static String _prettyPrintRawString(String s, int indent) { + // Escape triple quotes by replacing """ with ""\" + final escaped = s.replaceAll('"""', r'""\"'); + return 'r"""$escaped"""'; + } } /// Abstract base class for formula elements abstract class FormulaElement { - /// Creates a string literal representation of the FormulaElement that can be parsed - /// by the D4RT parser to recreate the same FormulaElement object. - String toStringLiteral(); + Map toMap(); + + String toStringLiteral() { + final map = toMap(); + return SetUtils.prettyPrint(map); + } } -class UnitSpec implements FormulaElement { +class UnitSpec extends FormulaElement { final String name; final String baseUnit; final String symbol; @@ -88,6 +184,19 @@ class UnitSpec implements FormulaElement { final String? codeFromUnitToBase; final String? codeFromBaseToUnit; + @override + Map toMap() { + return { + "name": name, + "baseUnit": baseUnit, + "symbol": symbol, + if (factorFromUnitToBase != null) 'factor': factorFromUnitToBase, + if (codeFromUnitToBase != null) 'toBase': codeFromUnitToBase, + if (codeFromBaseToUnit != null) 'fromBase': codeFromBaseToUnit, + }; + } + + UnitSpec({ required this.name, required this.baseUnit, @@ -139,33 +248,22 @@ class UnitSpec implements FormulaElement { return units.toList(growable: false); } - @override - String toStringLiteral() { - final buffer = StringBuffer('{'); - buffer.write('"name": "${SetUtils.escapeD4rtString(name)}", "symbol": "${SetUtils.escapeD4rtString(symbol)}"'); - - if (name == baseUnit && factorFromUnitToBase == 1) { - buffer.write(', "isBase": true'); - } else { - buffer.write(', "baseUnit": "${SetUtils.escapeD4rtString(baseUnit)}"'); - - if (factorFromUnitToBase != null) { - buffer.write(', "factor": $factorFromUnitToBase'); - } else if (codeFromUnitToBase != null && codeFromBaseToUnit != null) { - buffer.write(', "toBase": "${SetUtils.escapeD4rtString(codeFromUnitToBase!)}", "fromBase": "${SetUtils.escapeD4rtString(codeFromBaseToUnit!)}"'); - } - } - - buffer.write('}'); - return buffer.toString(); - } } -class VariableSpec { +class VariableSpec extends FormulaElement{ final String name; final String? unit; final List? values; + @override + Map toMap() { + return { + 'name': name, + if (unit != null) 'unit': unit, + if (values != null) 'values': List.from(values!,growable: false), + }; + } + VariableSpec({required this.name, this.unit, this.values}) { validate(); } @@ -195,31 +293,10 @@ class VariableSpec { @override int get hashCode => Object.hash(unit, name, values != null ? const DeepCollectionEquality().hash(values!) : 0); - @override - String toStringLiteral() { - final buffer = StringBuffer('{'); - buffer.write('"name": "${SetUtils.escapeD4rtString(name)}"'); - if (unit != null) { - buffer.write(', "unit": "${SetUtils.escapeD4rtString(unit!)}"'); - } - - if (values != null && values!.isNotEmpty) { - buffer.write(', "values": [${values!.map((value) { - if (value is String) { - return '"${SetUtils.escapeD4rtString(value)}"'; - } else { - return value.toString(); - } - }).join(", ")} ]'); - } - - buffer.write('}'); - return buffer.toString(); - } } -class Formula implements FormulaElement { +class Formula extends FormulaElement { final String name; final String? description; final List input; @@ -227,6 +304,18 @@ class Formula implements FormulaElement { final String d4rtCode; final List tags; + @override + Map toMap() { + return { + 'name': name, + if (description != null) 'description': description, + 'input': input.map((v) => v.toMap()).toList(growable: false), + 'output': output.toMap(), + 'd4rtCode': d4rtCode, + if (tags.isNotEmpty) 'tags': List.from(tags, growable: false), + }; + } + Formula({ required this.name, this.description, @@ -239,7 +328,9 @@ class Formula implements FormulaElement { } void validate() { - if (name.trim().isEmpty) { + if (name + .trim() + .isEmpty) { throw ArgumentError('Formula name cannot be empty'); } } @@ -251,20 +342,23 @@ class Formula implements FormulaElement { @override bool operator ==(Object other) => identical(this, other) || - other is Formula && - runtimeType == other.runtimeType && - name == other.name && - description == other.description && - output == other.output && - ListEquality().equals(input, other.input) && - d4rtCode == other.d4rtCode && - ListEquality().equals(tags, other.tags); + other is Formula && + runtimeType == other.runtimeType && + name == other.name && + description == other.description && + output == other.output && + ListEquality().equals(input, other.input) && + d4rtCode == other.d4rtCode && + ListEquality().equals(tags, other.tags); @override int get hashCode => - Object.hash(name, description, ListEquality().hash(input), output, d4rtCode, ListEquality().hash(tags)); + Object.hash( + name, description, ListEquality().hash(input), output, d4rtCode, + ListEquality().hash(tags)); - List inputVarNames() => input.map((v) => v.name).toList(growable: false); + List inputVarNames() => + input.map((v) => v.name).toList(growable: false); factory Formula.fromStringLiteral(String setStringLiteral) { var d4rt = D4rt(); @@ -296,9 +390,11 @@ class Formula implements FormulaElement { if (allowed != null) { final types = allowed.map((v) => v.runtimeType).toSet(); if (types.length > 1) { - throw ArgumentError('Allowed values must be all Strings or all Numbers'); + throw ArgumentError( + 'Allowed values must be all Strings or all Numbers'); } - if (!types.contains(String) && !types.contains(double) && !types.contains(int)) { + if (!types.contains(String) && !types.contains(double) && + !types.contains(int)) { throw ArgumentError('Allowed values must be Strings or Numbers'); } } @@ -311,9 +407,11 @@ class Formula implements FormulaElement { String name = SetUtils.stringValue(theSet, "name"); String? description = theSet["description"] as String?; - List tags = (theSet["tags"] as List? ?? []).map((t) => t.toString()).toList(); + List tags = (theSet["tags"] as List? ?? []).map((t) => + t.toString()).toList(); final List inputSet = SetUtils.listValue(theSet, "input"); - List input = inputSet.map((v) => parseVar(v as Map)).toList(growable: false); + List input = inputSet.map((v) => parseVar(v as Map)).toList( + growable: false); Map outputSet = theSet['output'] as Map; VariableSpec output = parseVar(outputSet); String d4rtCode = SetUtils.stringValue(theSet, "d4rtCode"); @@ -327,30 +425,5 @@ class Formula implements FormulaElement { d4rtCode: d4rtCode, ); } - - /// Creates a string literal representation of the Formula that can be parsed - /// by the D4RT parser to recreate the same Formula object. - @override - String toStringLiteral() { - final inputStrings = input.map((varSpec) => varSpec.toStringLiteral()).toList(); - - final buffer = StringBuffer('{'); - buffer.write('"name": "${SetUtils.escapeD4rtString(name)}"'); - - if (description != null) { - buffer.write(', "description": r"""${description!}"""'); - } - - buffer.write(', "input": [${inputStrings.join(", ")}]'); - buffer.write(', "output": ${output.toStringLiteral()}'); - - buffer.write(', "d4rtCode": r"""$d4rtCode"""'); - - if (tags.isNotEmpty) { - buffer.write(', "tags": [${tags.map((tag) => '"${SetUtils.escapeD4rtString(tag)}"').join(", ")}]'); - } - - buffer.write('}'); - return buffer.toString(); - } } + diff --git a/test/formula_models_test.dart b/test/formula_models_test.dart index af71609..fb1ae67 100644 --- a/test/formula_models_test.dart +++ b/test/formula_models_test.dart @@ -246,22 +246,22 @@ void main() { test('Corpus.withDependencies returns formula and its dependencies', () async { final corpus = await testCorpus; - + // Get a formula that has units associated with it final formula = corpus.getFormula("Newton's Second Law"); expect(formula, isNotNull); - + // Call withDependencies method final dependencies = corpus.withDependencies(formula!); - + // Check that the formula itself is included expect(dependencies.any((element) => element is Formula && element.name == formula.name), true); - + // Check that units from input and output are included for (final inputVar in formula.input) { if (inputVar.unit != null) { expect(dependencies.any((element) => element is UnitSpec && element.name == inputVar.unit), true); - + // Check that units with same base unit are included final unitsWithSameBase = corpus.unitsOfSameMagnitude(inputVar.unit!); for (final unitName in unitsWithSameBase) { @@ -269,20 +269,21 @@ void main() { } } } - + if (formula.output.unit != null) { expect(dependencies.any((element) => element is UnitSpec && element.name == formula.output.unit), true); - + // Check that units with same base unit as output are included final outputUnitsWithSameBase = corpus.unitsOfSameMagnitude(formula.output.unit!); for (final unitName in outputUnitsWithSameBase) { expect(dependencies.any((element) => element is UnitSpec && element.name == unitName), true); } } - + // Verify that there are no duplicates by checking the length of the list vs the set final uniqueDependencies = dependencies.toSet(); expect(dependencies.length, equals(uniqueDependencies.length)); }); + } From 0b0fccd4a3f605945f80025ce7576431fbf26249 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Wed, 4 Mar 2026 19:52:31 +0100 Subject: [PATCH 5/9] UUID for formulas --- TODO.md | 4 ++++ lib/corpus.dart | 19 +++++++++++++++---- lib/formula_models.dart | 31 ++++++++++++++++--------------- pubspec.lock | 2 +- pubspec.yaml | 3 +-- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/TODO.md b/TODO.md index fbcac80..22088a1 100644 --- a/TODO.md +++ b/TODO.md @@ -53,5 +53,9 @@ - _prettyPrintSet(Set s, int indent) - _prettyPrintArray(dynamic[] a, int indent) - _prettyPrintRawString(String s, int indent): Use _prettyPrintRawString when the string contains newlines, $, backlash... +- [X] Add a field to Formula: UUID. + - A constructor without UUID will generate a new random UUID. A constructor with UUID will use the provided UUID. + - The field should be used in database and everywhere instead of the name. The name is not unique anymore, but the UUID is. + - This will be used to identify formulas, instead of the name. This way, we can have formulas with the same name but different UUIDs. The name is not unique anymore. Corpus will be a list of UUIDs, instead of a list of formulas. The corpus.getFormula() method will return the first formula with that name. - [ ] When _FormulaScreenState._evaluateFormula() detect an error, instead of show an SnackBar, show a ExpansionTile with "⚠️ There were an error. Show details..." with the details of the exception. The ExpansionTile will be invisible if there is no error. - [ ] Investigate https://pub.dev/packages/quantity diff --git a/lib/corpus.dart b/lib/corpus.dart index b2f7cf2..acdfdec 100644 --- a/lib/corpus.dart +++ b/lib/corpus.dart @@ -25,12 +25,13 @@ class Multimap extends DelegatingMap> { class Corpus{ final Multimap _tags = Multimap.create(); + // Map formulas by uuid final Map _allFormulas = {}; void loadFormulas(List formulas, {bool replaceOnDuplicates = true, bool checkUnits = true}) { for (final formula in formulas) { - if (!replaceOnDuplicates && _allFormulas.containsKey(formula.name)) { - throw ArgumentError("Duplicate formula:$formula"); + if (!replaceOnDuplicates && _allFormulas.containsKey(formula.uuid)) { + throw ArgumentError("Duplicate formula:${formula}"); } if( checkUnits ){ @@ -41,7 +42,7 @@ class Corpus{ } } - _allFormulas[formula.name] = formula; + _allFormulas[formula.uuid] = formula; for( final tag in formula.tags ){ _tags[tag]?.add(formula); } @@ -59,8 +60,18 @@ class Corpus{ return _allFormulas.values.toList(growable:false); } + /// Returns first formula with the given name (preserves old API semantics). Formula? getFormula(String name) { - return _allFormulas.get(name); + try { + return _allFormulas.values.firstWhere((f) => f.name == name); + } catch (e) { + return null; + } + } + + /// Returns formula by uuid + Formula? getFormulaByUuid(String uuid) { + return _allFormulas[uuid]; } final Multimap _baseToUnits = Multimap.create(); diff --git a/lib/formula_models.dart b/lib/formula_models.dart index 5f80397..1aaa6b9 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -1,6 +1,8 @@ import 'package:d4rt/d4rt.dart'; import 'package:collection/collection.dart'; import 'package:d4rt_formulas/d4rt_formulas.dart'; +import 'dart:math'; +import 'package:uuid/uuid.dart'; typedef Number = double; @@ -45,7 +47,7 @@ abstract class SetUtils { .replaceAll('\n', r'\\n') .replaceAll('\r', r'\\r') .replaceAll('\t', r'\\t') - .replaceAll('"', r'\\"'); + .replaceAll('"', r'\"'); } /// Parses corpus elements from an array string literal. @@ -95,7 +97,7 @@ abstract class SetUtils { // Check if the string needs raw string formatting (newlines, $, backslashes, quotes) final needsRawString = s.contains('\n') || s.contains(r'$') || - s.contains(r'\') || + s.contains(r'\\') || s.contains('"'); if (needsRawString) { @@ -160,7 +162,7 @@ abstract class SetUtils { /// Uses Dart's raw string syntax r"""...""" static String _prettyPrintRawString(String s, int indent) { // Escape triple quotes by replacing """ with ""\" - final escaped = s.replaceAll('"""', r'""\"'); + final escaped = s.replaceAll('"""', r'""\\"'); return 'r"""$escaped"""'; } } @@ -296,7 +298,10 @@ class VariableSpec extends FormulaElement{ } +String _generateUuidV4() => Uuid().v4(); + class Formula extends FormulaElement { + final String uuid; final String name; final String? description; final List input; @@ -306,6 +311,7 @@ class Formula extends FormulaElement { @override Map toMap() { + // UUID NOT INCLUDED ON PURPOSE return { 'name': name, if (description != null) 'description': description, @@ -317,13 +323,14 @@ class Formula extends FormulaElement { } Formula({ + String? uuid = null, required this.name, this.description, required this.input, required this.output, required this.d4rtCode, this.tags = const [], - }) { + }) : uuid = uuid ?? _generateUuidV4() { validate(); } @@ -337,25 +344,17 @@ class Formula extends FormulaElement { @override String toString() => - 'Formula(name: $name, description: $description, input: $input, output: $output, d4rtCode: $d4rtCode, tags: $tags)'; + 'Formula(uuid: $uuid, name: $name, description: $description, input: $input, output: $output, d4rtCode: $d4rtCode, tags: $tags)'; @override bool operator ==(Object other) => identical(this, other) || other is Formula && runtimeType == other.runtimeType && - name == other.name && - description == other.description && - output == other.output && - ListEquality().equals(input, other.input) && - d4rtCode == other.d4rtCode && - ListEquality().equals(tags, other.tags); + uuid == other.uuid; @override - int get hashCode => - Object.hash( - name, description, ListEquality().hash(input), output, d4rtCode, - ListEquality().hash(tags)); + int get hashCode => uuid.hashCode; List inputVarNames() => input.map((v) => v.name).toList(growable: false); @@ -405,6 +404,7 @@ class Formula extends FormulaElement { ); } + String? uuid = theSet['uuid'] as String?; String name = SetUtils.stringValue(theSet, "name"); String? description = theSet["description"] as String?; List tags = (theSet["tags"] as List? ?? []).map((t) => @@ -417,6 +417,7 @@ class Formula extends FormulaElement { String d4rtCode = SetUtils.stringValue(theSet, "d4rtCode"); return Formula( + uuid: uuid, name: name, description: description, tags: tags, diff --git a/pubspec.lock b/pubspec.lock index d7b2023..6971366 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -990,7 +990,7 @@ packages: source: hosted version: "3.1.5" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" diff --git a/pubspec.yaml b/pubspec.yaml index b5a09ea..8c8e51e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,8 +41,7 @@ dependencies: flutter_markdown_plus: flutter_markdown_plus_latex: flutter_code_editor: - - # Drift dependencies for database support + uuid: drift: sqlite3_flutter_libs: path_provider: From bc38acbff653d0c2e255daf04a28da535ba5ead9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Wed, 4 Mar 2026 22:24:37 +0100 Subject: [PATCH 6/9] Initial formula solver --- assets/formulas/electromagnetism.d4rt | 2 +- lib/formula_evaluator.dart | 181 +++++++++++++++++++------- test/formula_solver_test.dart | 26 ++++ 3 files changed, 162 insertions(+), 47 deletions(-) create mode 100644 test/formula_solver_test.dart diff --git a/assets/formulas/electromagnetism.d4rt b/assets/formulas/electromagnetism.d4rt index b5ba790..17f408d 100644 --- a/assets/formulas/electromagnetism.d4rt +++ b/assets/formulas/electromagnetism.d4rt @@ -7,7 +7,7 @@ Calculates the magnitude of the electrostatic force between two point charges. Formula: $F = k \dfrac{q_1 q_2}{r^2}$ where $k = 8.9875517923\times10^9\ \mathrm{N\,m^2/C^2}$. -Inputs: `q1`, `q2` in coulombs; `r` in meters. +Inputs: $q_1$, $q_2$ in coulombs; $r$ in meters. Output: Force `F` in newtons (N).""", "input": [ {"name": "q1", "unit": "coulomb"}, diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart index 73efa5d..f985479 100644 --- a/lib/formula_evaluator.dart +++ b/lib/formula_evaluator.dart @@ -7,11 +7,6 @@ import 'formula_models.dart'; import 'error_handler.dart'; import 'd4rt_bridge.dart'; - - - - - /// Exception thrown when formula evaluation fails class FormulaEvaluationException implements Exception { final String message; @@ -20,26 +15,30 @@ class FormulaEvaluationException implements Exception { const FormulaEvaluationException(this.message, [this.cause]); @override - String toString() => 'FormulaEvaluationException: $message' + String toString() => + 'FormulaEvaluationException: $message' '${cause != null ? ' (caused by: $cause)' : ''}'; } -class MyMath{ +class MyMath { static Number myLog(Number x) => Math.log(x); - static Number myPow(Number b, Number e) => Math.pow(b,e) as Number; + + static Number myPow(Number b, Number e) => Math.pow(b, e) as Number; } -class FormulaResult{ +class FormulaResult { const FormulaResult(); } -class StringResult extends FormulaResult{ +class StringResult extends FormulaResult { final String value; + const StringResult(this.value); } -class NumberResult extends FormulaResult{ +class NumberResult extends FormulaResult { final Number value; + const NumberResult(this.value); } @@ -48,39 +47,44 @@ class FormulaEvaluator { static D4rt createDefaultInterpreter() => D4rt(); - FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? createDefaultInterpreter(){ + FormulaEvaluator([D4rt? interpreter]) + : _interpreter = interpreter ?? createDefaultInterpreter() { prepareInterpreter(_interpreter); } - static Number _getNumberValueOf(String s){ + static Number _getNumberValueOf(String s) { return double.parse(s); } - static void prepareInterpreter(D4rt interpreter){ + static void prepareInterpreter(D4rt interpreter) { final myMathDefinition = BridgedClass( nativeType: MyMath, name: 'MyMath', staticMethods: { 'myPow': (visitor, positionalArgs, namedArgs) { - final Number base = _getNumberValueOf( positionalArgs[0].toString() ); - final Number exp = _getNumberValueOf( positionalArgs[1].toString() ); - return MyMath.myPow(base,exp); + final Number base = _getNumberValueOf(positionalArgs[0].toString()); + final Number exp = _getNumberValueOf(positionalArgs[1].toString()); + return MyMath.myPow(base, exp); }, 'myLog': (visitor, positionalArgs, namedArgs) { - final Number x = _getNumberValueOf( positionalArgs[0].toString() ); + final Number x = _getNumberValueOf(positionalArgs[0].toString()); return MyMath.myLog(x); }, - } + }, + ); + + interpreter.registerBridgedClass( + myMathDefinition, + "package:d4rt_formulas.dart", ); - - interpreter.registerBridgedClass(myMathDefinition, "package:d4rt_formulas.dart"); registerD4rtBridgeBridges(interpreter); } static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) { final d4rtInterpreter = interpreter ?? createDefaultInterpreter(); prepareInterpreter(d4rtInterpreter); - final d4rtCode = """ + final d4rtCode = + """ $preamble main() { @@ -90,7 +94,7 @@ class FormulaEvaluator { }"""; print("evaluateExpression:\n$d4rtCode"); final result = d4rtInterpreter.execute(source: d4rtCode); - switch ( result ){ + switch (result) { case int value: return NumberResult(value.toDouble()); case Number value: @@ -98,23 +102,24 @@ class FormulaEvaluator { case String value: return StringResult(value); default: - throw FormulaEvaluationException( "Unexpected result type: ${result.runtimeType} -- $result" ); + throw FormulaEvaluationException( + "Unexpected result type: ${result.runtimeType} -- $result", + ); } } - + dynamic evaluate(Formula formula, Map inputValues) { _validateInputValues(formula, inputValues); final completeSource = _buildCompleteSource(formula, inputValues); try { final result = _interpreter.execute(source: completeSource); return result; - } - catch (e, stack) { + } catch (e, stack) { // SPECIAL CASE: If the error message starts with signalMagicString, treat it as a signal message and return it instead of throwing an exception // SEE signal() function in the generated d4rt code above for how this is used - print( "#######################"); - if(e.toString().contains(signalMagicString)){ - print( "***********************"); + print("#######################"); + if (e.toString().contains(signalMagicString)) { + print("***********************"); final signalMessage = e.toString().split(signalMagicString).last.trim(); return signalMessage; } @@ -151,8 +156,10 @@ class FormulaEvaluator { if (inputValue != null) { // Convert input value to string for comparison since allowed values are stored as strings final inputValueAsString = inputValue.toString(); - final containsValue = values.any((allowedValue) => allowedValue.toString() == inputValueAsString); - + final containsValue = values.any( + (allowedValue) => allowedValue.toString() == inputValueAsString, + ); + if (!containsValue) { throw FormulaEvaluationException( 'Invalid value for variable "${vs.name}" in formula "${formula.name}". ' @@ -164,15 +171,14 @@ class FormulaEvaluator { } } - - List getInputVariableOrder(Formula formula) { return formula.inputVarNames()..sort(); } static final String signalMagicString = "###"; - static final String preamble = """ + static final String preamble = + """ import 'dart:math'; import "package:d4rt_formulas.dart"; import "package:formulas/runtime_bridge.dart"; @@ -181,23 +187,28 @@ class FormulaEvaluator { """; - static const reservedVariableNames = { "variableValues", "indexOf", "variableAllowedValues"} ; + static const reservedVariableNames = { + "variableValues", + "indexOf", + "variableAllowedValues", + }; - String _buildCompleteSource(Formula formula, Map inputValues) { + String _buildCompleteSource( + Formula formula, + Map inputValues, + ) { final buffer = StringBuffer(); buffer.writeln(""" $preamble main() { - """ - ); - + """); for (final entry in inputValues.entries) { final varName = entry.key; final value = entry.value; - + if (value is String) { final escapedValue = value.replaceAll('"', '\\"'); buffer.writeln(""" @@ -241,13 +252,17 @@ class FormulaEvaluator { for (final vs in formula.input) { final values = vs.values; if (values != null && values.isNotEmpty) { - variableValuesMap[vs.name] = values.map((v) => v.toString()).toList(growable: false); + variableValuesMap[vs.name] = values + .map((v) => v.toString()) + .toList(growable: false); } } // Explicitly include the output VariableSpec if it has allowed values final outValues = formula.output.values; if (outValues != null && outValues.isNotEmpty) { - variableValuesMap[formula.output.name] = outValues.map((v) => v.toString()).toList(growable: false); + variableValuesMap[formula.output.name] = outValues + .map((v) => v.toString()) + .toList(growable: false); } // Write the variableValues map into the generated source without escaping names/values @@ -276,6 +291,80 @@ class FormulaEvaluator { } """); - return buffer.toString(); - } - } + return buffer.toString(); + } +} + +class NoSolutionException implements Exception { + final String message; + + const NoSolutionException(this.message); + + @override + String toString() => 'NoSolutionException: $message'; +} + +Number functionSolver( + Number Function(Number) f, { + Number hint = 0, + Number step = 10, + Number maxDelta = 0.01, + int maxTries = 100, +}) { + Number sign(Number x) => switch (x) { + > 0 => 1, + < 0 => -1, + _ => 0, + }; + + Number binarySearch(Number low, Number high) { + var yLow = f(low); + var yHigh = f(high); + assert(sign(yLow) != sign(yHigh)); + + int count = 0; + while ((high - low).abs() > maxDelta) { + count += 1; + if (count > maxTries) { + throw NoSolutionException("Failed to find a root after $maxTries tries."); + } + var mid = (low + high) / 2; + var yMid = f(mid); + if (sign(yMid) == sign(f(low))) { + low = mid; + yLow = yMid; + } else { + high = mid; + yHigh = yMid; + } + } + return (low + high) / 2; + } + + List searchApproximately(Number x1, Number x2) { + var y1 = f(x1); + var y2 = f(x2); + int count = 0; + while (sign(y1) == sign(y2)) { + count += 1; + if (count > maxTries) { + throw NoSolutionException("Failed to find a root after $maxTries tries."); + } + if (y1.abs() < y2.abs()) { + x2 = x1; + x1 = x1 - step; + y2 = y1; + y1 = f(x1); + } else { + x1 = x2; + x2 = x2 + step; + y1 = y2; + y2 = f(x2); + } + } + return [x1, x2]; + } + + var approx = searchApproximately(hint, hint + step); + return binarySearch(approx[0], approx[1]); +} diff --git a/test/formula_solver_test.dart b/test/formula_solver_test.dart new file mode 100644 index 0000000..3ad65c0 --- /dev/null +++ b/test/formula_solver_test.dart @@ -0,0 +1,26 @@ +import 'package:d4rt_formulas/formula_evaluator.dart'; +import 'package:d4rt_formulas/formula_models.dart'; +import 'package:flutter_test/flutter_test.dart'; + + +void main() { + test("Solve x^2", () { + Number f(Number x) => x*x; + var root = functionSolver(f, hint: 10, step: 1); + expect(root, closeTo(0, 0.1)); + }); + + test("Solve x^2 + 1", () { + Number f(Number x) => x*x+1; + + expect(()=> functionSolver(f, hint: 10, step: 1), throwsA(isA())); + }); + + test("Solve (x-2)(x-10", () { + Number f(Number x) => (x-2)*(x-10); + + expect( functionSolver(f, hint: 10, step: 1), closeTo(10, 0.1)); + }); + + +} From 5fabb4424c724b69b25d3ea0dd47cd9abee046c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 5 Mar 2026 12:47:10 +0100 Subject: [PATCH 7/9] Some tests, seems it is working --- test/formula_solver_test.dart | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/formula_solver_test.dart b/test/formula_solver_test.dart index 3ad65c0..2714a5f 100644 --- a/test/formula_solver_test.dart +++ b/test/formula_solver_test.dart @@ -1,6 +1,7 @@ import 'package:d4rt_formulas/formula_evaluator.dart'; import 'package:d4rt_formulas/formula_models.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'dart:math' as Math; void main() { @@ -10,6 +11,13 @@ void main() { expect(root, closeTo(0, 0.1)); }); + + test("Solve (x-1000)^2", () { + Number f(Number x) => (x-1000)*(x-1000); + var root = functionSolver(f, hint: 10, step: 1, maxTries: 1000); + expect(root, closeTo(1000, 0.1)); + }); + test("Solve x^2 + 1", () { Number f(Number x) => x*x+1; @@ -22,5 +30,28 @@ void main() { expect( functionSolver(f, hint: 10, step: 1), closeTo(10, 0.1)); }); + test('Solve sqrt(x) = 2 => x = 4', () { + Number f(Number x) => Math.sqrt(x) - 2; + var root = functionSolver(f, hint: 5, step: 1); + expect(root, closeTo(4, 0.1)); + }); + + test('Solve sin(x) = 0 near pi (hint 3)', () { + Number f(Number x) => Math.sin(x); + var root = functionSolver(f, hint: 3, step: 1); + expect(root, closeTo(Math.pi, 0.01)); + }); + + test('Solve tan(x) = 1 => x = pi/4', () { + Number f(Number x) => Math.tan(x) - 1; + var root = functionSolver(f, hint: 0, step: 1); + expect(root, closeTo(Math.pi / 4, 0.01)); + }); + + test('Solve exp(x) = 2 => x = ln(2)', () { + Number f(Number x) => Math.exp(x) - 2; + var root = functionSolver(f, hint: 1, step: 1); + expect(root, closeTo(Math.log(2), 0.01)); + }); } From e3c5d9cef91344e8100df87aaf402cc7f9b9bfd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 5 Mar 2026 18:27:07 +0100 Subject: [PATCH 8/9] Formula solver seems to work, now the UI should be reworked --- lib/formula_evaluator.dart | 48 +++++++++++++++++- test/formula_solver_test.dart | 94 +++++++++++++++++++++-------------- 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart index f985479..91f7914 100644 --- a/lib/formula_evaluator.dart +++ b/lib/formula_evaluator.dart @@ -117,9 +117,7 @@ class FormulaEvaluator { } catch (e, stack) { // SPECIAL CASE: If the error message starts with signalMagicString, treat it as a signal message and return it instead of throwing an exception // SEE signal() function in the generated d4rt code above for how this is used - print("#######################"); if (e.toString().contains(signalMagicString)) { - print("***********************"); final signalMessage = e.toString().split(signalMagicString).last.trim(); return signalMessage; } @@ -295,6 +293,52 @@ class FormulaEvaluator { } } +Number formulaSolver(Formula formula, + String variableToSolve, + Map fixedInputValues, { + Number hint = 0, + Number step = 10, + Number maxDelta = 0.01, + int maxTries = 100, + }) { + + if( variableToSolve == formula.output.name ){ + return FormulaEvaluator().evaluate(formula, fixedInputValues); + } + + if (!formula.inputVarNames().contains(variableToSolve) ){ + throw ArgumentError( + 'Variable "$variableToSolve" is not an input or output variable of the formula "${formula + .name}".', + ); + } + + + final modifiedInputValues = Map.from(fixedInputValues); + var evaluator = FormulaEvaluator(); + Number f(Number x) { + modifiedInputValues[variableToSolve] = x; + final result = evaluator.evaluate(formula, modifiedInputValues); + if (result is Number) { + return result; + } else { + throw FormulaEvaluationException( + 'Expected formula evaluation to return a number, but got: $result ${result.runtimeType}', + ); + } + } + + var fixedFormulaOutput = fixedInputValues[formula.output.name]; + + return functionSolver( + (Number x) => f(x) - fixedFormulaOutput, + hint: hint, + step: step, + maxDelta: maxDelta, + maxTries: maxTries, + ); +} + class NoSolutionException implements Exception { final String message; diff --git a/test/formula_solver_test.dart b/test/formula_solver_test.dart index 2714a5f..b0407ae 100644 --- a/test/formula_solver_test.dart +++ b/test/formula_solver_test.dart @@ -5,53 +5,75 @@ import 'dart:math' as Math; void main() { - test("Solve x^2", () { - Number f(Number x) => x*x; - var root = functionSolver(f, hint: 10, step: 1); - expect(root, closeTo(0, 0.1)); + + group("Formulas", (){ + + test("Solve x^2 formula", () { + final formula = Formula( + name: 'Test x^2', + input: [ + VariableSpec(name: 'x', unit: 'scalar'), + ], + output: VariableSpec(name: 'y', unit: 'scalar'), + d4rtCode: 'y = x*x;', + ); + + var solution = formulaSolver(formula, "x", {"y": 25}, maxDelta: 1e-10); + expect( solution, closeTo(5, 1e-10)); + }); + }); + group('Native functions', () { + test("Solve x^2", () { + Number f(Number x) => x * x; + var root = functionSolver(f, hint: 10, step: 1); + expect(root, closeTo(0, 0.1)); + }); - test("Solve (x-1000)^2", () { - Number f(Number x) => (x-1000)*(x-1000); - var root = functionSolver(f, hint: 10, step: 1, maxTries: 1000); - expect(root, closeTo(1000, 0.1)); - }); - test("Solve x^2 + 1", () { - Number f(Number x) => x*x+1; + test("Solve (x-1000)^2", () { + Number f(Number x) => (x - 1000) * (x - 1000); + var root = functionSolver(f, hint: 10, step: 1, maxTries: 1000); + expect(root, closeTo(1000, 0.1)); + }); - expect(()=> functionSolver(f, hint: 10, step: 1), throwsA(isA())); - }); + test("Solve x^2 + 1", () { + Number f(Number x) => x * x + 1; - test("Solve (x-2)(x-10", () { - Number f(Number x) => (x-2)*(x-10); + expect(() => functionSolver(f, hint: 10, step: 1), + throwsA(isA())); + }); - expect( functionSolver(f, hint: 10, step: 1), closeTo(10, 0.1)); - }); + test("Solve (x-2)(x-10", () { + Number f(Number x) => (x - 2) * (x - 10); - test('Solve sqrt(x) = 2 => x = 4', () { - Number f(Number x) => Math.sqrt(x) - 2; - var root = functionSolver(f, hint: 5, step: 1); - expect(root, closeTo(4, 0.1)); - }); + expect(functionSolver(f, hint: 10, step: 1), closeTo(10, 0.1)); + }); - test('Solve sin(x) = 0 near pi (hint 3)', () { - Number f(Number x) => Math.sin(x); - var root = functionSolver(f, hint: 3, step: 1); - expect(root, closeTo(Math.pi, 0.01)); - }); + test('Solve sqrt(x) = 2 => x = 4', () { + Number f(Number x) => Math.sqrt(x) - 2; + var root = functionSolver(f, hint: 5, step: 1); + expect(root, closeTo(4, 0.1)); + }); - test('Solve tan(x) = 1 => x = pi/4', () { - Number f(Number x) => Math.tan(x) - 1; - var root = functionSolver(f, hint: 0, step: 1); - expect(root, closeTo(Math.pi / 4, 0.01)); - }); + test('Solve sin(x) = 0 near pi (hint 3)', () { + Number f(Number x) => Math.sin(x); + var root = functionSolver(f, hint: 3, step: 1); + expect(root, closeTo(Math.pi, 0.01)); + }); - test('Solve exp(x) = 2 => x = ln(2)', () { - Number f(Number x) => Math.exp(x) - 2; - var root = functionSolver(f, hint: 1, step: 1); - expect(root, closeTo(Math.log(2), 0.01)); + test('Solve tan(x) = 1 => x = pi/4', () { + Number f(Number x) => Math.tan(x) - 1; + var root = functionSolver(f, hint: 0, step: 1); + expect(root, closeTo(Math.pi / 4, 0.01)); + }); + + test('Solve exp(x) = 2 => x = ln(2)', () { + Number f(Number x) => Math.exp(x) - 2; + var root = functionSolver(f, hint: 1, step: 1); + expect(root, closeTo(Math.log(2), 0.01)); + }); }); } From 78d7b190db4ad6f8bba2e4c589da34efb30e7191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lvaro=20Gonz=C3=A1lez?= Date: Thu, 5 Mar 2026 18:31:27 +0100 Subject: [PATCH 9/9] line wrap at 120 --- lib/formula_evaluator.dart | 68 ++++++++++++-------------------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/lib/formula_evaluator.dart b/lib/formula_evaluator.dart index 91f7914..492ffc1 100644 --- a/lib/formula_evaluator.dart +++ b/lib/formula_evaluator.dart @@ -47,8 +47,7 @@ class FormulaEvaluator { static D4rt createDefaultInterpreter() => D4rt(); - FormulaEvaluator([D4rt? interpreter]) - : _interpreter = interpreter ?? createDefaultInterpreter() { + FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? createDefaultInterpreter() { prepareInterpreter(_interpreter); } @@ -73,10 +72,7 @@ class FormulaEvaluator { }, ); - interpreter.registerBridgedClass( - myMathDefinition, - "package:d4rt_formulas.dart", - ); + interpreter.registerBridgedClass(myMathDefinition, "package:d4rt_formulas.dart"); registerD4rtBridgeBridges(interpreter); } @@ -102,9 +98,7 @@ class FormulaEvaluator { case String value: return StringResult(value); default: - throw FormulaEvaluationException( - "Unexpected result type: ${result.runtimeType} -- $result", - ); + throw FormulaEvaluationException("Unexpected result type: ${result.runtimeType} -- $result"); } } @@ -123,10 +117,7 @@ class FormulaEvaluator { } errorHandler.notify("$e\n$completeSource", stack); - throw FormulaEvaluationException( - 'Error evaluating formula "${formula.name}": $e', - e, - ); + throw FormulaEvaluationException('Error evaluating formula "${formula.name}": $e', e); } } @@ -154,9 +145,7 @@ class FormulaEvaluator { if (inputValue != null) { // Convert input value to string for comparison since allowed values are stored as strings final inputValueAsString = inputValue.toString(); - final containsValue = values.any( - (allowedValue) => allowedValue.toString() == inputValueAsString, - ); + final containsValue = values.any((allowedValue) => allowedValue.toString() == inputValueAsString); if (!containsValue) { throw FormulaEvaluationException( @@ -185,16 +174,9 @@ class FormulaEvaluator { """; - static const reservedVariableNames = { - "variableValues", - "indexOf", - "variableAllowedValues", - }; + static const reservedVariableNames = {"variableValues", "indexOf", "variableAllowedValues"}; - String _buildCompleteSource( - Formula formula, - Map inputValues, - ) { + String _buildCompleteSource(Formula formula, Map inputValues) { final buffer = StringBuffer(); buffer.writeln(""" @@ -250,17 +232,13 @@ class FormulaEvaluator { for (final vs in formula.input) { final values = vs.values; if (values != null && values.isNotEmpty) { - variableValuesMap[vs.name] = values - .map((v) => v.toString()) - .toList(growable: false); + variableValuesMap[vs.name] = values.map((v) => v.toString()).toList(growable: false); } } // Explicitly include the output VariableSpec if it has allowed values final outValues = formula.output.values; if (outValues != null && outValues.isNotEmpty) { - variableValuesMap[formula.output.name] = outValues - .map((v) => v.toString()) - .toList(growable: false); + variableValuesMap[formula.output.name] = outValues.map((v) => v.toString()).toList(growable: false); } // Write the variableValues map into the generated source without escaping names/values @@ -293,27 +271,25 @@ class FormulaEvaluator { } } -Number formulaSolver(Formula formula, - String variableToSolve, - Map fixedInputValues, { - Number hint = 0, - Number step = 10, - Number maxDelta = 0.01, - int maxTries = 100, - }) { - - if( variableToSolve == formula.output.name ){ +Number formulaSolver( + Formula formula, + String variableToSolve, + Map fixedInputValues, { + Number hint = 0, + Number step = 10, + Number maxDelta = 0.01, + int maxTries = 100, +}) { + if (variableToSolve == formula.output.name) { return FormulaEvaluator().evaluate(formula, fixedInputValues); } - - if (!formula.inputVarNames().contains(variableToSolve) ){ + + if (!formula.inputVarNames().contains(variableToSolve)) { throw ArgumentError( - 'Variable "$variableToSolve" is not an input or output variable of the formula "${formula - .name}".', + 'Variable "$variableToSolve" is not an input or output variable of the formula "${formula.name}".', ); } - final modifiedInputValues = Map.from(fixedInputValues); var evaluator = FormulaEvaluator(); Number f(Number x) {