2025-08-24 09:52:34 +00:00
import ' package:d4rt/d4rt.dart ' ;
2025-08-28 10:34:49 +00:00
import ' package:collection/collection.dart ' ;
2026-02-01 15:16:04 +00:00
import ' package:d4rt_formulas/d4rt_formulas.dart ' ;
2026-03-09 17:32:39 +00:00
import ' package:d4rt_formulas/set_utils.dart ' ;
2026-03-04 18:52:31 +00:00
import ' dart:math ' ;
import ' package:uuid/uuid.dart ' ;
2025-08-21 15:15:00 +00:00
2026-03-01 12:51:14 +00:00
typedef Number = double ;
2026-02-11 08:07:13 +00:00
/// Abstract base class for formula elements
2026-02-11 08:19:18 +00:00
abstract class FormulaElement {
2026-03-09 17:32:39 +00:00
Map < String , dynamic > toMap ( ) ;
2026-03-04 18:09:48 +00:00
String toStringLiteral ( ) {
final map = toMap ( ) ;
return SetUtils . prettyPrint ( map ) ;
}
2026-02-11 08:19:18 +00:00
}
2025-09-07 11:59:03 +00:00
2026-03-04 18:09:48 +00:00
class UnitSpec extends FormulaElement {
2025-09-07 11:59:03 +00:00
final String name ;
final String baseUnit ;
final String symbol ;
final Number ? factorFromUnitToBase ;
final String ? codeFromUnitToBase ;
final String ? codeFromBaseToUnit ;
2026-03-04 18:09:48 +00:00
@ 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 ,
} ;
}
2025-09-07 11:59:03 +00:00
UnitSpec ( {
required this . name ,
required this . baseUnit ,
required this . symbol ,
this . factorFromUnitToBase ,
this . codeFromBaseToUnit ,
this . codeFromUnitToBase ,
} ) ;
factory UnitSpec . fromSet ( Map < Object ? , Object ? > theSet ) {
String name = SetUtils . stringValue ( theSet , " name " ) ;
String symbol = SetUtils . stringValue ( theSet , " symbol " ) ;
2026-03-01 12:51:14 +00:00
if ( theSet . containsKey ( " isBase " ) ) {
2025-09-07 12:04:42 +00:00
return UnitSpec ( name: name , baseUnit: name , symbol: symbol , factorFromUnitToBase: 1 ) ;
2025-09-07 11:59:03 +00:00
}
String baseUnit = SetUtils . stringValue ( theSet , " baseUnit " ) ;
if ( theSet . containsKey ( " factor " ) ) {
Number factorFromUnitToBase = SetUtils . numberValue ( theSet , " factor " ) ;
2026-03-09 17:32:39 +00:00
return UnitSpec ( name: name , baseUnit: baseUnit , symbol: symbol , factorFromUnitToBase: factorFromUnitToBase ) ;
2026-03-01 12:51:14 +00:00
} else if ( theSet . containsKey ( " toBase " ) ) {
String codeFromBaseToUnit = SetUtils . stringValue ( theSet , " fromBase " ) ;
String codeFromUnitToBase = SetUtils . stringValue ( theSet , " toBase " ) ;
2025-09-07 11:59:03 +00:00
2026-03-01 12:51:14 +00:00
return UnitSpec (
name: name ,
baseUnit: baseUnit ,
symbol: symbol ,
codeFromBaseToUnit: codeFromBaseToUnit ,
codeFromUnitToBase: codeFromUnitToBase ,
) ;
} else {
throw ArgumentError ( " Need factor or toBase/fromBase " ) ;
2025-09-07 11:59:03 +00:00
}
}
static List < UnitSpec > fromArrayStringLiteral ( String arrayStringLiteral ) {
2026-03-01 12:51:14 +00:00
final List < Object ? > list = SetUtils . parseD4rtLiteral ( arrayStringLiteral ) ;
2025-09-07 11:59:03 +00:00
final units = list . map ( ( set ) = > UnitSpec . fromSet ( set as Map ) ) ;
return units . toList ( growable: false ) ;
}
}
2026-03-09 17:32:39 +00:00
class VariableSpec extends FormulaElement {
2025-08-24 09:52:34 +00:00
final String name ;
2025-11-09 19:29:58 +00:00
final String ? unit ;
2026-01-21 07:49:56 +00:00
final List < dynamic > ? values ;
2025-08-21 15:15:00 +00:00
2026-03-04 18:09:48 +00:00
@ override
Map < String , dynamic > toMap ( ) {
return {
' name ' : name ,
if ( unit ! = null ) ' unit ' : unit ,
2026-03-09 17:32:39 +00:00
if ( values ! = null ) ' values ' : List . from ( values ! , growable: false ) ,
2026-03-04 18:09:48 +00:00
} ;
2026-03-09 17:32:39 +00:00
}
2026-03-04 18:09:48 +00:00
2026-03-01 12:51:14 +00:00
VariableSpec ( { required this . name , this . unit , this . values } ) {
2026-02-01 15:16:04 +00:00
validate ( ) ;
}
2026-03-01 12:51:14 +00:00
void validate ( ) {
if ( FormulaEvaluator . reservedVariableNames . contains ( name ) ) {
2026-02-01 15:16:04 +00:00
throw ArgumentError ( " $ name : is a reserved variable name for FormulaEvaluator " ) ;
}
2026-01-21 07:49:56 +00:00
final valuesValid = values ! = null & & values ? . isNotEmpty = = true ;
2026-03-01 12:51:14 +00:00
if ( unit = = null & & ! valuesValid ) {
2026-01-21 07:49:56 +00:00
throw ArgumentError ( " $ name : at least unit or allowedValues should be valid " ) ;
2025-11-09 19:29:58 +00:00
}
}
2025-08-21 15:15:00 +00:00
@ override
2026-01-21 07:49:56 +00:00
String toString ( ) = > ' var( $ name : $ unit ${ values ! = null ? ' allowed: $ values ' : ' ' } ) ' ;
2025-08-21 15:15:00 +00:00
@ override
bool operator = = ( Object other ) = >
identical ( this , other ) | |
other is VariableSpec & &
runtimeType = = other . runtimeType & &
2025-09-15 19:42:15 +00:00
unit = = other . unit & &
2025-11-09 19:29:58 +00:00
name = = other . name & &
2026-01-21 07:49:56 +00:00
const DeepCollectionEquality ( ) . equals ( values , other . values ) ;
2025-08-21 15:15:00 +00:00
@ override
2026-01-21 07:49:56 +00:00
int get hashCode = > Object . hash ( unit , name , values ! = null ? const DeepCollectionEquality ( ) . hash ( values ! ) : 0 ) ;
2026-03-09 17:32:39 +00:00
}
String _generateUuidV4 ( ) = > Uuid ( ) . v4 ( ) ;
abstract class FormulaInterface {
String get uuid ;
String get name ;
String ? get description ;
List < VariableSpec > get input ;
VariableSpec get output ;
String get d4rtCode ;
List < String > get tags ;
2026-02-11 07:56:43 +00:00
2026-03-09 17:32:39 +00:00
Map < String , dynamic > toMap ( ) ;
2026-02-15 11:10:48 +00:00
2026-03-09 17:32:39 +00:00
Formula get originalFormula ;
2026-03-10 15:47:03 +00:00
static Formula getRootFormula ( FormulaInterface fi ) {
if ( fi is DerivedFormula ) {
return getRootFormula ( fi . originalFormula ) ;
}
if ( fi is Formula ) {
return fi as Formula ;
}
throw ArgumentError ( " Is not a known Formula subclass: ${ fi } ${ fi . runtimeType } " ) ;
}
2025-08-21 15:15:00 +00:00
}
2026-03-09 17:32:39 +00:00
class DerivedFormula implements FormulaInterface {
@ override
String get uuid = > originalFormula . uuid ;
@ override
2026-03-10 15:47:03 +00:00
String get name = > " ${ originalFormula . name } (Solving for ${ _output . name } ) " ;
2026-03-09 17:32:39 +00:00
@ override
String ? get description = > originalFormula . description ;
@ override
List < VariableSpec > get input = > _input ;
@ override
VariableSpec get output = > _output ;
@ override
String get d4rtCode = > " signal('no code for derived formula, use formulaSolver') " ;
@ override
List < String > get tags = > originalFormula . tags ;
@ override
2026-03-10 15:47:03 +00:00
late final Formula originalFormula ;
2026-03-09 17:32:39 +00:00
@ override
Map < String , dynamic > toMap ( ) = > originalFormula . toMap ( ) ;
String outputName ;
late List < VariableSpec > _input ;
late VariableSpec _output ;
2026-03-10 15:47:03 +00:00
static bool isDerivable ( Formula f ) {
return f . input . every ( ( vs ) = > vs . unit ! = " string " ) & & f . output . unit ! = " string " ;
}
2026-03-10 18:04:25 +00:00
DerivedFormula ( { required this . outputName , required this . originalFormula } ) {
2026-03-09 17:32:39 +00:00
2026-03-10 15:47:03 +00:00
2026-03-10 18:04:25 +00:00
if ( ! isDerivable ( originalFormula ) ) {
2026-03-09 17:32:39 +00:00
throw ArgumentError (
2026-03-10 15:47:03 +00:00
" Derived formulas are not supported for formulas with string inputs, because we can't solve for them. Original formula: ${ originalFormula . toString ( ) } " ) ;
2026-03-09 17:32:39 +00:00
}
2026-03-10 15:47:03 +00:00
_init ( ) ;
}
2026-03-09 17:32:39 +00:00
2026-03-10 15:47:03 +00:00
void _init ( ) {
2026-03-09 17:32:39 +00:00
var newInput = List < VariableSpec > . from ( originalFormula . input ) . where ( ( v ) = > v . name ! = outputName ) . toList ( ) ;
newInput . add ( originalFormula . output ) ;
_input = newInput . toList ( growable: false ) ;
_output = originalFormula . input . firstWhere (
( v ) = > v . name = = outputName ,
orElse: ( ) = > throw ArgumentError ( " New output variable $ outputName not found in original formula input " ) ,
) ;
}
}
2026-03-04 18:52:31 +00:00
2026-03-09 17:32:39 +00:00
class Formula extends FormulaElement implements FormulaInterface {
@ override
2026-03-04 18:52:31 +00:00
final String uuid ;
2026-03-09 17:32:39 +00:00
@ override
2025-08-21 15:15:00 +00:00
final String name ;
2026-03-09 17:32:39 +00:00
@ override
2025-09-15 19:58:11 +00:00
final String ? description ;
2026-03-09 17:32:39 +00:00
@ override
2025-08-24 09:52:34 +00:00
final List < VariableSpec > input ;
2026-03-09 17:32:39 +00:00
@ override
2025-08-24 09:52:34 +00:00
final VariableSpec output ;
2026-03-09 17:32:39 +00:00
@ override
2025-08-21 15:15:00 +00:00
final String d4rtCode ;
2026-03-09 17:32:39 +00:00
@ override
2025-09-16 16:22:29 +00:00
final List < String > tags ;
2025-08-21 15:15:00 +00:00
2026-03-09 17:32:39 +00:00
@ override
Formula get originalFormula = > this ;
2026-03-04 18:09:48 +00:00
@ override
Map < String , dynamic > toMap ( ) {
return {
2026-03-09 09:40:02 +00:00
' uuid ' : uuid ,
2026-03-04 18:09:48 +00:00
' 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 ) ,
} ;
}
2025-08-22 15:47:06 +00:00
Formula ( {
2026-03-09 09:40:02 +00:00
String ? uuid ,
2025-08-21 15:15:00 +00:00
required this . name ,
2025-09-15 19:58:11 +00:00
this . description ,
2025-08-21 15:15:00 +00:00
required this . input ,
required this . output ,
required this . d4rtCode ,
2025-09-16 16:22:29 +00:00
this . tags = const [ ] ,
2026-03-04 18:52:31 +00:00
} ) : uuid = uuid ? ? _generateUuidV4 ( ) {
2025-08-22 15:47:06 +00:00
validate ( ) ;
}
2026-01-21 07:49:56 +00:00
void validate ( ) {
2026-03-09 17:32:39 +00:00
if ( name . trim ( ) . isEmpty ) {
2025-08-22 15:47:06 +00:00
throw ArgumentError ( ' Formula name cannot be empty ' ) ;
}
}
2025-08-21 15:15:00 +00:00
@ override
String toString ( ) = >
2026-03-04 18:52:31 +00:00
' Formula(uuid: $ uuid , name: $ name , description: $ description , input: $ input , output: $ output , d4rtCode: $ d4rtCode , tags: $ tags ) ' ;
2025-08-21 15:15:00 +00:00
@ override
bool operator = = ( Object other ) = >
2026-03-09 17:32:39 +00:00
identical ( this , other ) | | other is Formula & & runtimeType = = other . runtimeType & & uuid = = other . uuid ;
2025-08-21 15:15:00 +00:00
@ override
2026-03-04 18:52:31 +00:00
int get hashCode = > uuid . hashCode ;
2025-08-21 15:15:00 +00:00
2026-03-09 17:32:39 +00:00
List < String > inputVarNames ( ) = > input . map ( ( v ) = > v . name ) . toList ( growable: false ) ;
2025-08-24 10:33:21 +00:00
2025-09-07 11:59:03 +00:00
factory Formula . fromStringLiteral ( String setStringLiteral ) {
2025-08-26 14:54:35 +00:00
var d4rt = D4rt ( ) ;
final buffer = StringBuffer ( ) ;
2025-09-07 11:59:03 +00:00
buffer . write ( " main(){ return $ setStringLiteral ; } " ) ;
2025-08-26 14:54:35 +00:00
final code = buffer . toString ( ) ;
final Map < Object ? , Object ? > setLiteral = d4rt . execute ( source : code ) ;
return Formula . fromSet ( setLiteral ) ;
}
2025-09-07 11:59:03 +00:00
static List < Formula > fromArrayStringLiteral ( String arrayStringLiteral ) {
2026-03-01 12:51:14 +00:00
final List < Object ? > list = SetUtils . parseD4rtLiteral ( arrayStringLiteral ) ;
2025-08-26 15:17:42 +00:00
2025-09-07 11:59:03 +00:00
final formulas = list . map ( ( set ) = > Formula . fromSet ( set as Map ) ) ;
2025-08-26 15:17:42 +00:00
2025-09-05 16:53:06 +00:00
return formulas . toList ( growable: false ) ;
2025-08-26 15:17:42 +00:00
}
2025-08-26 14:37:28 +00:00
factory Formula . fromSet ( Map < Object ? , Object ? > theSet ) {
VariableSpec parseVar ( Map < Object ? , Object ? > varSpec ) {
2025-09-07 11:59:03 +00:00
String name = SetUtils . stringValue ( varSpec , " name " ) ;
2025-11-09 19:29:58 +00:00
String ? unit ;
if ( varSpec . containsKey ( " unit " ) ) {
unit = SetUtils . stringValue ( varSpec , " unit " ) ;
}
final allowed = varSpec [ ' values ' ] as List < dynamic > ? ;
if ( allowed ! = null ) {
final types = allowed . map ( ( v ) = > v . runtimeType ) . toSet ( ) ;
if ( types . length > 1 ) {
2026-03-09 17:32:39 +00:00
throw ArgumentError ( ' Allowed values must be all Strings or all Numbers ' ) ;
2025-11-09 19:29:58 +00:00
}
2026-03-09 17:32:39 +00:00
if ( ! types . contains ( String ) & & ! types . contains ( double ) & & ! types . contains ( int ) ) {
2025-11-09 19:29:58 +00:00
throw ArgumentError ( ' Allowed values must be Strings or Numbers ' ) ;
}
}
2026-03-09 17:32:39 +00:00
return VariableSpec ( name: name , unit: unit , values: allowed ? . toList ( growable: false ) ) ;
2025-08-26 14:37:28 +00:00
}
2026-03-04 18:52:31 +00:00
String ? uuid = theSet [ ' uuid ' ] as String ? ;
2025-09-07 11:59:03 +00:00
String name = SetUtils . stringValue ( theSet , " name " ) ;
2026-03-01 12:51:14 +00:00
String ? description = theSet [ " description " ] as String ? ;
2026-03-09 17:32:39 +00:00
List < String > tags = ( theSet [ " tags " ] as List < Object ? > ? ? ? [ ] ) . map ( ( t ) = > t . toString ( ) ) . toList ( ) ;
2025-09-07 11:59:03 +00:00
final List < Object ? > inputSet = SetUtils . listValue ( theSet , " input " ) ;
2026-03-09 17:32:39 +00:00
List < VariableSpec > input = inputSet . map ( ( v ) = > parseVar ( v as Map ) ) . toList ( growable: false ) ;
2026-03-01 12:51:14 +00:00
Map < Object ? , Object ? > outputSet = theSet [ ' output ' ] as Map < Object ? , Object ? > ;
2025-08-26 14:37:28 +00:00
VariableSpec output = parseVar ( outputSet ) ;
2025-09-07 11:59:03 +00:00
String d4rtCode = SetUtils . stringValue ( theSet , " d4rtCode " ) ;
2025-08-26 14:37:28 +00:00
2025-09-05 16:53:06 +00:00
return Formula (
2026-03-04 18:52:31 +00:00
uuid: uuid ,
2025-08-26 14:37:28 +00:00
name: name ,
2025-09-15 19:58:11 +00:00
description: description ,
2025-10-05 14:53:46 +00:00
tags: tags ,
2025-08-26 14:37:28 +00:00
input: input ,
output: output ,
d4rtCode: d4rtCode ,
) ;
2025-08-24 10:33:21 +00:00
}
2025-08-21 15:15:00 +00:00
}