Merge branch 'master' of ssh://codeberg.org/alvarogonzalezsotillo/d4rt_formulas

This commit is contained in:
Álvaro González 2026-03-06 08:52:47 +01:00
commit 22e5590c88
17 changed files with 506 additions and 266 deletions

10
TODO.md
View file

@ -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

View file

@ -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"},

View file

@ -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
}

View file

@ -51,16 +51,15 @@ class _FormulaListState extends State<FormulaList> {
}).toList();
}
void _shareFormula(Formula formula) async {
try {
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());
}
// 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(', ')}]';
void _shareFormula(Formula formula) async {
try {
final exportString = _formulaAndDependenciesToStringLiteral(formula);
// Share the string
await share_plus.SharePlus.instance.share(
@ -93,14 +92,7 @@ class _FormulaListState extends State<FormulaList> {
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));

View file

@ -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;

View file

@ -25,12 +25,13 @@ class Multimap<K, V> extends DelegatingMap<K, List<V>> {
class Corpus{
final Multimap<String, Formula> _tags = Multimap.create();
// Map formulas by uuid
final Map<String, Formula> _allFormulas = {};
void loadFormulas(List<Formula> 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

View file

@ -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) {

View file

@ -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,29 +47,29 @@ 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");
@ -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,7 +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");
}
}
@ -108,22 +108,16 @@ class FormulaEvaluator {
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);
}
}
@ -164,15 +158,14 @@ class FormulaEvaluator {
}
}
List<String> 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<String, dynamic> inputValues) {
final buffer = StringBuffer();
@ -190,9 +183,7 @@ class FormulaEvaluator {
$preamble
main()
{
"""
);
""");
for (final entry in inputValues.entries) {
final varName = entry.key;
@ -278,4 +269,122 @@ class FormulaEvaluator {
return buffer.toString();
}
}
Number formulaSolver(
Formula formula,
String variableToSolve,
Map<String, dynamic> 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<String, dynamic>.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<Number> 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]);
}

View file

@ -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<Object?, Object?> map, String key) {
@ -21,11 +25,10 @@ abstract class SetUtils {
static Number numberValue(Map<Object?, Object?> map, String key) {
return double.parse(stringValue(map, key));
}
}
/// Parses a d4rt array literal (containing maps and arrays) to a List<Object?>
/// using d4rt
List<Object?> parseD4rtLiteral(String arrayStringLiteral) {
/// Parses a d4rt array literal (containing maps and arrays) to a List<Object?>
/// using d4rt
static List<Object?> parseD4rtLiteral(String arrayStringLiteral) {
var d4rt = D4rt();
final buffer = StringBuffer();
buffer.write("main(){ return $arrayStringLiteral; }");
@ -34,36 +37,33 @@ List<Object?> parseD4rtLiteral(String arrayStringLiteral) {
final List<Object?> list = d4rt.execute(source: code);
return list;
}
}
/// Escapes special characters in a string for use in D4RT literals
String escapeD4rtString(String input) {
/// 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') // Escape newlines
.replaceAll('\r', r'\r') // Escape carriage returns
.replaceAll('\t', r'\t') // Escape tabs
.replaceAll('"', r'\"'); // Escape quotes last
}
.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<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
/// Parses corpus elements from an array string literal.
/// Determines if each element is a formula or a unit and converts accordingly.
static List<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
final List<Object?> elements = parseD4rtLiteral(arrayStringLiteral);
final List<FormulaElement> result = [];
for (final element in elements) {
if (element is Map<Object?, Object?>) {
// 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')) {
} else
if (element.containsKey('name') && element.containsKey('symbol')) {
result.add(UnitSpec.fromSet(element));
}
else {
} else {
throw ArgumentError('Unknown element type: $element');
}
} else {
@ -72,18 +72,113 @@ List<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
}
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"""';
}
}
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<String,dynamic> 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<String, dynamic> 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",
);
String codeFromUnitToBase = SetUtils.stringValue(
theSet,
"toBase",
);
} else if (theSet.containsKey("toBase")) {
String codeFromBaseToUnit = SetUtils.stringValue(theSet, "fromBase");
String codeFromUnitToBase = SetUtils.stringValue(theSet, "toBase");
return UnitSpec(name: name,
return UnitSpec(
name: name,
baseUnit: baseUnit,
symbol: symbol,
codeFromBaseToUnit: codeFromBaseToUnit,
codeFromUnitToBase: codeFromUnitToBase);
codeFromUnitToBase: codeFromUnitToBase,
);
} else {
throw ArgumentError("Need factor or toBase/fromBase");
}
else{
throw ArgumentError( "Need factor or toBase/fromBase");
}
}
static List<UnitSpec> fromArrayStringLiteral(String arrayStringLiteral) {
final List<Object?> list = parseD4rtLiteral(arrayStringLiteral);
final List<Object?> 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<dynamic>? values;
VariableSpec({required this.name, this.unit, this.values}){
@override
Map<String, dynamic> 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<VariableSpec> input;
@ -239,42 +309,52 @@ class Formula implements FormulaElement {
final String d4rtCode;
final List<String> tags;
@override
Map<String, dynamic> 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);
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<String> inputVarNames() =>
input.map((v) => v.name).toList(growable: false);
@ -291,7 +371,7 @@ class Formula implements FormulaElement {
}
static List<Formula> fromArrayStringLiteral(String arrayStringLiteral) {
final List<Object?> list = parseD4rtLiteral(arrayStringLiteral);
final List<Object?> 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<String> tags = (theSet["tags"] as List<Object?>? ?? []).map((t) => t.toString()).toList();
String? description = theSet["description"] as String?;
List<String> tags = (theSet["tags"] as List<Object?>? ?? []).map((t) =>
t.toString()).toList();
final List<Object?> inputSet = SetUtils.listValue(theSet, "input");
List<VariableSpec> input = inputSet
.map((v) => parseVar(v as Map))
.toList(growable: false);
Map<Object?, Object?> outputSet = theSet.get("output");
List<VariableSpec> input = inputSet.map((v) => parseVar(v as Map)).toList(
growable: false);
Map<Object?, Object?> outputSet = theSet['output'] as Map<Object?, Object?>;
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();
}
}

View file

@ -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';

View file

@ -990,7 +990,7 @@ packages:
source: hosted
version: "3.1.5"
uuid:
dependency: transitive
dependency: "direct main"
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"

View file

@ -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:

View file

@ -1,6 +1,5 @@
import 'package:test/test.dart';
import 'package:d4rt/d4rt.dart';
import 'dart:math' as Math;
void main(){

View file

@ -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() {

View file

@ -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<Object?, Object?>;
final parsedUnit = UnitSpec.fromSet(parsedMap);
@ -285,4 +285,5 @@ void main() {
expect(dependencies.length, equals(uniqueDependencies.length));
});
}

View file

@ -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<NoSolutionException>()));
});
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));
});
});
}

View file

@ -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();