├── .gitignore ├── LICENSE ├── README.md ├── ast.js ├── bin └── solvent.js ├── calculator.js ├── compute.js ├── dist └── solvent.js ├── index.html ├── index.js ├── math.js ├── math.ne ├── package.json ├── parse.js └── test └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Chris Andrejewski 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # solvent 2 | A calculator with equations and variables 3 | -------------------------------------------------------------------------------- /ast.js: -------------------------------------------------------------------------------- 1 | 2 | function Node(type) { 3 | return function _Node(nodes, attrs) { 4 | if(!(Array.isArray(nodes) && nodes.length)) { 5 | var msg = 'Node "'+type+'" needs an array of at least one subnode.'; 6 | throw new Error(msg); 7 | } 8 | return { 9 | type: type, 10 | nodes: nodes, 11 | attrs: attrs, 12 | }; 13 | } 14 | } 15 | 16 | function capitalize(str) { 17 | return str.charAt(0).toUpperCase() + str.slice(1); 18 | } 19 | 20 | var types = [ 21 | 'assignment', 22 | 'exponentiation', 23 | 'multiplication', 24 | 'division', 25 | 'addition', 26 | 'subtraction', 27 | 'negation', 28 | 'modulo', 29 | 'function', 30 | 'variable', 31 | 'number', 32 | ]; 33 | 34 | var ast = types.reduce(function(obj, type) { 35 | obj[capitalize(type)] = Node(type); 36 | return obj; 37 | }, {}); 38 | 39 | module.exports = ast; 40 | 41 | -------------------------------------------------------------------------------- /bin/solvent.js: -------------------------------------------------------------------------------- 1 | 2 | var nopt = require('nopt'); 3 | var path = require('path'); 4 | var Stream = require('stream').Stream; 5 | var readline = require('readline'); 6 | 7 | var solvent = require('../'); 8 | 9 | var options = { 10 | parse: String, 11 | compute: String, 12 | evaluate: String, 13 | solve: String, 14 | }; 15 | 16 | var shortOptions = { 17 | p: 'parse', 18 | c: 'compute', 19 | e: 'evaluate', 20 | s: 'solve', 21 | }; 22 | 23 | var data = nopt(options, shortOptions); 24 | 25 | if(process.argv.length === 2) { 26 | // repl 27 | var r = readline.createInterface(process.stdin, process.stdout); 28 | r.setPrompt('> '); 29 | r.prompt(); 30 | 31 | var running = ''; 32 | 33 | function input(statement) { 34 | try { 35 | var exp = solvent.parse(statement); 36 | // console.log(exp); 37 | var val = solvent.compute(exp); 38 | console.log(val); 39 | } catch(error) { 40 | throw error; 41 | } 42 | } 43 | 44 | r.on('line', function(line) { 45 | if(line.slice(-1) === '\\') { 46 | running += line.slice(0, -1); 47 | } else { 48 | running += line; 49 | input(running); 50 | running = ''; 51 | } 52 | r.prompt(); 53 | }).on('close', function() { 54 | console.log(''); 55 | process.exit(0); 56 | }); 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /calculator.js: -------------------------------------------------------------------------------- 1 | 2 | var $ = require('jquery'); 3 | var solvent = require('./'); 4 | 5 | //-- 6 | function $dom(selector, props, content) { 7 | var tagName = selector.split('#')[0].split('.')[0]; 8 | if(selector.indexOf('#') !== -1) { 9 | props.id = selector.split('#')[1].split('.')[0]; 10 | } 11 | 12 | var attrs = selector.slice(tagName.length).split('#'); 13 | var classNames = attrs[0].split('.') 14 | .concat(attrs[1].split('.').slice(1)) 15 | .filter(function(x) {return x.trim().length;}); 16 | props.className = ((props.className||'')+' '+classNames).trim(); 17 | 18 | tagName = tagName || 'div'; 19 | var tagBody = Object.keys(props) 20 | .filter(function(key) {return props[key];}) 21 | .map(function(key) { 22 | var mKey = key === 'className' ? 'class' : key; 23 | return hyphenCase(camelCase(mKey, true))+'=\"'+props[key]+'\"'; 24 | }).join(' '); 25 | if(tagBody.length) tagBody = ' '+tagBody; 26 | 27 | if(!content) { 28 | return '<'+tagName+tagBody+'/>'; 29 | } else { 30 | if(Array.isArray(content)) content = content.join(''); 31 | return '<'+tagName+tagBody+'>'+content+''; 32 | } 33 | } 34 | 35 | function hyphenCase(words, split) { 36 | return split 37 | ? words.split('-') 38 | : words.join('-'); 39 | } 40 | 41 | function snakeCase(words, split) { 42 | return split 43 | ? words.split('_') 44 | : words 45 | .map(function(x) {return x.toLowerCase();}) 46 | .join('_'); 47 | } 48 | 49 | function camelCase(words, split) { 50 | return split 51 | ? words.replace(/(A-Z){1}/g, function(x) { 52 | return ' '+x.toLowerCase(); 53 | }).join(' ') 54 | : words.map(function(w, i) { 55 | if(i === 0) return w; 56 | return w.charAt(0).toUpperCase() + w.slice(1); 57 | }).join(''); 58 | } 59 | 60 | var d = $dom; 61 | //-- 62 | 63 | var context = solvent.Context([], Math); 64 | 65 | var $expressionList = $('#expression-list'); 66 | var $expressionCreate = $('#expression-create'); 67 | 68 | $expressionCreate.on('submit', function(e) { 69 | e.preventDefault(); 70 | var str = $(this).serialize().string; 71 | this.reset(); 72 | context.expressions.push(solvent.parse(str)); 73 | $expressionList.html(renderContext(context)); 74 | }); 75 | 76 | function renderContext(context) { 77 | return d('ul.expression-list', {}, context.expressions.map(function(exp, index) { 78 | return d('li.expression', {id: 'exp-'+index}, [ 79 | d('.equation', {}, renderEquation(exp, context)), 80 | d('.computed', {}, solvent.compute(solvent.evaluate(exp, context))), 81 | ]); 82 | })); 83 | } 84 | 85 | function renderEquation(eq, context) { 86 | function renderNode(node) { 87 | 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /compute.js: -------------------------------------------------------------------------------- 1 | 2 | var operations = { 3 | number: function _number(ns) {return ns;}, 4 | negation: function _negation(ns) {return -ns;}, 5 | 'function': function _function(ns, val, idx, list) { 6 | if(idx > 1) return ns; 7 | if(typeof ns !== 'function') { 8 | var msg = 'Function "'+ns+'" is not a function.'; 9 | throw new Error(msg); 10 | } 11 | return ns.apply(null, list.slice(1)); 12 | }, 13 | 14 | addition: function addition(x,y) {return x+y;}, 15 | subtraction: function subtraction(x,y) {return x-y;}, 16 | multiplication: function multiplication(x,y) {return x*y;}, 17 | division: function division(x,y) {return x/y;}, 18 | exponentiation: function exponentiation(x,y) {return Math.pow(x,y);}, 19 | 20 | modulo: function modulo(x,y) {return x%y}, 21 | assignment: function assignment(x, y) {return y;}, 22 | }; 23 | 24 | function compute(exp) { 25 | if(typeof exp === 'number') return exp; 26 | var fn; 27 | if (fn = operations[exp.type]) { 28 | if(exp.type === 'negation') exp.nodes.push(0); // exec hack 29 | return exp.nodes.map(compute).reduce(fn); 30 | } else { 31 | var msg = 'Node type "'+exp.type+'" not recognized for \n'+ 32 | JSON.stringify(exp); 33 | throw new Error(msg); 34 | } 35 | } 36 | 37 | module.exports = compute; 38 | 39 | -------------------------------------------------------------------------------- /dist/solvent.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { 390 | addedRules.push(r); 391 | new State(r, 0, location).epsilonClosure(location, ind, table); 392 | } else { 393 | // Empty rule 394 | // This is special 395 | var copy = me.consumeNonTerminal(r.name); 396 | if (r.postprocess) { 397 | copy.data[copy.data.length-1] = r.postprocess([], this.reference); 398 | } else { 399 | copy.data[copy.data.length-1] = []; 400 | } 401 | copy.epsilonClosure(location, ind, table); 402 | } 403 | } 404 | }); 405 | } 406 | }; 407 | 408 | State.prototype.isComplete = function() { 409 | return this.expect === this.rule.symbols.length; 410 | } 411 | 412 | /** 413 | * Computes all possible epsilon-steps from the current state at 414 | * given location. States 0 through ind-1 in location are considered 415 | * for possible nullables. 416 | */ 417 | State.prototype.epsilonClosure = function(location, ind, table, result) { 418 | var col = table[location]; 419 | if (!result) result = table[location]; // convenient common case 420 | 421 | result.push(this); 422 | 423 | if (!this.isComplete()) { 424 | for (var i = 0; i < ind; i++) { 425 | var state = col[i]; 426 | if (state.isComplete() && state.reference === location) { 427 | var x = this.consumeNonTerminal(state.rule.name); 428 | if (x) { 429 | x.data[x.data.length-1] = state.data; 430 | x.epsilonClosure(location, ind, table); 431 | } 432 | } 433 | } 434 | } 435 | } 436 | 437 | 438 | function Parser(rules, start) { 439 | var table = this.table = []; 440 | this.rules = rules.map(function (r) { return (new Rule(r.name, r.symbols, r.postprocess)); }); 441 | this.start = start = start || this.rules[0].name; 442 | // Setup a table 443 | var addedRules = []; 444 | this.table.push([]); 445 | // I could be expecting anything. 446 | this.rules.forEach(function (r) { 447 | if (r.name === start) { // add all rules named start 448 | addedRules.push(r); 449 | table[0].push(new State(r, 0, 0)); 450 | }}); // this should refer to this object, not each rule inside the forEach 451 | this.advanceTo(0, addedRules); 452 | this.current = 0; 453 | } 454 | 455 | // create a reserved token for indicating a parse fail 456 | Parser.fail = {}; 457 | 458 | Parser.prototype.advanceTo = function(n, addedRules) { 459 | // Advance a table, take the closure of .process for location n in the input stream 460 | var w = 0; 461 | while (w < this.table[n].length) { 462 | (this.table[n][w]).process(n, w, this.table, this.rules, addedRules); 463 | w++; 464 | } 465 | } 466 | 467 | Parser.prototype.feed = function(chunk) { 468 | for (var chunkPos = 0; chunkPos < chunk.length; chunkPos++) { 469 | // We add new states to table[current+1] 470 | this.table.push([]); 471 | 472 | // Advance all tokens that expect the symbol 473 | // So for each state in the previous row, 474 | 475 | for (var w = 0; w < this.table[this.current + chunkPos].length; w++) { 476 | var s = this.table[this.current + chunkPos][w]; 477 | var x = s.consumeTerminal(chunk[chunkPos]); // Try to consume the token 478 | if (x) { 479 | // And then add it 480 | this.table[this.current + chunkPos + 1].push(x); 481 | } 482 | } 483 | 484 | // Next, for each of the rules, we either 485 | // (a) complete it, and try to see if the reference row expected that 486 | // rule 487 | // (b) predict the next nonterminal it expects by adding that 488 | // nonterminal's start state 489 | // To prevent duplication, we also keep track of rules we have already 490 | // added 491 | 492 | var addedRules = []; 493 | this.advanceTo(this.current + chunkPos + 1, addedRules); 494 | 495 | // If needed, throw an error: 496 | if (this.table[this.table.length-1].length === 0) { 497 | // No states at all! This is not good. 498 | var err = new Error( 499 | "nearley: No possible parsings (@" + (this.current + chunkPos) 500 | + ": '" + chunk[chunkPos] + "')." 501 | ); 502 | err.offset = this.current + chunkPos; 503 | throw err; 504 | } 505 | } 506 | 507 | this.current += chunkPos; 508 | // Incrementally keep track of results 509 | this.results = this.finish(); 510 | 511 | // Allow chaining, for whatever it's worth 512 | return this; 513 | }; 514 | 515 | Parser.prototype.finish = function() { 516 | // Return the possible parsings 517 | var considerations = []; 518 | var myself = this; 519 | this.table[this.table.length-1].forEach(function (t) { 520 | if (t.rule.name === myself.start 521 | && t.expect === t.rule.symbols.length 522 | && t.reference === 0 523 | && t.data !== Parser.fail) { 524 | considerations.push(t); 525 | } 526 | }); 527 | return considerations.map(function(c) {return c.data; }); 528 | }; 529 | 530 | var nearley = { 531 | Parser: Parser, 532 | Rule: Rule 533 | }; 534 | 535 | if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { 536 | module.exports = nearley; 537 | } else { 538 | window.nearley = nearley; 539 | } 540 | })(); 541 | 542 | },{}],6:[function(require,module,exports){ 543 | 544 | var nearley = require('nearley'); 545 | var grammar = require('./math'); 546 | 547 | function parse(str) { 548 | var parser = new nearley.Parser(grammar.ParserRules, grammar.ParserStart); 549 | var output = parser.feed(str).results; 550 | var ast = clean(output); 551 | console.log(JSON.stringify(ast, null, 2)); 552 | return ast; 553 | } 554 | 555 | function clean(ast) { 556 | if(!(ast.length && ast[0].length)) return null; 557 | function _clean(ast) { 558 | while(Array.isArray(ast)) ast = ast[0]; 559 | if(typeof ast !== 'object') return ast; 560 | ast.nodes = ast.nodes.map(_clean); 561 | return ast; 562 | } 563 | return _clean(ast); 564 | } 565 | 566 | module.exports = parse; 567 | 568 | 569 | },{"./math":4,"nearley":5}]},{},[3]); 570 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Solvent 5 | 6 | 7 |
8 |
9 | 10 |
11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | var parse = require('./parse'); 3 | var compute = require('./compute'); 4 | 5 | function symbolicSolveFor(left, right) { 6 | var result = {success: false}; 7 | return result; 8 | } 9 | 10 | function findNode(tree, predicate) { 11 | var searchSet = [tree]; 12 | var node; 13 | while(searchSet.length) { 14 | node = searchSet.shift(); 15 | if(predicate(node)) return node; 16 | node.nodes.forEach(function(x) { 17 | if(typeof x === 'object') { 18 | searchSet.push(x); 19 | } 20 | }); 21 | } 22 | return undefined; 23 | } 24 | 25 | function findNodes(tree, predicate) { 26 | var searchSet = [tree]; 27 | var nodes = []; 28 | while(searchSet.length) { 29 | node = searchSet.shift(); 30 | if(predicate(node)) nodes.push(node); 31 | node.nodes.forEach(function(x) { 32 | if(typeof x === 'object') { 33 | searchSet.push(x); 34 | } 35 | }); 36 | } 37 | return nodes; 38 | } 39 | 40 | function solveFor(exp, variableName) { 41 | var equalities = findNode(exp, function(x) { 42 | return x.type === 'assignment'; 43 | }); 44 | if(!equalities.length) { 45 | var msg = "Equation not provided, unable to solve for \""+variableName+"\"." 46 | throw new Error(msg); 47 | } 48 | 49 | var totalVarCount = 0; 50 | var eqs = equalities.map(function(eq) { 51 | var side = {}; 52 | side.equality = eq; 53 | side.varNodes = findNodes(eq, function(x) { 54 | return x.type === 'variable' && x.nodes[0] === variableName; 55 | }); 56 | side.varCount = side.varNodes.length; 57 | totalVarCount += side.varCount; 58 | return side; 59 | }).sort(function(a, b) { 60 | return b.varCount - a.varCount; 61 | }); 62 | 63 | if(!totalVarCount) { 64 | var msg = "Variable \""+variableName+"\" not found in equation."; 65 | throw new Error(msg); 66 | } 67 | 68 | var leastVarIndex = 0; 69 | while(eqs[leastVarIndex].varCount === 0) leastVarIndex++; 70 | 71 | var impotentEqs = eqs.slice(0, leastVarIndex); 72 | var variableEqs = eqs.slice(leastVarIndex); 73 | 74 | var iLen = impotentEqs.length; 75 | var vLen = variableEqs.length; 76 | 77 | var attempts = 0; 78 | for(var v = 0; v < vLen; v++) { 79 | for(var i = 0; i < iLen; i++) { 80 | attempts++; 81 | variableEq = variableEqs[v]; 82 | impotentEq = impotentEqs[i]; 83 | var result = symbolicSolveFor(variableEq, impotentEqs); 84 | if(result.success) { 85 | return result.expression; 86 | } 87 | } 88 | } 89 | 90 | var msg = "After \""+attempts+"\" tries, the variable \""+variableName+"\" was not solved for."; 91 | throw new Error(msg); 92 | } 93 | 94 | function Context(expressions, constants) { 95 | return { 96 | expressions: expressions, 97 | constants: constants, 98 | }; 99 | } 100 | 101 | function evaluate(expression, context) { 102 | context = context || Context([], Math); 103 | // populate variables and functions 104 | return expression; 105 | } 106 | 107 | module.exports = { 108 | parse: parse, 109 | evaluate: evaluate, 110 | Context: Context, 111 | compute: compute, 112 | solveFor: solveFor, 113 | }; 114 | 115 | -------------------------------------------------------------------------------- /math.js: -------------------------------------------------------------------------------- 1 | // Generated automatically by nearley 2 | // http://github.com/Hardmath123/nearley 3 | (function () { 4 | function id(x) {return x[0]; } 5 | var ast = typeof window !== 'undefined' ? window.ast : require('./ast.js'); var grammar = { 6 | ParserRules: [ 7 | {"name": "main", "symbols": ["_", "EXP", "_"], "postprocess": function(d) { return d[1]; }}, 8 | {"name": "_", "symbols": [], "postprocess": function(d) {return null;}}, 9 | {"name": "_", "symbols": ["_", /[\s]/], "postprocess": function(d) {return null;}}, 10 | {"name": "CHAR$ebnf$1", "symbols": [/[a-zA-Z]/]}, 11 | {"name": "CHAR$ebnf$1", "symbols": [/[a-zA-Z]/, "CHAR$ebnf$1"], "postprocess": function arrconcat(d) {return [d[0]].concat(d[1]);}}, 12 | {"name": "CHAR", "symbols": ["CHAR$ebnf$1"], "postprocess": function(d) {return d[0].join(""); }}, 13 | {"name": "STRING", "symbols": ["CHAR"], "postprocess": function(d) {return d[0];}}, 14 | {"name": "VAR", "symbols": ["STRING"], "postprocess": function(d) {return ast.Variable([d[0]]); }}, 15 | {"name": "INT$ebnf$1", "symbols": [/[0-9]/]}, 16 | {"name": "INT$ebnf$1", "symbols": [/[0-9]/, "INT$ebnf$1"], "postprocess": function arrconcat(d) {return [d[0]].concat(d[1]);}}, 17 | {"name": "INT", "symbols": ["INT$ebnf$1"], "postprocess": function(d) {return d[0].join("");}}, 18 | {"name": "DECIMAL", "symbols": ["INT", {"literal":"."}, "INT"], "postprocess": function(d) {return parseFloat(d[0] + d[1] + d[2]);}}, 19 | {"name": "DECIMAL", "symbols": ["INT"], "postprocess": function(d) {return parseInt(d[0]);}}, 20 | {"name": "NUM", "symbols": ["DECIMAL"], "postprocess": function(d) {return ast.Number([d[0]]);}}, 21 | {"name": "VAL", "symbols": ["VAR"]}, 22 | {"name": "VAL", "symbols": ["NUM"]}, 23 | {"name": "FUNC", "symbols": ["STRING"]}, 24 | {"name": "ADD", "symbols": [{"literal":"+"}]}, 25 | {"name": "SUB", "symbols": [{"literal":"-"}]}, 26 | {"name": "MUL", "symbols": [{"literal":"*"}]}, 27 | {"name": "DIV", "symbols": [{"literal":"/"}]}, 28 | {"name": "MOD", "symbols": [{"literal":"%"}]}, 29 | {"name": "POW", "symbols": [{"literal":"^"}]}, 30 | {"name": "SET", "symbols": [{"literal":"="}]}, 31 | {"name": "EXP", "symbols": ["ASSIGNMENT"]}, 32 | {"name": "ASSIGNMENT", "symbols": ["ASSIGNMENT", "_", "SET", "_", "ADD_SUB"], "postprocess": function(d) {return ast.Assignment([d[0], d[4]]);}}, 33 | {"name": "ASSIGNMENT", "symbols": ["ADD_SUB"], "postprocess": id}, 34 | {"name": "ADD_SUB", "symbols": ["ADD_SUB", "_", "ADD", "_", "MUL_DIV"], "postprocess": function(d) {return ast.Addition([d[0], d[4]]);}}, 35 | {"name": "ADD_SUB", "symbols": ["ADD_SUB", "_", "SUB", "_", "MUL_DIV"], "postprocess": function(d) {return ast.Subtraction([d[0], d[4]]);}}, 36 | {"name": "ADD_SUB", "symbols": ["MUL_DIV"], "postprocess": id}, 37 | {"name": "MUL_DIV", "symbols": ["MUL_DIV", "_", "MUL", "_", "EXPONENTIATION"], "postprocess": function(d) {return ast.Multiplication([d[0], d[4]]);}}, 38 | {"name": "MUL_DIV", "symbols": ["NUM", "_", {"literal":"("}, "_", "EXP", "_", {"literal":")"}], "postprocess": function(d) {return ast.Multiplication([d[0], d[4]]);}}, 39 | {"name": "MUL_DIV", "symbols": ["MUL_DIV", "_", "DIV", "_", "EXPONENTIATION"], "postprocess": function(d) {return ast.Division([d[0], d[4]]);}}, 40 | {"name": "MUL_DIV", "symbols": ["EXPONENTIATION"], "postprocess": id}, 41 | {"name": "EXPONENTIATION", "symbols": ["EXPONENTIATION", "_", "POW", "_", "PARENTHESIS"], "postprocess": function(d) {return ast.Exponentiation([d[0], d[4]]);}}, 42 | {"name": "EXPONENTIATION", "symbols": ["PARENTHESIS"], "postprocess": id}, 43 | {"name": "NEGATION", "symbols": ["SUB", "_", "EXP"], "postprocess": function(d) {return ast.Negation([d[2]]);}}, 44 | {"name": "MODULO", "symbols": ["EXP", "_", "MOD", "_", "EXP"], "postprocess": function(d) {return ast.Modulo([d[0], d[4]]);}}, 45 | {"name": "PARENTHESIS", "symbols": [{"literal":"("}, "_", "ATOMIC", "_", {"literal":")"}], "postprocess": function(d) {return d[2];}}, 46 | {"name": "PARENTHESIS", "symbols": ["ATOMIC"], "postprocess": id}, 47 | {"name": "ATOMIC", "symbols": ["MODULO"]}, 48 | {"name": "ATOMIC", "symbols": ["NEGATION"]}, 49 | {"name": "ATOMIC", "symbols": ["FUNCTION"]}, 50 | {"name": "ATOMIC", "symbols": ["VAL"]}, 51 | {"name": "FUNCTION", "symbols": ["FUNC", "_", {"literal":"("}, "_", "FUNCARGS", "_", {"literal":")"}], "postprocess": function(d) {return ast.Function([d[0]].concat(d[4]));}}, 52 | {"name": "ARGUMENT", "symbols": ["_", "EXP", "_"], "postprocess": function(d) {return d[1];}}, 53 | {"name": "FUNCARGS", "symbols": ["ARGUMENT"], "postprocess": function(d) {return [d[0]];}}, 54 | {"name": "FUNCARGS", "symbols": ["FUNCARGS", "_", {"literal":","}, "_", "ARGUMENT"], "postprocess": function(d) {return d[0].concat(d[4]);}} 55 | ] 56 | , ParserStart: "main" 57 | } 58 | if (typeof module !== 'undefined'&& typeof module.exports !== 'undefined') { 59 | module.exports = grammar; 60 | } else { 61 | window.grammar = grammar; 62 | } 63 | })(); 64 | -------------------------------------------------------------------------------- /math.ne: -------------------------------------------------------------------------------- 1 | 2 | @{% var ast = typeof window !== 'undefined' ? window.ast : require('./ast.js'); %} 3 | 4 | main -> _ EXP _ {% function(d) { return d[1]; } %} 5 | 6 | _ -> null {% function(d) {return null;} %} 7 | | _ [\s] {% function(d) {return null;} %} 8 | 9 | CHAR -> [a-zA-Z]:+ {% function(d) {return d[0].join(""); } %} 10 | STRING -> CHAR {% function(d) {return d[0];} %} 11 | VAR -> STRING {% function(d) {return ast.Variable([d[0]]); } %} 12 | 13 | INT -> [0-9]:+ {% function(d) {return d[0].join("");} %} 14 | DECIMAL -> INT "." INT {% function(d) {return parseFloat(d[0] + d[1] + d[2]);} %} 15 | | INT {% function(d) {return parseInt(d[0]);} %} 16 | NUM -> DECIMAL {% function(d) {return ast.Number([d[0]]);} %} 17 | 18 | VAL -> VAR | NUM 19 | FUNC -> STRING 20 | 21 | ADD -> "+" 22 | SUB -> "-" 23 | MUL -> "*" 24 | DIV -> "/" 25 | MOD -> "%" 26 | POW -> "^" 27 | SET -> "=" 28 | 29 | EXP -> ASSIGNMENT 30 | 31 | ASSIGNMENT -> ASSIGNMENT _ SET _ ADD_SUB {% function(d) {return ast.Assignment([d[0], d[4]]);} %} 32 | | ADD_SUB {% id %} 33 | 34 | # ADDITION -> EXP _ ADD _ EXP {% function(d) {return ast.Addition([d[0], d[4]]);} %} 35 | # SUBTRACTION -> EXP _ SUB _ EXP {% function(d) {return ast.Subtraction([d[0], d[4]]);} %} 36 | 37 | ADD_SUB -> ADD_SUB _ ADD _ MUL_DIV {% function(d) {return ast.Addition([d[0], d[4]]);} %} 38 | | ADD_SUB _ SUB _ MUL_DIV {% function(d) {return ast.Subtraction([d[0], d[4]]);} %} 39 | | MUL_DIV {% id %} 40 | 41 | # MULTIPLICATION -> EXP _ MUL _ EXP {% function(d) {return ast.Multiplication([d[0], d[4]]);} %} 42 | # MULTIPLICATION_IMPLICIT -> NUM _ "(" _ EXP _ ")" {% function(d) {return ast.Multiplication([d[0], d[4]]);} %} 43 | # DIVISION -> EXP _ DIV _ EXP {% function(d) {return ast.Division([d[0], d[4]]);} %} 44 | 45 | MUL_DIV -> MUL_DIV _ MUL _ EXPONENTIATION {% function(d) {return ast.Multiplication([d[0], d[4]]);} %} 46 | | NUM _ "(" _ EXP _ ")" {% function(d) {return ast.Multiplication([d[0], d[4]]);} %} 47 | | MUL_DIV _ DIV _ EXPONENTIATION {% function(d) {return ast.Division([d[0], d[4]]);} %} 48 | | EXPONENTIATION {% id %} 49 | 50 | EXPONENTIATION -> EXPONENTIATION _ POW _ PARENTHESIS {% function(d) {return ast.Exponentiation([d[0], d[4]]);} %} 51 | | PARENTHESIS {% id %} 52 | 53 | NEGATION -> SUB _ EXP {% function(d) {return ast.Negation([d[2]]);} %} 54 | MODULO -> EXP _ MOD _ EXP {% function(d) {return ast.Modulo([d[0], d[4]]);} %} 55 | 56 | PARENTHESIS -> "(" _ ATOMIC _ ")" {% function(d) {return d[2];} %} 57 | | ATOMIC {% id %} 58 | 59 | ATOMIC -> MODULO | NEGATION | FUNCTION | VAL 60 | 61 | FUNCTION -> FUNC _ "(" _ FUNCARGS _ ")" {% function(d) {return ast.Function([d[0]].concat(d[4]));} %} 62 | ARGUMENT -> _ EXP _ {% function(d) {return d[1];} %} 63 | FUNCARGS -> ARGUMENT {% function(d) {return [d[0]];} %} 64 | | FUNCARGS _ "," _ ARGUMENT {% function(d) {return d[0].concat(d[4]);} %} 65 | 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "solvent", 3 | "version": "0.0.1", 4 | "description": "A calculator with equations and variables", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "build": "nearleyc math.ne > math.js && browserify index.js -o dist/solvent.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/andrejewski/solvent.git" 13 | }, 14 | "keywords": [ 15 | "calculator", 16 | "variables", 17 | "equations" 18 | ], 19 | "author": "Chris Andrejewski (chrisandrejewski.com)", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/andrejewski/solvent/issues" 23 | }, 24 | "homepage": "https://github.com/andrejewski/solvent#readme", 25 | "devDependencies": { 26 | "browserify": "^13.0.0", 27 | "mocha": "^2.4.5" 28 | }, 29 | "dependencies": { 30 | "nearley": "^2.4.0", 31 | "nopt": "^3.0.6" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /parse.js: -------------------------------------------------------------------------------- 1 | 2 | var nearley = require('nearley'); 3 | var grammar = require('./math'); 4 | 5 | function parse(str) { 6 | var parser = new nearley.Parser(grammar.ParserRules, grammar.ParserStart); 7 | var output = parser.feed(str).results; 8 | var ast = clean(output); 9 | // console.log(JSON.stringify(ast, null, 2)); 10 | return ast; 11 | } 12 | 13 | function clean(ast) { 14 | if(!(ast.length && ast[0].length)) return null; 15 | function _clean(ast) { 16 | while(Array.isArray(ast)) ast = ast[0]; 17 | if(typeof ast !== 'object') return ast; 18 | ast.nodes = ast.nodes.map(_clean); 19 | return ast; 20 | } 21 | return _clean(ast); 22 | } 23 | 24 | module.exports = parse; 25 | 26 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrejewski/solvent/73835256dd01fafc8821b806f00d58459e8c7bf5/test/index.js --------------------------------------------------------------------------------