123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432 |
- #!/usr/bin/env node
- /**
- * math.js
- * https://github.com/josdejong/mathjs
- *
- * Math.js is an extensive math library for JavaScript and Node.js,
- * It features real and complex numbers, units, matrices, a large set of
- * mathematical functions, and a flexible expression parser.
- *
- * Usage:
- *
- * mathjs [scriptfile(s)] {OPTIONS}
- *
- * Options:
- *
- * --version, -v Show application version
- * --help, -h Show this message
- * --tex Generate LaTeX instead of evaluating
- * --string Generate string instead of evaluating
- * --parenthesis= Set the parenthesis option to
- * either of "keep", "auto" and "all"
- *
- * Example usage:
- * mathjs Open a command prompt
- * mathjs 1+2 Evaluate expression
- * mathjs script.txt Run a script file
- * mathjs script1.txt script2.txt Run two script files
- * mathjs script.txt > results.txt Run a script file, output to file
- * cat script.txt | mathjs Run input stream
- * cat script.txt | mathjs > results.txt Run input stream, output to file
- *
- * @license
- * Copyright (C) 2013-2023 Jos de Jong <wjosdejong@gmail.com>
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy
- * of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
- const fs = require('fs')
- const path = require('path')
- const { createEmptyMap } = require('../lib/cjs/utils/map.js')
- let scope = createEmptyMap()
- const PRECISION = 14 // decimals
- /**
- * "Lazy" load math.js: only require when we actually start using it.
- * This ensures the cli application looks like it loads instantly.
- * When requesting help or version number, math.js isn't even loaded.
- * @return {*}
- */
- function getMath () {
- return require('../lib/cjs/defaultInstance.js').default
- }
- /**
- * Helper function to format a value. Regular numbers will be rounded
- * to 14 digits to prevent round-off errors from showing up.
- * @param {*} value
- */
- function format (value) {
- const math = getMath()
- return math.format(value, {
- fn: function (value) {
- if (typeof value === 'number') {
- // round numbers
- return math.format(value, PRECISION)
- } else {
- return math.format(value)
- }
- }
- })
- }
- /**
- * auto complete a text
- * @param {String} text
- * @return {[Array, String]} completions
- */
- function completer (text) {
- const math = getMath()
- let matches = []
- let keyword
- const m = /[a-zA-Z_0-9]+$/.exec(text)
- if (m) {
- keyword = m[0]
- // scope variables
- for (const def in scope.keys()) {
- if (def.indexOf(keyword) === 0) {
- matches.push(def)
- }
- }
- // commandline keywords
- ['exit', 'quit', 'clear'].forEach(function (cmd) {
- if (cmd.indexOf(keyword) === 0) {
- matches.push(cmd)
- }
- })
- // math functions and constants
- const ignore = ['expr', 'type']
- for (const func in math.expression.mathWithTransform) {
- if (hasOwnProperty(math.expression.mathWithTransform, func)) {
- if (func.indexOf(keyword) === 0 && ignore.indexOf(func) === -1) {
- matches.push(func)
- }
- }
- }
- // units
- const Unit = math.Unit
- for (const name in Unit.UNITS) {
- if (hasOwnProperty(Unit.UNITS, name)) {
- if (name.indexOf(keyword) === 0) {
- matches.push(name)
- }
- }
- }
- for (const name in Unit.PREFIXES) {
- if (hasOwnProperty(Unit.PREFIXES, name)) {
- const prefixes = Unit.PREFIXES[name]
- for (const prefix in prefixes) {
- if (hasOwnProperty(prefixes, prefix)) {
- if (prefix.indexOf(keyword) === 0) {
- matches.push(prefix)
- } else if (keyword.indexOf(prefix) === 0) {
- const unitKeyword = keyword.substring(prefix.length)
- for (const n in Unit.UNITS) {
- if (hasOwnProperty(Unit.UNITS, n)) {
- if (n.indexOf(unitKeyword) === 0 &&
- Unit.isValuelessUnit(prefix + n)) {
- matches.push(prefix + n)
- }
- }
- }
- }
- }
- }
- }
- }
- // remove duplicates
- matches = matches.filter(function (elem, pos, arr) {
- return arr.indexOf(elem) === pos
- })
- }
- return [matches, keyword]
- }
- /**
- * Run stream, read and evaluate input and stream that to output.
- * Text lines read from the input are evaluated, and the results are send to
- * the output.
- * @param input Input stream
- * @param output Output stream
- * @param mode Output mode
- * @param parenthesis Parenthesis option
- */
- function runStream (input, output, mode, parenthesis) {
- const readline = require('readline')
- const rl = readline.createInterface({
- input: input || process.stdin,
- output: output || process.stdout,
- completer: completer
- })
- if (rl.output.isTTY) {
- rl.setPrompt('> ')
- rl.prompt()
- }
- // load math.js now, right *after* loading the prompt.
- const math = getMath()
- // TODO: automatic insertion of 'ans' before operators like +, -, *, /
- rl.on('line', function (line) {
- const expr = line.trim()
- switch (expr.toLowerCase()) {
- case 'quit':
- case 'exit':
- // exit application
- rl.close()
- break
- case 'clear':
- // clear memory
- scope = createEmptyMap()
- console.log('memory cleared')
- // get next input
- if (rl.output.isTTY) {
- rl.prompt()
- }
- break
- default:
- if (!expr) {
- break
- }
- switch (mode) {
- case 'evaluate':
- // evaluate expression
- try {
- let node = math.parse(expr)
- let res = node.evaluate(scope)
- if (math.isResultSet(res)) {
- // we can have 0 or 1 results in the ResultSet, as the CLI
- // does not allow multiple expressions separated by a return
- res = res.entries[0]
- node = node.blocks
- .filter(function (entry) { return entry.visible })
- .map(function (entry) { return entry.node })[0]
- }
- if (node) {
- if (math.isAssignmentNode(node)) {
- const name = findSymbolName(node)
- if (name !== null) {
- const value = scope.get(name)
- scope.set('ans', value)
- console.log(name + ' = ' + format(value))
- } else {
- scope.set('ans', res)
- console.log(format(res))
- }
- } else if (math.isHelp(res)) {
- console.log(res.toString())
- } else {
- scope.set('ans', res)
- console.log(format(res))
- }
- }
- } catch (err) {
- console.log(err.toString())
- }
- break
- case 'string':
- try {
- const string = math.parse(expr).toString({ parenthesis: parenthesis })
- console.log(string)
- } catch (err) {
- console.log(err.toString())
- }
- break
- case 'tex':
- try {
- const tex = math.parse(expr).toTex({ parenthesis: parenthesis })
- console.log(tex)
- } catch (err) {
- console.log(err.toString())
- }
- break
- }
- }
- // get next input
- if (rl.output.isTTY) {
- rl.prompt()
- }
- })
- rl.on('close', function () {
- console.log()
- process.exit(0)
- })
- }
- /**
- * Find the symbol name of an AssignmentNode. Recurses into the chain of
- * objects to the root object.
- * @param {AssignmentNode} node
- * @return {string | null} Returns the name when found, else returns null.
- */
- function findSymbolName (node) {
- const math = getMath()
- let n = node
- while (n) {
- if (math.isSymbolNode(n)) {
- return n.name
- }
- n = n.object
- }
- return null
- }
- /**
- * Output application version number.
- * Version number is read version from package.json.
- */
- function outputVersion () {
- fs.readFile(path.join(__dirname, '/../package.json'), function (err, data) {
- if (err) {
- console.log(err.toString())
- } else {
- const pkg = JSON.parse(data)
- const version = pkg && pkg.version ? pkg.version : 'unknown'
- console.log(version)
- }
- process.exit(0)
- })
- }
- /**
- * Output a help message
- */
- function outputHelp () {
- console.log('math.js')
- console.log('https://mathjs.org')
- console.log()
- console.log('Math.js is an extensive math library for JavaScript and Node.js. It features ')
- console.log('real and complex numbers, units, matrices, a large set of mathematical')
- console.log('functions, and a flexible expression parser.')
- console.log()
- console.log('Usage:')
- console.log(' mathjs [scriptfile(s)|expression] {OPTIONS}')
- console.log()
- console.log('Options:')
- console.log(' --version, -v Show application version')
- console.log(' --help, -h Show this message')
- console.log(' --tex Generate LaTeX instead of evaluating')
- console.log(' --string Generate string instead of evaluating')
- console.log(' --parenthesis= Set the parenthesis option to')
- console.log(' either of "keep", "auto" and "all"')
- console.log()
- console.log('Example usage:')
- console.log(' mathjs Open a command prompt')
- console.log(' mathjs 1+2 Evaluate expression')
- console.log(' mathjs script.txt Run a script file')
- console.log(' mathjs script.txt script2.txt Run two script files')
- console.log(' mathjs script.txt > results.txt Run a script file, output to file')
- console.log(' cat script.txt | mathjs Run input stream')
- console.log(' cat script.txt | mathjs > results.txt Run input stream, output to file')
- console.log()
- process.exit(0)
- }
- /**
- * Process input and output, based on the command line arguments
- */
- const scripts = [] // queue of scripts that need to be processed
- let mode = 'evaluate' // one of 'evaluate', 'tex' or 'string'
- let parenthesis = 'keep'
- let version = false
- let help = false
- process.argv.forEach(function (arg, index) {
- if (index < 2) {
- return
- }
- switch (arg) {
- case '-v':
- case '--version':
- version = true
- break
- case '-h':
- case '--help':
- help = true
- break
- case '--tex':
- mode = 'tex'
- break
- case '--string':
- mode = 'string'
- break
- case '--parenthesis=keep':
- parenthesis = 'keep'
- break
- case '--parenthesis=auto':
- parenthesis = 'auto'
- break
- case '--parenthesis=all':
- parenthesis = 'all'
- break
- // TODO: implement configuration via command line arguments
- default:
- scripts.push(arg)
- }
- })
- if (version) {
- outputVersion()
- } else if (help) {
- outputHelp()
- } else if (scripts.length === 0) {
- // run a stream, can be user input or pipe input
- runStream(process.stdin, process.stdout, mode, parenthesis)
- } else {
- fs.stat(scripts[0], function (e, f) {
- if (e) {
- console.log(getMath().evaluate(scripts.join(' ')).toString())
- } else {
- // work through the queue of scripts
- scripts.forEach(function (arg) {
- // run a script file
- runStream(fs.createReadStream(arg), process.stdout, mode, parenthesis)
- })
- }
- })
- }
- // helper function to safely check whether an object as a property
- // copy from the function in object.js which is ES6
- function hasOwnProperty (object, property) {
- return object && Object.hasOwnProperty.call(object, property)
- }
|