1 module libdmathexpr.mathexpr; 2 /** 3 * LibDMathExpr contains functions and classes for parsing and writing math 4 * equations. 5 * 6 * Copyright: (c) 2009 William K. Moore, III (nyphbl8d (at) gmail (dot) com, opticron on freenode) 7 * Authors: William K. Moore, III 8 * License: Boost Software License - Version 1.0 - August 17th, 2003 9 10 Permission is hereby granted, free of charge, to any person or organization 11 obtaining a copy of the software and accompanying documentation covered by 12 this license (the "Software") to use, reproduce, display, distribute, 13 execute, and transmit the Software, and to prepare derivative works of the 14 Software, and to permit third-parties to whom the Software is furnished to 15 do so, all subject to the following: 16 17 The copyright notices in the Software and this entire statement, including 18 the above license grant, this restriction and the following disclaimer, 19 must be included in all copies of the Software, in whole or in part, and 20 all derivative works of the Software, unless such copies or derivative 21 works are solely in the form of machine-executable object code generated by 22 a source language processor. 23 24 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 25 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 27 SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 28 FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 29 ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 30 DEALINGS IN THE SOFTWARE. 31 32 */ 33 import std.conv: to; 34 import std.math; 35 import std.mathspecial: gamma; 36 import std.regex; 37 import std.string: cmp, icmp, join, replace, split, strip, stripLeft, toLower; 38 39 40 /// An exception thrown on math parsing errors. 41 class MathParseError : Exception { 42 // Throws an exception with an error message. 43 this(string msg) @safe { 44 super(msg); 45 } 46 } 47 48 interface IMathObject { 49 void parse(ref string input) @safe; 50 string toString() @safe; 51 real evaluate(real[string]vars) @safe; 52 IMathObject simplify(real[string]vars) @safe; 53 } 54 55 const string tryEval = 56 " try { 57 return new MONumber(evaluate(vars)); 58 } catch(Exception) {}"; 59 60 enum MOOpType { 61 Addition, 62 Multiplication, 63 Exponentiation, 64 Negation, 65 Modulus, 66 Inversion 67 } 68 string toString(MOOpType op) @safe { 69 switch(op) { 70 case MOOpType.Addition: return " + "; 71 case MOOpType.Multiplication: return " * "; 72 case MOOpType.Negation: return " -"; 73 case MOOpType.Inversion: return "1/"; 74 case MOOpType.Modulus: return "%"; 75 default: throw new MathParseError("Unknown operation!"); 76 } 77 } 78 79 int getPrevElem(IMathObject[]tokens,int i) @safe { 80 i--; 81 for (;i>=0;i--) { 82 if (tokens[i]) return i; 83 } 84 return -1; 85 } 86 87 int getNextElem(IMathObject[]tokens,int i) @safe { 88 i++; 89 for (;i<tokens.length;i++) { 90 if (tokens[i]) return i; 91 } 92 return -1; 93 } 94 95 IMathObject parseMathExpr(string input) @safe { 96 // split the string into IMathObject tokens 97 IMathObject[]tokens = splitMathExpr(input); 98 // go through the process of turning this thing into a tree instead of a flat list 99 // parens are already expanded 100 // exponentials 101 for (int i = 0;i < tokens.length;i++) { 102 auto caster = cast(MOOperation)tokens[i]; 103 if (caster && caster.op == MOOpType.Exponentiation) { 104 // must not be at the beginning or end of the string 105 if (i == 0 || i == tokens.length-1) { 106 throw new MathParseError("Exponentiation must have 2 operands"); 107 } 108 // subitems must be parens or numbers 109 int prev = getPrevElem(tokens,i),next = getNextElem(tokens,i); 110 if (prev == -1 || (!cast(MOParens)tokens[prev] && !cast(MONumber)tokens[prev] && !cast(MOIdentifier)tokens[prev])) { 111 // the previous entity can be an operation, but only if it's also an exponential 112 throw new MathParseError("Exponentiation operators must be parens or numbers"); 113 } 114 if (next == -1 || (!cast(MOParens)tokens[next] && !cast(MONumber)tokens[next] && !cast(MOIdentifier)tokens[next] && !cast(MONegation)tokens[next])) { 115 throw new MathParseError("Exponentiation operators must be parens or numbers"); 116 } 117 // resolve negation here for more fun 118 if (cast(MONegation)tokens[next]) { 119 // find next non-negation node 120 int noneg = next+1; 121 while (noneg < tokens.length) { 122 if (tokens[noneg] && !cast(MONegation)tokens[noneg]) { 123 break; 124 } 125 noneg++; 126 } 127 if (!tokens[noneg] || cast(MONegation)tokens[noneg]) { 128 throw new MathParseError("Negation slide as exponent blew up"); 129 } 130 // fold negation slide into exponential 131 int lastgood = noneg; 132 noneg--; 133 while (noneg >= next) { 134 if (tokens[noneg]) { 135 (cast(MONegation)tokens[noneg]).rhs = tokens[lastgood]; 136 tokens[lastgood] = null; 137 lastgood = noneg; 138 } 139 noneg--; 140 } 141 } 142 // pull a quick one and convert to the function call 143 tokens[i] = new MOIdentifier("pow",[cast(IMathObject)tokens[prev],tokens[next]]); 144 // move the adjacent objects into the tree 145 tokens[prev] = null; 146 tokens[next] = null; 147 } 148 } 149 // evaluate inversion 150 for (int i = cast(int)tokens.length-1;i >= 0;i--) { 151 auto caster = cast(MOInversion)tokens[i]; 152 if (caster) { 153 // must not be at the end of the string 154 if (i == tokens.length-1) { 155 throw new MathParseError("Inversion must have an operand"); 156 } 157 // subitems must be parens or numbers 158 int next = getNextElem(tokens,i); 159 if (next == -1) { 160 throw new MathParseError("Inversion must have an operand"); 161 } 162 // if this param is an operation, both fields need to be filled 163 auto ntmp = cast(MOOperation)tokens[next]; 164 if (ntmp) { 165 throw new MathParseError("Double operators? really? (inversion)"); 166 } 167 // move the adjacent objects into the tree 168 caster.rhs = tokens[next]; 169 tokens[next] = null; 170 } 171 } 172 // evaluate negation back to front to accomodate slides 173 for (int i = cast(int)tokens.length-1;i >= 0;i--) { 174 auto caster = cast(MONegation)tokens[i]; 175 if (caster) { 176 // must not be at the end of the string 177 if (i == tokens.length-1) { 178 throw new MathParseError("Negation must have an operand"); 179 } 180 // subitems must be parens or numbers 181 int next = getNextElem(tokens,i); 182 if (next == -1) { 183 throw new MathParseError("Negation must have an operand"); 184 } 185 // if this param is an operation, both fields need to be filled 186 auto ntmp = cast(MOOperation)tokens[next]; 187 if (ntmp && !ntmp.validate()) { 188 throw new MathParseError("Double operators? really? (negation)"); 189 } 190 // move the adjacent objects into the tree 191 caster.rhs = tokens[next]; 192 tokens[next] = null; 193 } 194 } 195 // helper function 196 void parseHelper(bool delegate(MOOpType) @safe valid) @safe { 197 for (int i = 0;i < tokens.length;i++) { 198 auto caster = cast(MOOperation)tokens[i]; 199 if (caster && valid(caster.op)) { 200 // must not be at the beginning or end of the string 201 if (i == 0 || i == tokens.length-1) { 202 throw new MathParseError("Operators must have 2 operands"); 203 } 204 // subitems must be parens or numbers 205 int prev = getPrevElem(tokens,i),next = getNextElem(tokens,i); 206 if (prev == -1) { 207 throw new MathParseError("Operators must have 2 operands"); 208 } 209 // if this param is an operation, both fields need to be filled 210 auto ptmp = cast(MOOperation)tokens[prev]; 211 if (ptmp && !ptmp.validate()) { 212 throw new MathParseError("Double operators? really?"); 213 } 214 if (next == -1) { 215 throw new MathParseError("Operators must have 2 operands"); 216 } 217 // if this param is an operation, both fields need to be filled 218 auto ntmp = cast(MOOperation)tokens[next]; 219 if (ntmp && !ntmp.validate()) { 220 throw new MathParseError("Double operators? really?"); 221 } 222 // move the adjacent objects into the tree 223 caster.operands ~= tokens[prev]; 224 tokens[prev] = null; 225 caster.operands ~= tokens[next]; 226 tokens[next] = null; 227 } 228 } 229 } 230 // multiplication 231 parseHelper((MOOpType o){return o == MOOpType.Multiplication || o == MOOpType.Modulus;}); 232 // addition 233 parseHelper((MOOpType o){return o == MOOpType.Addition;}); 234 // now that we have the tree fully formed simplify the tree 235 // flatten tree 236 foreach (token;tokens) if (token) { 237 auto tmp = cast(MOOperation)token; 238 if (tmp) { 239 tmp.flatten(); 240 } 241 } 242 // verify that we're left with a single item in the list 243 long count = 0; 244 uint first; 245 foreach(uint i,c;tokens) if (c) {count++;first=i;} 246 if (count != 1) { 247 throw new MathParseError("FUUUUUUUUUUU count="~count.to!string); 248 } 249 return tokens[first]; 250 } 251 252 enum { 253 NUMBER, 254 OPERATOR, 255 PAREN, 256 SPACE, 257 IDENTIFIER, 258 } 259 260 int getCharType(char i) @safe { 261 if (i >= '0' && i <= '9') return NUMBER; 262 if (i >= 'a' && i <= 'z' || i >= 'A' && i <= 'Z') return IDENTIFIER; 263 switch (i) { 264 case '+': 265 case '-': 266 case '*': 267 case '/': 268 case '^': 269 case '%': 270 return OPERATOR; 271 case ' ': 272 case '\n': 273 case '\r': 274 return SPACE; 275 case '(': 276 return PAREN; 277 case '.': 278 return NUMBER; 279 default: 280 throw new MathParseError("Invalid character: "~i); 281 } 282 } 283 284 IMathObject[] splitMathExpr(string input) @safe { 285 // respect parens and pass them back into parseMathExpr for further parsing 286 IMathObject[]tmp; 287 void doAutoMul() @safe { 288 if (tmp.length && (cast(MONumber)tmp[$-1] || cast(MOParens)tmp[$-1] || cast(MOIdentifier)tmp[$-1])) { 289 tmp ~= new MOOperation("*"); 290 } 291 } 292 int slice = 0; 293 while (input.length) { 294 switch (getCharType(input[0])) { 295 case NUMBER: 296 // if the previous item was a number, barf, can't have adjacent numerics because it doesn't make sense 297 if (tmp.length && cast(MONumber)tmp[$-1]) throw new MathParseError("Numerics can not be adjacent. Secondary starting at: "~input); 298 // add mul op if necessary 299 doAutoMul(); 300 tmp ~= new MONumber(input); 301 break; 302 case OPERATOR: 303 // take care of division via mul+inversion 304 if (input[0] == '/') { 305 tmp ~= new MOOperation("*"); 306 tmp ~= new MOInversion(input); 307 input = input[1..$]; 308 continue; 309 } else if (input[0] == '-') { 310 if (tmp.length && !cast(MOOperation)tmp[$-1] && !cast(MONegation)tmp[$-1]) { 311 // tack on a + 312 tmp ~= new MOOperation("+"); 313 } 314 tmp ~= new MONegation(input); 315 input = input[1..$]; 316 continue; 317 } 318 tmp ~= new MOOperation(input[0..1]); 319 input = input[1..$]; 320 break; 321 case PAREN: 322 // add mul op if necessary 323 doAutoMul(); 324 tmp ~= new MOParens(input); 325 break; 326 case SPACE: 327 input = input[1..$]; 328 break; 329 default:// assume identifier 330 case IDENTIFIER: 331 // add mul op if necessary 332 doAutoMul(); 333 tmp ~= new MOIdentifier(input); 334 break; 335 } 336 } 337 return tmp; 338 } 339 340 class MONegation:IMathObject { 341 IMathObject rhs; 342 this (ref string c) @safe { 343 parse(c); 344 } 345 this() @safe {} 346 /// Return the string that was used to generate this node. 347 override string toString() @safe { 348 return "-"~rhs.toString(); 349 } 350 /// Return the numeric data represented by this node. 351 real evaluate(real[string]vars) @safe { 352 return -rhs.evaluate(vars); 353 } 354 /// This function parses a set of nodes out of an equation string. 355 void parse(ref string source) @safe { 356 // this does nothing on purpose, it will not be used by default 357 } 358 /// Return the simplification of this node (where possible). 359 IMathObject simplify(real[string]vars) @safe { 360 mixin(tryEval); 361 // pass the simplification up the tree first 362 rhs = rhs.simplify(vars); 363 // see if we can wrap two negations into one 364 auto caster = cast(MONegation)rhs; 365 if (caster) return caster.rhs; 366 return this; 367 } 368 } 369 370 class MOInversion:IMathObject { 371 IMathObject rhs; 372 this (ref string c) @safe { 373 parse(c); 374 } 375 /// Return the string that was used to generate this node. 376 override string toString() @safe { 377 return "1/"~rhs.toString(); 378 } 379 /// Return the numeric data represented by this node. 380 real evaluate(real[string]vars) @safe { 381 return 1/rhs.evaluate(vars); 382 } 383 /// This function parses a set of nodes out of an equation string. 384 void parse(ref string source) @safe { 385 // this does nothing on purpose, it will not be used by default 386 } 387 /// Return the simplification of this node (where possible). 388 IMathObject simplify(real[string]vars) @safe { 389 mixin(tryEval); 390 // it only makes sense to pass the simplification up the tree here since negation is so simple 391 rhs = rhs.simplify(vars); 392 return this; 393 } 394 } 395 396 397 // holds the builtin functions and the number of args 398 int[string]builtins; 399 static this () { 400 builtins = ["cos"[]:1, "tan":1, "sin":1, "cosh":1, "tanh":1, "sinh":1, "acos":1, "atan":1, "asin":1, "acosh":1, "atanh":1, "asinh":1, "pow":2, "log":1, "ln":1, "int":1, "frac":1, "abs":1, "sqrt":1, "exp":1, "gamma":1, "mod":2]; 401 } 402 class MOIdentifier:IMathObject { 403 string identifier; 404 IMathObject[]args; 405 this (ref string c) @safe { 406 parse(c); 407 } 408 this (string id,IMathObject[]inargs) @safe { 409 identifier = id; 410 args = inargs; 411 } 412 /// Return the string that was used to generate this node. 413 override string toString() @safe { 414 auto ret = identifier; 415 if (args.length) { 416 ret ~= "("; 417 foreach(arg;args) ret ~= arg.toString(); 418 ret ~= ")"; 419 } 420 return ret; 421 } 422 bool isBuiltin(string fun) @safe { 423 return (fun in builtins) != null; 424 } 425 /// Return the numeric data represented by this node. 426 real evaluate(real[string]vars) @safe { 427 // ensure we have the right number of args if it's a function 428 if (isBuiltin(identifier) && args.length != builtins[identifier]) { 429 throw new MathParseError("Wrong number of arguments for "~identifier); 430 } 431 switch (identifier) { 432 case "cos": 433 return cos(args[0].evaluate(vars)); 434 case "tan": 435 return tan(args[0].evaluate(vars)); 436 case "sin": 437 return sin(args[0].evaluate(vars)); 438 case "cosh": 439 return cosh(args[0].evaluate(vars)); 440 case "tanh": 441 return tanh(args[0].evaluate(vars)); 442 case "sinh": 443 return sinh(args[0].evaluate(vars)); 444 case "acos": 445 return acos(args[0].evaluate(vars)); 446 case "atan": 447 return atan(args[0].evaluate(vars)); 448 case "asin": 449 return asin(args[0].evaluate(vars)); 450 case "acosh": 451 return acosh(args[0].evaluate(vars)); 452 case "atanh": 453 return atanh(args[0].evaluate(vars)); 454 case "asinh": 455 return asinh(args[0].evaluate(vars)); 456 case "pow": 457 return pow(args[0].evaluate(vars),args[1].evaluate(vars)); 458 case "log": 459 return log10(args[0].evaluate(vars)); 460 case "ln": 461 return log(args[0].evaluate(vars)); 462 case "abs": 463 return abs(args[0].evaluate(vars)); 464 case "sqrt": 465 return sqrt(args[0].evaluate(vars)); 466 case "exp": 467 return exp(args[0].evaluate(vars)); 468 case "gamma": 469 return gamma(args[0].evaluate(vars)); 470 case "int": 471 auto tmp = args[0].evaluate(vars); 472 return tmp-(tmp%1); 473 case "frac": 474 return args[0].evaluate(vars)%1; 475 case "mod": 476 return args[0].evaluate(vars)%args[1].evaluate(vars); 477 case "e": return E; 478 case "pi": return PI; 479 default: 480 // check variables 481 if (identifier in vars) { 482 auto ret = vars[identifier]; 483 if (args.length == 1) { 484 ret *= args[0].evaluate(vars); 485 } 486 return ret; 487 } 488 throw new MathParseError("Unknown identifier: "~identifier); 489 } 490 } 491 /// This function parses a set of nodes out of an equation string. 492 void parse(ref string source) @safe { 493 string getIden(ref string src) { 494 foreach(i,c;src) if (!(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z')) { 495 auto tmp = src[0..i]; 496 src = src[i..$]; 497 return tmp; 498 } 499 auto tmp = src; 500 src = ""; 501 return tmp; 502 } 503 // if the identifier is our variable, don't look for parens or args 504 identifier = toLower(getIden(source)); 505 // strip out whitespace 506 source = stripLeft(source); 507 // parse out parameters if necessary 508 if (!source.length || source[0] != '(') { 509 if (isBuiltin(identifier)) { 510 throw new MathParseError("Couldn't parse args for "~identifier); 511 } 512 // this is a variable 513 return; 514 } 515 int count = 0; 516 uint laststart = 1; 517 foreach (uint i,c;source) { 518 switch (c) { 519 case '(': count++; break; 520 case ')': count--; break; 521 default: break; 522 case ',': 523 // parse a new arg if we're at the first level 524 if (count == 1) { 525 args ~= parseMathExpr(source[laststart..i]); 526 laststart = i+1; 527 } 528 break; 529 } 530 if (!count) { 531 args ~= parseMathExpr(source[laststart..i]); 532 source = source[i+1..$]; 533 if (!isBuiltin(identifier) && args.length > 1) { 534 throw new MathParseError("Unrecognized function: "~identifier); 535 } 536 return; 537 } 538 } 539 throw new MathParseError("Unexpected end of input: "~source); 540 } 541 /// Return the simplification of this node (where possible). 542 IMathObject simplify(real[string]vars) @safe { 543 mixin(tryEval); 544 if (isBuiltin(identifier)) { 545 // a function! 546 // attempt evaluation of subnodes 547 foreach (ref arg;args) { 548 arg = arg.simplify(vars); 549 } 550 return this; 551 } else { 552 // a variable? 553 if (args.length) { 554 // try to simplify the arg/mul 555 args[0] = args[0].simplify(vars); 556 } 557 if (identifier in vars) { 558 // since we can resolve the identifier, do so 559 auto num = new MONumber(vars[identifier]); 560 if (!args.length) return num; 561 // convert into a multiplication 562 auto op = new MOOperation(MOOpType.Multiplication, [cast(IMathObject)num, new MOParens(args[0])]); 563 // this needs parens to ensure proper display when required 564 // and proper reparsing upon output 565 return op.simplify(vars); 566 } 567 return this; 568 } 569 } 570 } 571 572 class MOOperation:IMathObject { 573 IMathObject[]operands; 574 MOOpType op; 575 /// Constructor that takes pre-parsed objects and an op 576 this (MOOpType inop, IMathObject[]ops) @safe { 577 op = inop; 578 operands = ops; 579 } 580 /// Empty constructor 581 this () @safe {} 582 /// Constructor taking an operation 583 this (string c) @safe { 584 parse(c); 585 } 586 private bool validate() @safe { 587 switch(op) { 588 case MOOpType.Addition: 589 if (operands.length < 2) throw new MathParseError("Addition requires 2 or more operands"); 590 break; 591 case MOOpType.Multiplication: 592 if (operands.length < 2) throw new MathParseError("Multiplication requires 2 or more operands"); 593 break; 594 case MOOpType.Modulus: 595 if (operands.length != 2) throw new MathParseError("Modulus requires 2 operands"); 596 break; 597 default: throw new MathParseError("Unknown operation!"); 598 } 599 return true; 600 } 601 /// Return the string that was used to generate this node. 602 override string toString() @safe { 603 string[]ret; 604 validate(); 605 foreach (oper;operands) { 606 ret ~= oper.toString(); 607 } 608 return ret.join(.toString(op)); 609 } 610 real doAdd(real accum,real add) @safe { 611 return accum+add; 612 } 613 real doMul(real accum,real mul) @safe { 614 return accum*mul; 615 } 616 /// Return the numeric data represented by this node. 617 real evaluate(real[string]vars) @safe { 618 real delegate(real n1,real n2) @safe doOp; 619 validate(); 620 real accum; 621 switch(op) { 622 case MOOpType.Addition: 623 doOp = &doAdd; 624 accum = 0; 625 break; 626 case MOOpType.Multiplication: 627 doOp = &doMul; 628 accum = 1; 629 break; 630 case MOOpType.Modulus: 631 return operands[0].evaluate(vars)%operands[1].evaluate(vars); 632 default: throw new MathParseError("Unknown operation!"); 633 } 634 foreach (operand;operands) { 635 accum = doOp(accum,operand.evaluate(vars)); 636 } 637 return accum; 638 } 639 /// This function parses a set of nodes out of an equation string. 640 void parse(ref string source) { 641 switch (source[0]) { 642 case '+': op = MOOpType.Addition; break; 643 case '*': op = MOOpType.Multiplication; break; 644 case '^': op = MOOpType.Exponentiation; break; 645 case '%': op = MOOpType.Modulus; break; 646 default: throw new MathParseError("Operation not recognized: "~source[0]); 647 } 648 } 649 /// Return the simplification of this node (where possible). 650 IMathObject simplify(real[string]vars) { 651 mixin(tryEval); 652 // simplify operands 653 foreach (ref myop;operands) { 654 myop = myop.simplify(vars); 655 } 656 // combine numerics 657 real delegate(real n1,real n2) @safe doOp; 658 validate(); 659 real accum,orig; 660 switch(op) { 661 case MOOpType.Addition: 662 doOp = &doAdd; 663 orig = accum = 0; 664 break; 665 case MOOpType.Multiplication: 666 doOp = &doMul; 667 orig = accum = 1; 668 break; 669 case MOOpType.Modulus: 670 // no further simplification for modulus 671 return this; 672 default: throw new MathParseError("Unknown operation!"); 673 } 674 for (int i = 0;i < operands.length;i++) { 675 auto tmp = cast(MONumber)operands[i]; 676 if (tmp) { 677 accum = doOp(accum, tmp.evaluate(vars)); 678 operands[i] = null; 679 } 680 } 681 // fill in the nulls 682 for (int i = 0;i < operands.length;i++) { 683 if (!operands[i]) { 684 operands[i] = operands[$-1]; 685 operands.length = operands.length - 1; 686 i--; 687 } 688 } 689 // additional optimizations for multiplication 690 if (op == MOOpType.Multiplication) { 691 // if mul by 0 692 if (accum == 0) return new MONumber(0); 693 // if mul by -1 694 if (accum == -1) { 695 auto neg = new MONegation(); 696 auto par = new MOParens(this); 697 // wrap this in a MONegation and return it 698 if (operands.length == 1) par.holder = operands[0]; 699 neg.rhs = par; 700 return neg; 701 } 702 } 703 // identity operation simplification for both addition and multiplication 704 if (orig != accum) { 705 operands ~= new MONumber(accum); 706 } 707 // if we've optimized out all but one parameter, this operation is irrelevant 708 if (operands.length == 1) return operands[0]; 709 return this; 710 } 711 void flatten() @safe { 712 // flatten everything we can 713 for (int i = 0;i < operands.length;i++) { 714 auto tmp = cast(MOOperation)operands[i]; 715 if (tmp) { 716 if (tmp.op == op) { 717 operands ~= tmp.operands; 718 operands[i] = null; 719 } else { 720 tmp.flatten(); 721 } 722 } 723 } 724 // fill in the nulls 725 for (int i = 0;i < operands.length;i++) { 726 if (!operands[i]) { 727 operands[i] = operands[$-1]; 728 operands.length = operands.length - 1; 729 i--; 730 } 731 } 732 } 733 } 734 735 class MOParens:IMathObject { 736 IMathObject holder; 737 /// Constructor that takes a pre-parsed object 738 this(IMathObject obj) @safe { 739 holder = obj; 740 } 741 /// Constructor that eats an input string 742 this(ref string input) @safe { 743 parse(input); 744 } 745 /// Return the string that was used to generate this node. 746 override string toString() @safe { 747 return "("~holder.toString()~")"; 748 } 749 /// Return the numeric data represented by this node. 750 real evaluate(real[string]vars) @safe { 751 return holder.evaluate(vars); 752 } 753 /// This function parses a set of nodes out of an equation string. 754 void parse(ref string input) @safe { 755 // find the end of the paren that opens here 756 if (!input.length || input[0] != '(') { 757 throw new MathParseError("Couldn't instantiate parens"); 758 } 759 int count = 0; 760 foreach (i,c;input) { 761 switch (c) { 762 case '(': count++; break; 763 case ')': count--; break; 764 default: break; 765 } 766 if (!count) { 767 holder = parseMathExpr(input[1..i]); 768 // skip the closing paren 769 input = input[i+1..$]; 770 return; 771 } 772 } 773 throw new MathParseError("Unexpected end of input: "~input); 774 } 775 /// Return the simplification of this node (where possible). 776 IMathObject simplify(real[string]vars) @safe { 777 mixin(tryEval); 778 // it only makes sense to pass the simplification up the tree here, parens perform no real ops 779 holder = holder.simplify(vars); 780 return this; 781 } 782 } 783 784 class MONumber:IMathObject { 785 real _data; 786 /// Empty constructor 787 this () @safe {} 788 /// Real constructor 789 this (real indata) @safe {_data = indata;} 790 /// Constructor that eats an input string 791 this (ref string input) @safe { 792 parse(input); 793 } 794 /// Return the string that was used to generate this node. 795 override string toString() @safe { 796 return _data.to!string; 797 } 798 /// Return the numeric data represented by this node. 799 real evaluate(real[string]vars) @safe { 800 return _data; 801 } 802 /// This function parses a number out of a string and eats characters as it goes, hence the ref string parameter. 803 void parse(ref string source) @safe { 804 // this parser sucks... 805 int i = 0; 806 // sift through whole numerics 807 if (i < source.length && source[i] <= '9' && source[i] >= '0') { 808 while (i < source.length && source[i] >= '0' && source[i] <= '9') i++; 809 } else if (i < source.length && source[i] != '.') throw new MathParseError("A numeric parse error occurred while parsing the numeric beginning at: "~source); 810 // if the next char is not a '.', we know we're done with fractional parts 811 if (i < source.length) { 812 if (source[i] == '.') { 813 i++; 814 while (i < source.length && source[i] >= '0' && source[i] <= '9') i++; 815 } 816 // if the next char is e or E, we're poking at an exponential 817 if (i < source.length && (source[i] == 'e' || source[i] == 'E')) { 818 i++; 819 if (source[i] == '-' || source[i] == '+') i++; 820 while (i< source.length && source[i] >= '0' && source[i] <= '9') i++; 821 } 822 } 823 _data = source[0..i].to!real; 824 source = stripLeft(source[i..$]); 825 } 826 /// Return the simplification of this node (where possible). 827 IMathObject simplify(real[string]vars) @safe { 828 // this is already as simpified as it gets 829 return this; 830 } 831 } 832 833 version(MATHEXPR_main) { 834 void main() {} 835 } 836 837 @safe unittest { 838 import std.stdio : writeln; 839 real[string]vars; 840 void tryGood(string expr,real expected) @safe { 841 writeln("MathExpr: "~expr~" == "~expected.to!string); 842 assert(approxEqual(parseMathExpr(expr).evaluate(vars), expected)); 843 } 844 void tryBad(string expr) @safe { 845 writeln("MathExpr: "~expr~" (intentional bad expression)"); 846 try { 847 parseMathExpr(expr).evaluate(vars); 848 assert(0); 849 } catch(Exception) {} 850 } 851 void setVar(string var,real val) @safe { 852 vars[var] = val; 853 writeln("Set "~var~" to "~val.to!string); 854 } 855 setVar("x",3); 856 tryGood("x+x",6); 857 tryGood("2x",6); 858 tryGood("x+4",7); 859 tryGood("12/4",3); 860 tryGood("x^x",27); 861 tryGood("-1",-1); 862 tryGood("-x",-3); 863 tryGood("2*-x",-6); 864 tryGood("-x*2",-6); 865 tryGood("-x^2",-9); 866 tryGood("2^-x",0.125); 867 tryGood("2^----x",8); 868 tryGood("2x",6); 869 tryGood("cos(x)",cos(3.0)); 870 tryGood("5cos(x)",5*cos(3.0)); 871 tryGood("3x+2",11); 872 tryGood("(46+36+52+41+27+56+30+24+30)/9",38); 873 tryGood("1(-0)",0); 874 tryGood("0/1",0); 875 writeln("MathExpr: cos(3x+2)"); 876 real t1 = parseMathExpr("cos(3x+2)").evaluate(vars),t2 = cos(11.0); 877 writeln("got "~t1.to!string~" == "~t2.to!string); 878 //assert(feqrel(t1,t2)); 879 setVar("y",32); 880 tryGood("x+y",vars["x"]+vars["y"]); 881 setVar("toaster",5); 882 tryGood("x(4)toaster",vars["x"]*4*vars["toaster"]); 883 tryGood("4*3/3",4); 884 tryGood("x3x",27); 885 tryGood("04 / 02",2); 886 tryGood("int(4.5)",4); 887 tryGood("frac(4.5)",0.5); 888 tryGood("mod(4.5,1)",0.5); 889 tryGood("4.5*1",4.5); 890 tryGood("4.5%1",0.5); 891 tryGood("5*4.5%3*3+1",5.5); 892 tryBad("blah(4)"); 893 tryBad("3++3");// should probably evaluate good... 894 tryBad("3//3"); 895 tryBad("3**3"); 896 tryBad("3^^3"); 897 tryBad("3+-*3"); 898 tryBad("3*/3"); 899 tryBad("3/*3"); 900 tryBad("3-/3"); 901 tryBad("1..4"); 902 writeln("MathExpr: Simplify 3+4"); 903 assert(parseMathExpr("3+4").simplify(vars).toString() == "7"); 904 writeln("MathExpr: Simplify x+z"); 905 assert(parseMathExpr("x+z").simplify(vars).toString() == "z + 3"); 906 writeln("MathExpr: Simplify 7(x+z)"); 907 assert(parseMathExpr("7(x+z)").simplify(vars).toString() == "(z + 3) * 7"); 908 writeln("MathExpr: Simplify 7(x+z-2y)"); 909 assert(parseMathExpr("7(x+z-2y)").simplify(vars).toString() == "(z + -61) * 7"); 910 writeln("Completed all unit tests"); 911 }