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