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)); }); + }