What is the purpose of adding a variable to an expression? Let's see:
An expression containing X cannot be directly evaluated. You need to assign values to X first. For example, if (y = x * X) is constantly assigned values and values, you can draw a one-to-one column (X, y) to draw a smooth parabolic curve.
Therefore, an xnode node must have an attribute that can be assigned a value: xvalue, which should be static. The following is the definition of xnode (super simple, isn't it ?) :
public class XNode : NodeBase { public static double XValue { get; set; } public XNode(int index, string data, string expression) : base(index, data, expression) { } public override double GetValue() { return XValue; } }
As long as the nodes of variables and constants are parsed at one time, the expressions will not appear again in the future. Therefore, you can inherit iexpressionadjustor for advance processing, after processing, xfinder can be removed from finders and divided to improve performance.
The following is the definition of xfinder (now you finally feel the benefits of refactoring! Hey ):
Public class xfinder: finderbase, iexpressionadjustor {public override int priority {get {return (INT) finderpriority. xfinder ;}} protected override string rule {get {return @ "X" ;}} protected override inode generatenode (string sourceexpression, string data, int index) {If (sourceexpression. length> match. index + match. value. length) {var tailchar = sourceexpression [match. index + match. value. length]; // if the next character is ':' or [A-Za-Z], it is a character in a function and is not suitable for variable processing, otherwise, it is treated as the variable if (tailchar = '(' | (tailchar> = 'A' & tailchar <= 'Z ') | (tailchar >='a' & tailchar <= 'Z') return NULL;} return New xnode (index, Data, sourceexpression );} // refactor the original expression and generate the public void adjustexpression (ref string expression, Ref List <ifinder> finders) {While (true) of each function's finder instance) {inode node = find (expression); If (node = NULL) break; addnode (calculator. foundnodes, node); expression = expression. replaceonce (node. value, node. ID, node. index);} finders = finders. except T (new list <ifinder> {This }). tolist (); // The responsibilities of the current class have been completed, remove it }}
Because the expressions containing variables cannot be directly evaluated, the calculateexpression method in calculator is not enough. We need to provide another method: getvalue (Double X) to assign values to variables first and then take values.
The following is the updated calculator class. We can see that we also provide a getvalues () method for ease of use:
Public class calculator {private list <inode> _ foundnodes; public list <inode> foundnodes {get {return _ foundnodes ;}} public inode rootnode {get {return foundnodes. last () ;}} public double calculateexpression (string expression) {_ foundnodes = new list <inode> (); finderbase. findallnodes (this, ref expression); If (foundnodes! = NULL & foundnodes. count> = 1) return rootnode. getvalue (); return double. nan;} public double getvalue (Double X) {xnode. xvalue = x; return rootnode. getvalue ();} // For example, you can return points :( X1, Y1), (X2, Y2 )... public list <tuple <double, double> getvalues (double xfrom, double xto, int steps) {double onestep = (xto-xfrom)/steps; vaR rlt = new list <tuple <double, double> (); For (INT I = 0; I <steps; I ++) {xnode. xvalue = xfrom + onestep * I; rootnode. getvalue (); RLT. add (New tuple <double, double> (xnode. xvalue, rootnode. getvalue ();} return rlt;} internal inode getnode (string ID) {return foundnodes. firstordefault (n => N. id = ID );}}
You can note that before calling the getvalue () method for the first time, you must first call calculateexpression () for node resolution. Therefore, you do not need to parse it because you can directly use the resolved nodes.
How to demonstrate the beauty of X? For the graphic demonstration, the plane Cartesian coordinate system is described below:
The difference between the plane Cartesian coordinate system and the computer's screen Coordinate System in mathematics is needless to say. Here are three points:
- The Y axis is in the opposite direction.
- The origin location (the origin of the screen or UI controls such as canvas) is at the top left vertex, and the mathematical coordinate system is usually at the center)
- Unit (the screen coordinate system is usually in pixels, and the mathematical coordinate system is usually in units (for example, 1 cm as a unit ))
Well, with these differences, it is bound to involve mathematical transformation, such as converting the location on the screen (physicalpoint) to the location on the mathematical logic (logicalpoint ).. Net already has the point type to indicate a location. We can use it directly. However, to avoid confusion, we should first define physicalpoint and logicalpoint respectively, in the future, the name may not be right:
public class LogicalPoint:PointBase { public LogicalPoint (double x,double y) : base(x,y){} public override PhysicalPoint ToPhysical(CoordinateSystem cs) { return cs.ToPhysical(this); } } public class PhysicalPoint : PointBase { public PhysicalPoint(double x, double y) : base(x,y){} public override LogicalPoint ToLogical(CoordinateSystem cs) { return cs.ToLogical(this); } }
The conversion will inevitably use coordinatesystem, which is the mathematical coordinate system defined by us. Because we may use multiple coordinate systems, and the length of each unit may be different. The following is the definition of coordinatesystem. We inherit canvas directly, because the coordinate system usually needs to draw axes and scales. Of course, you can also use the aggregate method to use canvas as an attribute of coordinatesystem.
Public class coordinatesystem: canvas {public coordinatesystem (double width, double height) {This. width = width; this. height = height; origin = new physicalpoint (width/2, height/2);} private physicalpoint origin; private const double unitlength = 50; // The Screen pixel corresponding to each unit length # region coordinate transforms public logicalpoint tological (physicalpoint p) {return New logicalpoint (P. x-origin. x)/unitlength,-(P. y-origin. y)/unitlength);} public physicalpoint tophysical (logicalpoint p) {return New physicalpoint (origin. X + P. x * unitlength, origin. y-P. y * unitlength) ;}# endregion}
Well, as for drawing lines and points on the canvas, I don't need to talk about it anymore. The special features of Silverlight, huh, huh. If you are not clear, you can directly view the code!
Below is the run, isn't it cool?
Well, the mystery of X is so simple, but why can't we see any of the legendary curves? You may have guessed that the implementation curve is still difficult! If we directly connect a vertex to a vertex using a line segment, it is okay for expressions like sin (x). What if Sin (1/x? Or is y = 1/X simpler? Tragedy! This involves the issue of finding a valid range. We have not yet come up with a completely feasible solution. I hope you will not be able to give me some advice if you know it. Thank you!
[Source code and demo address]
In the next section, we will make a summary of what we have done. Specifically, we will add an animated demonstration to better illustrate the process of expression parsing and value evaluation, so as to achieve the purpose of teaching!