diff --git a/TODO.md b/TODO.md index 5c7f1de..8993abb 100644 --- a/TODO.md +++ b/TODO.md @@ -47,6 +47,16 @@ - [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... +- [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. - [R] When FormulaEditor._save formula, ensure formula is updated in the initial FormulaList - [ ] Refresh FormulaList each time it gets focus, so formulas are updated from corpus 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/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/ai/formula_list.dart b/lib/ai/formula_list.dart index a9648e6..6f8ecf7 100644 --- a/lib/ai/formula_list.dart +++ b/lib/ai/formula_list.dart @@ -51,16 +51,15 @@ class _FormulaListState extends State { }).toList(); } + String _formulaAndDependenciesToStringLiteral(Formula formula) { + // Get the formula and its dependencies + final dependencies = widget.corpus.withDependencies(formula); + return SetUtils.prettyPrint(dependencies.map((f) => f.toMap()).toList()); + } + 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( @@ -93,14 +92,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/ai/formula_screen.dart b/lib/ai/formula_screen.dart index 9e92252..b91ebfd 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/lib/corpus.dart b/lib/corpus.dart index e04ee88..aeab11b 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]; } /// Updates a formula in the corpus diff --git a/lib/database/database_service.dart b/lib/database/database_service.dart index 4ccf2de..02a83e3 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_evaluator.dart b/lib/formula_evaluator.dart index 73efa5d..492ffc1 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,31 +47,31 @@ 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"); registerD4rtBridgeBridges(interpreter); } @@ -80,7 +79,8 @@ class FormulaEvaluator { static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) { final d4rtInterpreter = interpreter ?? createDefaultInterpreter(); prepareInterpreter(d4rtInterpreter); - final d4rtCode = """ + final d4rtCode = + """ $preamble main() { @@ -90,7 +90,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,32 +98,26 @@ 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( "***********************"); + if (e.toString().contains(signalMagicString)) { final signalMessage = e.toString().split(signalMagicString).last.trim(); return signalMessage; } errorHandler.notify("$e\n$completeSource", stack); - throw FormulaEvaluationException( - 'Error evaluating formula "${formula.name}": $e', - e, - ); + throw FormulaEvaluationException('Error evaluating formula "${formula.name}": $e', e); } } @@ -152,7 +146,7 @@ class FormulaEvaluator { // 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); - + if (!containsValue) { throw FormulaEvaluationException( 'Invalid value for variable "${vs.name}" in formula "${formula.name}". ' @@ -164,15 +158,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,7 +174,7 @@ class FormulaEvaluator { """; - static const reservedVariableNames = { "variableValues", "indexOf", "variableAllowedValues"} ; + static const reservedVariableNames = {"variableValues", "indexOf", "variableAllowedValues"}; String _buildCompleteSource(Formula formula, Map inputValues) { final buffer = StringBuffer(); @@ -190,14 +183,12 @@ class FormulaEvaluator { $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(""" @@ -276,6 +267,124 @@ class FormulaEvaluator { } """); - return buffer.toString(); - } - } + return buffer.toString(); + } +} + +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; + + 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/lib/formula_models.dart b/lib/formula_models.dart index 12ceaba..1aaa6b9 100644 --- a/lib/formula_models.dart +++ b/lib/formula_models.dart @@ -1,6 +1,10 @@ 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; abstract class SetUtils { static Object safeGet(Map map, String key) { @@ -21,69 +25,160 @@ 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; -} + 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 -} + /// 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 + .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. -List parseCorpusElements(String arrayStringLiteral) { - final List elements = parseD4rtLiteral(arrayStringLiteral); + /// 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) { - // 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'); + 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; + } + + /// 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 { - throw ArgumentError('Element must be a Map: $element'); + return value.toString(); } } - return result; + /// 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"""'; + } } -typedef Number = double; /// 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; @@ -91,6 +186,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, @@ -104,7 +212,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,76 +226,56 @@ 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)); return units.toList(growable: false); } - @override - String toStringLiteral() { - final buffer = StringBuffer('{'); - buffer.write('"name": "${escapeD4rtString(name)}", "symbol": "${escapeD4rtString(symbol)}"'); - - if (name == baseUnit && factorFromUnitToBase == 1) { - // This is a base unit - buffer.write(', "isBase": true'); - } else { - buffer.write(', "baseUnit": "${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('}'); - return buffer.toString(); - } } -class VariableSpec { +class VariableSpec extends FormulaElement{ final String name; final String? unit; final List? values; - VariableSpec({required this.name, this.unit, this.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(); } - 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"); } } @@ -207,31 +295,13 @@ 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": "${escapeD4rtString(name)}"'); - if (unit != null) { - buffer.write(', "unit": "${escapeD4rtString(unit!)}"'); - } - - if (values != null && values!.isNotEmpty) { - buffer.write(', "values": [${values!.map((value) { - if (value is String) { - return '"${escapeD4rtString(value)}"'; - } else { - return value.toString(); - } - }).join(", ")}]'); - } - - buffer.write('}'); - return buffer.toString(); - } } -class Formula implements FormulaElement { +String _generateUuidV4() => Uuid().v4(); + +class Formula extends FormulaElement { + final String uuid; final String name; final String? description; final List input; @@ -239,42 +309,52 @@ class Formula implements FormulaElement { final String d4rtCode; final List tags; + @override + Map toMap() { + // UUID NOT INCLUDED ON PURPOSE + 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({ + String? uuid = null, required this.name, this.description, required this.input, required this.output, required this.d4rtCode, this.tags = const [], - }) { + }) : uuid = uuid ?? _generateUuidV4() { validate(); } void validate() { - if (name.trim().isEmpty) { + if (name + .trim() + .isEmpty) { throw ArgumentError('Formula name cannot be empty'); } } @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); + other is Formula && + runtimeType == other.runtimeType && + 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); @@ -291,7 +371,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)); @@ -309,9 +389,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'); } } @@ -322,18 +404,20 @@ class Formula implements 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) => t.toString()).toList(); + 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"); return Formula( + uuid: uuid, name: name, description: description, tags: tags, @@ -342,30 +426,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": "$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) => '"${escapeD4rtString(tag)}"').join(", ")}]'); - } - - buffer.write('}'); - return buffer.toString(); - } } + diff --git a/lib/main.dart b/lib/main.dart index 3fc772f..38b389e 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/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: diff --git a/test/dart_test.dart b/test/dart_test.dart index 9e78acc..64802b5 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/formula_models_test.dart b/test/formula_models_test.dart index 01a9953..fb1ae67 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); @@ -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)); }); + } diff --git a/test/formula_solver_test.dart b/test/formula_solver_test.dart new file mode 100644 index 0000000..b0407ae --- /dev/null +++ b/test/formula_solver_test.dart @@ -0,0 +1,79 @@ +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() { + + 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; + + 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)); + }); + + 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)); + }); + }); + +} 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();