1 /// 2 module arith_eval.evaluable; 3 4 import arith_eval.exceptions; 5 import arith_eval.internal.eval; 6 7 import std.ascii; 8 import std.conv : to; 9 import std.exception; 10 import std.experimental.checkedint : Checked, Throw; 11 import std.format; 12 import std.meta : allSatisfy, aliasSeqOf; 13 import std.string; 14 import std.traits : isNumeric, isIntegral, isFloatingPoint; 15 16 /// 17 public immutable struct Evaluable(Vars...) 18 if (allSatisfy!(isValidVariableName, Vars)) 19 { 20 /// Expression this instance is set to evaluate 21 string expr; 22 23 /** 24 Basic constructor of the type. 25 26 Params: expr = expression this instance evaluates 27 28 Throws: InvalidExpressionException if the expression cannot 29 evaluated. 30 */ 31 this(string expr) 32 in 33 { 34 assert(expr !is null); 35 } 36 do 37 { 38 //TODO: perform runtime checking of variables in the expression 39 40 enforce!InvalidExpressionException(isExpressionValid(expr), 41 format("Expression \"%s\" cannot be evaluated.", expr)); 42 43 this.expr = expr; 44 } 45 46 /** 47 Evaluates the expression. 48 49 Returns: The value after evaluating the expression (at 50 the point specified by evalPoint, if the number 51 of variables is greater than 0). 52 53 Throws: EvaluationException if an error, such as overflow, 54 has occurred during the evaluation. 55 */ 56 public EvalType eval(EvalType = float)(EvalType[Vars.length] evalPoint...) const 57 if (isFloatingPoint!EvalType) 58 { 59 import std.range : iota; 60 string replacedExpr = expr; 61 62 foreach(i; aliasSeqOf!(iota(0, Vars.length))) 63 { 64 import std.array : replace; 65 replacedExpr = replacedExpr.replace(Vars[i], to!string(evalPoint[i])); 66 } 67 68 try 69 { 70 import std.math : approxEqual; 71 72 immutable EvalType evaluation = evalExpr!EvalType(replacedExpr); 73 74 if (evaluation.approxEqual(EvalType.max) || 75 evaluation.approxEqual(-EvalType.max) || 76 evaluation.approxEqual(EvalType.infinity) || 77 evaluation.approxEqual(-EvalType.infinity)) 78 throw new Exception("Evaluation reached maximum possible value and is not reliable."); 79 return evaluation; 80 } 81 catch(Exception e) 82 { 83 static if (Vars.length == 0) 84 immutable string msg = format("Error evaluating expression \"%s\" for type %s.", 85 expr, EvalType.stringof); 86 else 87 immutable string msg = format("Error evaluating expression \"%s\" for type %s " ~ 88 "on point '%s'.", expr, EvalType.stringof, to!string(evalPoint)); 89 throw new EvaluationException(msg, e); 90 } 91 } 92 } 93 94 // TESTS 95 version(unittest) 96 { 97 import unit_threaded; 98 99 @("Evaluable.__ctor does not throw for supported expressions") 100 unittest 101 { 102 Evaluable!()("12.34").shouldNotThrow(); 103 Evaluable!()("-12.34").shouldNotThrow(); 104 Evaluable!()("12.34e10").shouldNotThrow(); 105 Evaluable!()("12.34e+10").shouldNotThrow(); 106 Evaluable!()("12.34e-10").shouldNotThrow(); 107 Evaluable!("foo")("foo").shouldNotThrow(); 108 Evaluable!()("1 + 2").shouldNotThrow(); 109 Evaluable!()("1 - 2").shouldNotThrow(); 110 Evaluable!()("1 * 2").shouldNotThrow(); 111 Evaluable!()("1 / 2").shouldNotThrow(); 112 Evaluable!()("1 ^ 2").shouldNotThrow(); 113 } 114 115 @("Evaluable.__ctor throws for unsupported expressions") 116 unittest 117 { 118 Evaluable!()("2**2").shouldThrow!InvalidExpressionException(); 119 Evaluable!("foo")("2foo").shouldThrow!InvalidExpressionException(); 120 Evaluable!("foo", "bar")("foo bar").shouldThrow!InvalidExpressionException(); 121 } 122 123 @("Evaluable.eval() returns value of the expression according to arguments") 124 unittest 125 { 126 auto noVariables = Evaluable!()("2 + 3"); 127 noVariables.eval().shouldEqual(5); 128 129 auto oneVar = Evaluable!"x"("2*x"); 130 oneVar.eval(1f).shouldEqual(2); 131 oneVar.eval(3f).shouldEqual(6); 132 133 auto twoVars = Evaluable!("x", "y")("x + y"); 134 twoVars.eval(1f, 2f).shouldEqual(3); 135 twoVars.eval(5f, 5f).shouldEqual(10); 136 } 137 138 @("Evaluable.eval() throws EvaluationException if evaluation reaches maximum value") 139 unittest 140 { 141 auto simpleFunction = Evaluable!"x"("x"); 142 simpleFunction.eval(float.max).shouldThrow!EvaluationException(); 143 144 auto add1 = Evaluable!"x"("x + 1"); 145 add1.eval(float.max).shouldThrow!EvaluationException(); 146 147 auto oneOverZero = Evaluable!()("1 / 0"); 148 oneOverZero.eval().shouldThrow!EvaluationException(); 149 } 150 } 151 152 private enum isAlphaNumOrUnderscoreString(string s) 153 { 154 foreach(char c; s) 155 if(!c.isAlphaNum && c != '_') 156 return false; 157 158 return true; 159 } 160 161 private enum isValidVariableName(string T) = 162 T[0].isAlpha && 163 isAlphaNumOrUnderscoreString(T); 164 165 version(unittest) 166 { 167 import unit_threaded; 168 169 @("Variable names can only be programming variable identifiers") 170 unittest 171 { 172 isValidVariableName!"x".shouldBeTrue(); 173 isValidVariableName!"HeLlO_w0rLd".shouldBeTrue(); 174 isValidVariableName!"9unicorns".shouldBeFalse(); 175 isValidVariableName!"hello world".shouldBeFalse(); 176 } 177 } 178