Merge branch 'master' of ssh://codeberg.org/alvarogonzalezsotillo/d4rt_formulas
This commit is contained in:
commit
22e5590c88
17 changed files with 506 additions and 266 deletions
10
TODO.md
10
TODO.md
|
|
@ -47,6 +47,16 @@
|
||||||
- [R] There is one row for the ouput variable, similar to the row for the input variable
|
- [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] d4rtCode is a text area with dart syntax highligthing
|
||||||
- [R] At the botton, a button allows to test the edited Formula, launching a FormulaScreen
|
- [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.
|
- [ ] 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
|
- [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
|
- [ ] Refresh FormulaList each time it gets focus, so formulas are updated from corpus
|
||||||
|
|
|
||||||
|
|
@ -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}$.
|
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).""",
|
Output: Force `F` in newtons (N).""",
|
||||||
"input": [
|
"input": [
|
||||||
{"name": "q1", "unit": "coulomb"},
|
{"name": "q1", "unit": "coulomb"},
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -51,16 +51,15 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
}).toList();
|
}).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 {
|
void _shareFormula(Formula formula) async {
|
||||||
try {
|
try {
|
||||||
// Get the formula and its dependencies
|
final exportString = _formulaAndDependenciesToStringLiteral(formula);
|
||||||
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(', ')}]';
|
|
||||||
|
|
||||||
// Share the string
|
// Share the string
|
||||||
await share_plus.SharePlus.instance.share(
|
await share_plus.SharePlus.instance.share(
|
||||||
|
|
@ -93,14 +92,7 @@ class _FormulaListState extends State<FormulaList> {
|
||||||
|
|
||||||
void _copyFormula(Formula formula) async {
|
void _copyFormula(Formula formula) async {
|
||||||
try {
|
try {
|
||||||
// Get the formula and its dependencies
|
final exportString = _formulaAndDependenciesToStringLiteral(formula);
|
||||||
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(', ')}]';
|
|
||||||
|
|
||||||
// Copy to clipboard
|
// Copy to clipboard
|
||||||
await Clipboard.setData(ClipboardData(text: exportString));
|
await Clipboard.setData(ClipboardData(text: exportString));
|
||||||
|
|
|
||||||
|
|
@ -63,10 +63,10 @@ class D4rtEditingController extends TextEditingController {
|
||||||
if( _validateAsD4rtExpression(text) && _lastValue is StringResult ){
|
if( _validateAsD4rtExpression(text) && _lastValue is StringResult ){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if( _validateAsD4rtExpression('"' + text + '"') && _lastValue is StringResult ){
|
if( _validateAsD4rtExpression('"$text"') && _lastValue is StringResult ){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if( _validateAsD4rtExpression("'" + text + "'") && _lastValue is StringResult ){
|
if( _validateAsD4rtExpression("'$text'") && _lastValue is StringResult ){
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -25,12 +25,13 @@ class Multimap<K, V> extends DelegatingMap<K, List<V>> {
|
||||||
|
|
||||||
class Corpus{
|
class Corpus{
|
||||||
final Multimap<String, Formula> _tags = Multimap.create();
|
final Multimap<String, Formula> _tags = Multimap.create();
|
||||||
|
// Map formulas by uuid
|
||||||
final Map<String, Formula> _allFormulas = {};
|
final Map<String, Formula> _allFormulas = {};
|
||||||
|
|
||||||
void loadFormulas(List<Formula> formulas, {bool replaceOnDuplicates = true, bool checkUnits = true}) {
|
void loadFormulas(List<Formula> formulas, {bool replaceOnDuplicates = true, bool checkUnits = true}) {
|
||||||
for (final formula in formulas) {
|
for (final formula in formulas) {
|
||||||
if (!replaceOnDuplicates && _allFormulas.containsKey(formula.name)) {
|
if (!replaceOnDuplicates && _allFormulas.containsKey(formula.uuid)) {
|
||||||
throw ArgumentError("Duplicate formula:$formula");
|
throw ArgumentError("Duplicate formula:${formula}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if( checkUnits ){
|
if( checkUnits ){
|
||||||
|
|
@ -41,7 +42,7 @@ class Corpus{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_allFormulas[formula.name] = formula;
|
_allFormulas[formula.uuid] = formula;
|
||||||
for( final tag in formula.tags ){
|
for( final tag in formula.tags ){
|
||||||
_tags[tag]?.add(formula);
|
_tags[tag]?.add(formula);
|
||||||
}
|
}
|
||||||
|
|
@ -59,8 +60,18 @@ class Corpus{
|
||||||
return _allFormulas.values.toList(growable:false);
|
return _allFormulas.values.toList(growable:false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns first formula with the given name (preserves old API semantics).
|
||||||
Formula? getFormula(String name) {
|
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
|
/// Updates a formula in the corpus
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import '../formula_models.dart';
|
||||||
import 'corpus_database_interface.dart';
|
import 'corpus_database_interface.dart';
|
||||||
import 'formulas_database.dart';
|
import 'formulas_database.dart';
|
||||||
import 'package:d4rt_formulas/formula_models.dart' as models;
|
import 'package:d4rt_formulas/formula_models.dart' as models;
|
||||||
|
|
@ -11,7 +12,7 @@ extension CorpusDatabaseExtension on FormulasDatabase {
|
||||||
|
|
||||||
for (final element in elements) {
|
for (final element in elements) {
|
||||||
try {
|
try {
|
||||||
final parsed = models.parseCorpusElements('[${element.elementText}]');
|
final parsed = SetUtils.parseCorpusElements('[${element.elementText}]');
|
||||||
print("PARSED:$element");
|
print("PARSED:$element");
|
||||||
parsedElements.addAll(parsed);
|
parsedElements.addAll(parsed);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,6 @@ import 'formula_models.dart';
|
||||||
import 'error_handler.dart';
|
import 'error_handler.dart';
|
||||||
import 'd4rt_bridge.dart';
|
import 'd4rt_bridge.dart';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/// Exception thrown when formula evaluation fails
|
/// Exception thrown when formula evaluation fails
|
||||||
class FormulaEvaluationException implements Exception {
|
class FormulaEvaluationException implements Exception {
|
||||||
final String message;
|
final String message;
|
||||||
|
|
@ -20,26 +15,30 @@ class FormulaEvaluationException implements Exception {
|
||||||
const FormulaEvaluationException(this.message, [this.cause]);
|
const FormulaEvaluationException(this.message, [this.cause]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'FormulaEvaluationException: $message'
|
String toString() =>
|
||||||
|
'FormulaEvaluationException: $message'
|
||||||
'${cause != null ? ' (caused by: $cause)' : ''}';
|
'${cause != null ? ' (caused by: $cause)' : ''}';
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyMath{
|
class MyMath {
|
||||||
static Number myLog(Number x) => Math.log(x);
|
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();
|
const FormulaResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
class StringResult extends FormulaResult{
|
class StringResult extends FormulaResult {
|
||||||
final String value;
|
final String value;
|
||||||
|
|
||||||
const StringResult(this.value);
|
const StringResult(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
class NumberResult extends FormulaResult{
|
class NumberResult extends FormulaResult {
|
||||||
final Number value;
|
final Number value;
|
||||||
|
|
||||||
const NumberResult(this.value);
|
const NumberResult(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,29 +47,29 @@ class FormulaEvaluator {
|
||||||
|
|
||||||
static D4rt createDefaultInterpreter() => D4rt();
|
static D4rt createDefaultInterpreter() => D4rt();
|
||||||
|
|
||||||
FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? createDefaultInterpreter(){
|
FormulaEvaluator([D4rt? interpreter]) : _interpreter = interpreter ?? createDefaultInterpreter() {
|
||||||
prepareInterpreter(_interpreter);
|
prepareInterpreter(_interpreter);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Number _getNumberValueOf(String s){
|
static Number _getNumberValueOf(String s) {
|
||||||
return double.parse(s);
|
return double.parse(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void prepareInterpreter(D4rt interpreter){
|
static void prepareInterpreter(D4rt interpreter) {
|
||||||
final myMathDefinition = BridgedClass(
|
final myMathDefinition = BridgedClass(
|
||||||
nativeType: MyMath,
|
nativeType: MyMath,
|
||||||
name: 'MyMath',
|
name: 'MyMath',
|
||||||
staticMethods: {
|
staticMethods: {
|
||||||
'myPow': (visitor, positionalArgs, namedArgs) {
|
'myPow': (visitor, positionalArgs, namedArgs) {
|
||||||
final Number base = _getNumberValueOf( positionalArgs[0].toString() );
|
final Number base = _getNumberValueOf(positionalArgs[0].toString());
|
||||||
final Number exp = _getNumberValueOf( positionalArgs[1].toString() );
|
final Number exp = _getNumberValueOf(positionalArgs[1].toString());
|
||||||
return MyMath.myPow(base,exp);
|
return MyMath.myPow(base, exp);
|
||||||
},
|
},
|
||||||
'myLog': (visitor, positionalArgs, namedArgs) {
|
'myLog': (visitor, positionalArgs, namedArgs) {
|
||||||
final Number x = _getNumberValueOf( positionalArgs[0].toString() );
|
final Number x = _getNumberValueOf(positionalArgs[0].toString());
|
||||||
return MyMath.myLog(x);
|
return MyMath.myLog(x);
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
interpreter.registerBridgedClass(myMathDefinition, "package:d4rt_formulas.dart");
|
interpreter.registerBridgedClass(myMathDefinition, "package:d4rt_formulas.dart");
|
||||||
|
|
@ -80,7 +79,8 @@ class FormulaEvaluator {
|
||||||
static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) {
|
static FormulaResult evaluateExpression(String code, [D4rt? interpreter]) {
|
||||||
final d4rtInterpreter = interpreter ?? createDefaultInterpreter();
|
final d4rtInterpreter = interpreter ?? createDefaultInterpreter();
|
||||||
prepareInterpreter(d4rtInterpreter);
|
prepareInterpreter(d4rtInterpreter);
|
||||||
final d4rtCode = """
|
final d4rtCode =
|
||||||
|
"""
|
||||||
$preamble
|
$preamble
|
||||||
main()
|
main()
|
||||||
{
|
{
|
||||||
|
|
@ -90,7 +90,7 @@ class FormulaEvaluator {
|
||||||
}""";
|
}""";
|
||||||
print("evaluateExpression:\n$d4rtCode");
|
print("evaluateExpression:\n$d4rtCode");
|
||||||
final result = d4rtInterpreter.execute(source: d4rtCode);
|
final result = d4rtInterpreter.execute(source: d4rtCode);
|
||||||
switch ( result ){
|
switch (result) {
|
||||||
case int value:
|
case int value:
|
||||||
return NumberResult(value.toDouble());
|
return NumberResult(value.toDouble());
|
||||||
case Number value:
|
case Number value:
|
||||||
|
|
@ -98,7 +98,7 @@ class FormulaEvaluator {
|
||||||
case String value:
|
case String value:
|
||||||
return StringResult(value);
|
return StringResult(value);
|
||||||
default:
|
default:
|
||||||
throw FormulaEvaluationException( "Unexpected result type: ${result.runtimeType} -- $result" );
|
throw FormulaEvaluationException("Unexpected result type: ${result.runtimeType} -- $result");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -108,22 +108,16 @@ class FormulaEvaluator {
|
||||||
try {
|
try {
|
||||||
final result = _interpreter.execute(source: completeSource);
|
final result = _interpreter.execute(source: completeSource);
|
||||||
return result;
|
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
|
// 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
|
// SEE signal() function in the generated d4rt code above for how this is used
|
||||||
print( "#######################");
|
if (e.toString().contains(signalMagicString)) {
|
||||||
if(e.toString().contains(signalMagicString)){
|
|
||||||
print( "***********************");
|
|
||||||
final signalMessage = e.toString().split(signalMagicString).last.trim();
|
final signalMessage = e.toString().split(signalMagicString).last.trim();
|
||||||
return signalMessage;
|
return signalMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
errorHandler.notify("$e\n$completeSource", stack);
|
errorHandler.notify("$e\n$completeSource", stack);
|
||||||
throw FormulaEvaluationException(
|
throw FormulaEvaluationException('Error evaluating formula "${formula.name}": $e', e);
|
||||||
'Error evaluating formula "${formula.name}": $e',
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,15 +158,14 @@ class FormulaEvaluator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
List<String> getInputVariableOrder(Formula formula) {
|
List<String> getInputVariableOrder(Formula formula) {
|
||||||
return formula.inputVarNames()..sort();
|
return formula.inputVarNames()..sort();
|
||||||
}
|
}
|
||||||
|
|
||||||
static final String signalMagicString = "###";
|
static final String signalMagicString = "###";
|
||||||
|
|
||||||
static final String preamble = """
|
static final String preamble =
|
||||||
|
"""
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import "package:d4rt_formulas.dart";
|
import "package:d4rt_formulas.dart";
|
||||||
import "package:formulas/runtime_bridge.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) {
|
String _buildCompleteSource(Formula formula, Map<String, dynamic> inputValues) {
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
|
|
@ -190,9 +183,7 @@ class FormulaEvaluator {
|
||||||
$preamble
|
$preamble
|
||||||
main()
|
main()
|
||||||
{
|
{
|
||||||
"""
|
""");
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
for (final entry in inputValues.entries) {
|
for (final entry in inputValues.entries) {
|
||||||
final varName = entry.key;
|
final varName = entry.key;
|
||||||
|
|
@ -276,6 +267,124 @@ class FormulaEvaluator {
|
||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
|
|
||||||
return buffer.toString();
|
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]);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import 'package:d4rt/d4rt.dart';
|
import 'package:d4rt/d4rt.dart';
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:d4rt_formulas/d4rt_formulas.dart';
|
import 'package:d4rt_formulas/d4rt_formulas.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
typedef Number = double;
|
||||||
|
|
||||||
abstract class SetUtils {
|
abstract class SetUtils {
|
||||||
static Object safeGet(Map<Object?, Object?> map, String key) {
|
static Object safeGet(Map<Object?, Object?> map, String key) {
|
||||||
|
|
@ -21,69 +25,160 @@ abstract class SetUtils {
|
||||||
static Number numberValue(Map<Object?, Object?> map, String key) {
|
static Number numberValue(Map<Object?, Object?> map, String key) {
|
||||||
return double.parse(stringValue(map, key));
|
return double.parse(stringValue(map, key));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses a d4rt array literal (containing maps and arrays) to a List<Object?>
|
/// Parses a d4rt array literal (containing maps and arrays) to a List<Object?>
|
||||||
/// using d4rt
|
/// using d4rt
|
||||||
List<Object?> parseD4rtLiteral(String arrayStringLiteral) {
|
static List<Object?> parseD4rtLiteral(String arrayStringLiteral) {
|
||||||
var d4rt = D4rt();
|
var d4rt = D4rt();
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
buffer.write("main(){ return $arrayStringLiteral; }");
|
buffer.write("main(){ return $arrayStringLiteral; }");
|
||||||
final code = buffer.toString();
|
final code = buffer.toString();
|
||||||
|
|
||||||
final List<Object?> list = d4rt.execute(source: code);
|
final List<Object?> list = d4rt.execute(source: code);
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Escapes special characters in a string for use in D4RT literals
|
/// Escapes special characters in a string for use in D4RT literals
|
||||||
String escapeD4rtString(String input) {
|
@deprecated
|
||||||
return input
|
static String escapeD4rtString(String input) {
|
||||||
.replaceAll(r'\', r'\\') // Escape backslashes first
|
return input
|
||||||
.replaceAll('\n', r'\n') // Escape newlines
|
.replaceAll(r'\\', r'\\\\') // escape backslashes first
|
||||||
.replaceAll('\r', r'\r') // Escape carriage returns
|
.replaceAll('\n', r'\\n')
|
||||||
.replaceAll('\t', r'\t') // Escape tabs
|
.replaceAll('\r', r'\\r')
|
||||||
.replaceAll('"', r'\"'); // Escape quotes last
|
.replaceAll('\t', r'\\t')
|
||||||
}
|
.replaceAll('"', r'\"');
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses corpus elements from an array string literal.
|
/// Parses corpus elements from an array string literal.
|
||||||
/// Determines if each element is a formula or a unit and converts accordingly.
|
/// Determines if each element is a formula or a unit and converts accordingly.
|
||||||
List<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
|
static List<FormulaElement> parseCorpusElements(String arrayStringLiteral) {
|
||||||
final List<Object?> elements = parseD4rtLiteral(arrayStringLiteral);
|
final List<Object?> elements = parseD4rtLiteral(arrayStringLiteral);
|
||||||
|
|
||||||
final List<FormulaElement> result = [];
|
final List<FormulaElement> result = [];
|
||||||
for (final element in elements) {
|
for (final element in elements) {
|
||||||
if (element is Map<Object?, Object?>) {
|
if (element is Map<Object?, Object?>) {
|
||||||
// Check if it's a formula by looking for required formula properties
|
if (element.containsKey('d4rtCode')) {
|
||||||
// Formulas typically have 'd4rtCode' and 'input'/'output' properties
|
result.add(Formula.fromSet(element));
|
||||||
if (element.containsKey('d4rtCode')) {
|
} else
|
||||||
result.add(Formula.fromSet(element));
|
if (element.containsKey('name') && element.containsKey('symbol')) {
|
||||||
}
|
result.add(UnitSpec.fromSet(element));
|
||||||
// Units typically have 'name', 'symbol', and 'baseUnit' properties
|
} else {
|
||||||
else if (element.containsKey('name') && element.containsKey('symbol')) {
|
throw ArgumentError('Unknown element type: $element');
|
||||||
result.add(UnitSpec.fromSet(element));
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
throw ArgumentError('Element must be a Map: $element');
|
||||||
throw ArgumentError('Unknown element type: $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 {
|
} 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 base class for formula elements
|
||||||
abstract class FormulaElement {
|
abstract class FormulaElement {
|
||||||
/// Creates a string literal representation of the FormulaElement that can be parsed
|
Map<String,dynamic> toMap();
|
||||||
/// by the D4RT parser to recreate the same FormulaElement object.
|
|
||||||
String toStringLiteral();
|
String toStringLiteral() {
|
||||||
|
final map = toMap();
|
||||||
|
return SetUtils.prettyPrint(map);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class UnitSpec implements FormulaElement {
|
class UnitSpec extends FormulaElement {
|
||||||
final String name;
|
final String name;
|
||||||
final String baseUnit;
|
final String baseUnit;
|
||||||
final String symbol;
|
final String symbol;
|
||||||
|
|
@ -91,6 +186,19 @@ class UnitSpec implements FormulaElement {
|
||||||
final String? codeFromUnitToBase;
|
final String? codeFromUnitToBase;
|
||||||
final String? codeFromBaseToUnit;
|
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({
|
UnitSpec({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.baseUnit,
|
required this.baseUnit,
|
||||||
|
|
@ -104,7 +212,7 @@ class UnitSpec implements FormulaElement {
|
||||||
String name = SetUtils.stringValue(theSet, "name");
|
String name = SetUtils.stringValue(theSet, "name");
|
||||||
String symbol = SetUtils.stringValue(theSet, "symbol");
|
String symbol = SetUtils.stringValue(theSet, "symbol");
|
||||||
|
|
||||||
if( theSet.containsKey("isBase") ){
|
if (theSet.containsKey("isBase")) {
|
||||||
return UnitSpec(name: name, baseUnit: name, symbol: symbol, factorFromUnitToBase: 1);
|
return UnitSpec(name: name, baseUnit: name, symbol: symbol, factorFromUnitToBase: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -118,76 +226,56 @@ class UnitSpec implements FormulaElement {
|
||||||
symbol: symbol,
|
symbol: symbol,
|
||||||
factorFromUnitToBase: factorFromUnitToBase,
|
factorFromUnitToBase: factorFromUnitToBase,
|
||||||
);
|
);
|
||||||
}
|
} else if (theSet.containsKey("toBase")) {
|
||||||
else if( theSet.containsKey("toBase")) {
|
String codeFromBaseToUnit = SetUtils.stringValue(theSet, "fromBase");
|
||||||
String codeFromBaseToUnit = SetUtils.stringValue(
|
String codeFromUnitToBase = SetUtils.stringValue(theSet, "toBase");
|
||||||
theSet,
|
|
||||||
"fromBase",
|
return UnitSpec(
|
||||||
|
name: name,
|
||||||
|
baseUnit: baseUnit,
|
||||||
|
symbol: symbol,
|
||||||
|
codeFromBaseToUnit: codeFromBaseToUnit,
|
||||||
|
codeFromUnitToBase: codeFromUnitToBase,
|
||||||
);
|
);
|
||||||
String codeFromUnitToBase = SetUtils.stringValue(
|
} else {
|
||||||
theSet,
|
throw ArgumentError("Need factor or toBase/fromBase");
|
||||||
"toBase",
|
|
||||||
);
|
|
||||||
|
|
||||||
return UnitSpec(name: name,
|
|
||||||
baseUnit: baseUnit,
|
|
||||||
symbol: symbol,
|
|
||||||
codeFromBaseToUnit: codeFromBaseToUnit,
|
|
||||||
codeFromUnitToBase: codeFromUnitToBase);
|
|
||||||
}
|
}
|
||||||
else{
|
|
||||||
throw ArgumentError( "Need factor or toBase/fromBase");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<UnitSpec> fromArrayStringLiteral(String arrayStringLiteral) {
|
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));
|
final units = list.map((set) => UnitSpec.fromSet(set as Map));
|
||||||
|
|
||||||
return units.toList(growable: false);
|
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 name;
|
||||||
final String? unit;
|
final String? unit;
|
||||||
final List<dynamic>? values;
|
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();
|
validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void validate(){
|
void validate() {
|
||||||
if( FormulaEvaluator.reservedVariableNames.contains(name) ){
|
if (FormulaEvaluator.reservedVariableNames.contains(name)) {
|
||||||
throw ArgumentError("$name: is a reserved variable name for FormulaEvaluator");
|
throw ArgumentError("$name: is a reserved variable name for FormulaEvaluator");
|
||||||
}
|
}
|
||||||
final valuesValid = values != null && values?.isNotEmpty == true;
|
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");
|
throw ArgumentError("$name: at least unit or allowedValues should be valid");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -207,31 +295,13 @@ class VariableSpec {
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(unit, name, values != null ? const DeepCollectionEquality().hash(values!) : 0);
|
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 name;
|
||||||
final String? description;
|
final String? description;
|
||||||
final List<VariableSpec> input;
|
final List<VariableSpec> input;
|
||||||
|
|
@ -239,42 +309,52 @@ class Formula implements FormulaElement {
|
||||||
final String d4rtCode;
|
final String d4rtCode;
|
||||||
final List<String> tags;
|
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({
|
Formula({
|
||||||
|
String? uuid = null,
|
||||||
required this.name,
|
required this.name,
|
||||||
this.description,
|
this.description,
|
||||||
required this.input,
|
required this.input,
|
||||||
required this.output,
|
required this.output,
|
||||||
required this.d4rtCode,
|
required this.d4rtCode,
|
||||||
this.tags = const [],
|
this.tags = const [],
|
||||||
}) {
|
}) : uuid = uuid ?? _generateUuidV4() {
|
||||||
validate();
|
validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
void validate() {
|
void validate() {
|
||||||
if (name.trim().isEmpty) {
|
if (name
|
||||||
|
.trim()
|
||||||
|
.isEmpty) {
|
||||||
throw ArgumentError('Formula name cannot be empty');
|
throw ArgumentError('Formula name cannot be empty');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() =>
|
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
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
identical(this, other) ||
|
identical(this, other) ||
|
||||||
other is Formula &&
|
other is Formula &&
|
||||||
runtimeType == other.runtimeType &&
|
runtimeType == other.runtimeType &&
|
||||||
name == other.name &&
|
uuid == other.uuid;
|
||||||
description == other.description &&
|
|
||||||
output == other.output &&
|
|
||||||
ListEquality().equals(input, other.input) &&
|
|
||||||
d4rtCode == other.d4rtCode &&
|
|
||||||
ListEquality().equals(tags, other.tags);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode => uuid.hashCode;
|
||||||
Object.hash(name, description, ListEquality().hash(input), output, d4rtCode, ListEquality().hash(tags));
|
|
||||||
|
|
||||||
List<String> inputVarNames() =>
|
List<String> inputVarNames() =>
|
||||||
input.map((v) => v.name).toList(growable: false);
|
input.map((v) => v.name).toList(growable: false);
|
||||||
|
|
@ -291,7 +371,7 @@ class Formula implements FormulaElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
static List<Formula> fromArrayStringLiteral(String arrayStringLiteral) {
|
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));
|
final formulas = list.map((set) => Formula.fromSet(set as Map));
|
||||||
|
|
||||||
|
|
@ -309,9 +389,11 @@ class Formula implements FormulaElement {
|
||||||
if (allowed != null) {
|
if (allowed != null) {
|
||||||
final types = allowed.map((v) => v.runtimeType).toSet();
|
final types = allowed.map((v) => v.runtimeType).toSet();
|
||||||
if (types.length > 1) {
|
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');
|
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 name = SetUtils.stringValue(theSet, "name");
|
||||||
String? description = theSet ["description"] as String?;
|
String? description = theSet["description"] as String?;
|
||||||
List<String> tags = (theSet["tags"] as List<Object?>? ?? []).map((t) => t.toString()).toList();
|
List<String> tags = (theSet["tags"] as List<Object?>? ?? []).map((t) =>
|
||||||
|
t.toString()).toList();
|
||||||
final List<Object?> inputSet = SetUtils.listValue(theSet, "input");
|
final List<Object?> inputSet = SetUtils.listValue(theSet, "input");
|
||||||
List<VariableSpec> input = inputSet
|
List<VariableSpec> input = inputSet.map((v) => parseVar(v as Map)).toList(
|
||||||
.map((v) => parseVar(v as Map))
|
growable: false);
|
||||||
.toList(growable: false);
|
Map<Object?, Object?> outputSet = theSet['output'] as Map<Object?, Object?>;
|
||||||
Map<Object?, Object?> outputSet = theSet.get("output");
|
|
||||||
VariableSpec output = parseVar(outputSet);
|
VariableSpec output = parseVar(outputSet);
|
||||||
String d4rtCode = SetUtils.stringValue(theSet, "d4rtCode");
|
String d4rtCode = SetUtils.stringValue(theSet, "d4rtCode");
|
||||||
|
|
||||||
return Formula(
|
return Formula(
|
||||||
|
uuid: uuid,
|
||||||
name: name,
|
name: name,
|
||||||
description: description,
|
description: description,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
|
@ -342,30 +426,5 @@ class Formula implements FormulaElement {
|
||||||
d4rtCode: d4rtCode,
|
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:d4rt_formulas/d4rt_formulas.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'database/database_service.dart';
|
import 'database/database_service.dart';
|
||||||
import 'package:drift/drift.dart' as drift;
|
|
||||||
import 'service_locator.dart';
|
import 'service_locator.dart';
|
||||||
|
|
||||||
import 'ai/formula_list.dart';
|
import 'ai/formula_list.dart';
|
||||||
|
|
|
||||||
|
|
@ -990,7 +990,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.5"
|
version: "3.1.5"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: uuid
|
name: uuid
|
||||||
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
|
||||||
|
|
|
||||||
|
|
@ -41,8 +41,7 @@ dependencies:
|
||||||
flutter_markdown_plus:
|
flutter_markdown_plus:
|
||||||
flutter_markdown_plus_latex:
|
flutter_markdown_plus_latex:
|
||||||
flutter_code_editor:
|
flutter_code_editor:
|
||||||
|
uuid:
|
||||||
# Drift dependencies for database support
|
|
||||||
drift:
|
drift:
|
||||||
sqlite3_flutter_libs:
|
sqlite3_flutter_libs:
|
||||||
path_provider:
|
path_provider:
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
import 'package:d4rt/d4rt.dart';
|
|
||||||
import 'dart:math' as Math;
|
|
||||||
|
|
||||||
|
|
||||||
void main(){
|
void main(){
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:d4rt_formulas/database/database_service.dart';
|
|
||||||
import 'package:d4rt_formulas/service_locator.dart';
|
import 'package:d4rt_formulas/service_locator.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
final literal = originalUnit.toStringLiteral();
|
final literal = originalUnit.toStringLiteral();
|
||||||
final parsedList = parseD4rtLiteral('[${literal}]');
|
final parsedList = SetUtils.parseD4rtLiteral('[${literal}]');
|
||||||
final parsedMap = parsedList[0] as Map<Object?, Object?>;
|
final parsedMap = parsedList[0] as Map<Object?, Object?>;
|
||||||
final parsedUnit = UnitSpec.fromSet(parsedMap);
|
final parsedUnit = UnitSpec.fromSet(parsedMap);
|
||||||
|
|
||||||
|
|
@ -285,4 +285,5 @@ void main() {
|
||||||
expect(dependencies.length, equals(uniqueDependencies.length));
|
expect(dependencies.length, equals(uniqueDependencies.length));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
test/formula_solver_test.dart
Normal file
79
test/formula_solver_test.dart
Normal 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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,6 @@ import 'package:d4rt_formulas/corpus.dart';
|
||||||
import 'package:d4rt_formulas/defaults/default_corpus.dart';
|
import 'package:d4rt_formulas/defaults/default_corpus.dart';
|
||||||
import 'package:d4rt_formulas/formula_evaluator.dart';
|
import 'package:d4rt_formulas/formula_evaluator.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:d4rt_formulas/formula_models.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
TestWidgetsFlutterBinding.ensureInitialized();
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue