UNPKG

12.2 kBJavaScriptView Raw
1#!/usr/bin/env node
2/**
3 * math.js
4 * https://github.com/josdejong/mathjs
5 *
6 * Math.js is an extensive math library for JavaScript and Node.js,
7 * It features real and complex numbers, units, matrices, a large set of
8 * mathematical functions, and a flexible expression parser.
9 *
10 * Usage:
11 *
12 * mathjs [scriptfile(s)] {OPTIONS}
13 *
14 * Options:
15 *
16 * --version, -v Show application version
17 * --help, -h Show this message
18 * --tex Generate LaTeX instead of evaluating
19 * --string Generate string instead of evaluating
20 * --parenthesis= Set the parenthesis option to
21 * either of "keep", "auto" and "all"
22 *
23 * Example usage:
24 * mathjs Open a command prompt
25 * mathjs 1+2 Evaluate expression
26 * mathjs script.txt Run a script file
27 * mathjs script1.txt script2.txt Run two script files
28 * mathjs script.txt > results.txt Run a script file, output to file
29 * cat script.txt | mathjs Run input stream
30 * cat script.txt | mathjs > results.txt Run input stream, output to file
31 *
32 * @license
33 * Copyright (C) 2013-2020 Jos de Jong <wjosdejong@gmail.com>
34 *
35 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
36 * use this file except in compliance with the License. You may obtain a copy
37 * of the License at
38 *
39 * https://www.apache.org/licenses/LICENSE-2.0
40 *
41 * Unless required by applicable law or agreed to in writing, software
42 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
43 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
44 * License for the specific language governing permissions and limitations under
45 * the License.
46 */
47
48let scope = {}
49const fs = require('fs')
50const path = require('path')
51
52const PRECISION = 14 // decimals
53
54/**
55 * "Lazy" load math.js: only require when we actually start using it.
56 * This ensures the cli application looks like it loads instantly.
57 * When requesting help or version number, math.js isn't even loaded.
58 * @return {*}
59 */
60function getMath () {
61 return require('../lib/bundleAny')
62}
63
64/**
65 * Helper function to format a value. Regular numbers will be rounded
66 * to 14 digits to prevent round-off errors from showing up.
67 * @param {*} value
68 */
69function format (value) {
70 const math = getMath()
71
72 return math.format(value, {
73 fn: function (value) {
74 if (typeof value === 'number') {
75 // round numbers
76 return math.format(value, PRECISION)
77 } else {
78 return math.format(value)
79 }
80 }
81 })
82}
83
84/**
85 * auto complete a text
86 * @param {String} text
87 * @return {[Array, String]} completions
88 */
89function completer (text) {
90 const math = getMath()
91 let matches = []
92 let keyword
93 const m = /[a-zA-Z_0-9]+$/.exec(text)
94 if (m) {
95 keyword = m[0]
96
97 // scope variables
98 for (const def in scope) {
99 if (hasOwnProperty(scope, def)) {
100 if (def.indexOf(keyword) === 0) {
101 matches.push(def)
102 }
103 }
104 }
105
106 // commandline keywords
107 ['exit', 'quit', 'clear'].forEach(function (cmd) {
108 if (cmd.indexOf(keyword) === 0) {
109 matches.push(cmd)
110 }
111 })
112
113 // math functions and constants
114 const ignore = ['expr', 'type']
115 for (const func in math.expression.mathWithTransform) {
116 if (hasOwnProperty(math.expression.mathWithTransform, func)) {
117 if (func.indexOf(keyword) === 0 && ignore.indexOf(func) === -1) {
118 matches.push(func)
119 }
120 }
121 }
122
123 // units
124 const Unit = math.Unit
125 for (const name in Unit.UNITS) {
126 if (hasOwnProperty(Unit.UNITS, name)) {
127 if (name.indexOf(keyword) === 0) {
128 matches.push(name)
129 }
130 }
131 }
132 for (const name in Unit.PREFIXES) {
133 if (hasOwnProperty(Unit.PREFIXES, name)) {
134 const prefixes = Unit.PREFIXES[name]
135 for (const prefix in prefixes) {
136 if (hasOwnProperty(prefixes, prefix)) {
137 if (prefix.indexOf(keyword) === 0) {
138 matches.push(prefix)
139 } else if (keyword.indexOf(prefix) === 0) {
140 const unitKeyword = keyword.substring(prefix.length)
141 for (const n in Unit.UNITS) {
142 if (hasOwnProperty(Unit.UNITS, n)) {
143 if (n.indexOf(unitKeyword) === 0 &&
144 Unit.isValuelessUnit(prefix + n)) {
145 matches.push(prefix + n)
146 }
147 }
148 }
149 }
150 }
151 }
152 }
153 }
154
155 // remove duplicates
156 matches = matches.filter(function (elem, pos, arr) {
157 return arr.indexOf(elem) === pos
158 })
159 }
160
161 return [matches, keyword]
162}
163
164/**
165 * Run stream, read and evaluate input and stream that to output.
166 * Text lines read from the input are evaluated, and the results are send to
167 * the output.
168 * @param input Input stream
169 * @param output Output stream
170 * @param mode Output mode
171 * @param parenthesis Parenthesis option
172 */
173function runStream (input, output, mode, parenthesis) {
174 const readline = require('readline')
175 const rl = readline.createInterface({
176 input: input || process.stdin,
177 output: output || process.stdout,
178 completer: completer
179 })
180
181 if (rl.output.isTTY) {
182 rl.setPrompt('> ')
183 rl.prompt()
184 }
185
186 // load math.js now, right *after* loading the prompt.
187 const math = getMath()
188
189 // TODO: automatic insertion of 'ans' before operators like +, -, *, /
190
191 rl.on('line', function (line) {
192 const expr = line.trim()
193
194 switch (expr.toLowerCase()) {
195 case 'quit':
196 case 'exit':
197 // exit application
198 rl.close()
199 break
200 case 'clear':
201 // clear memory
202 scope = {}
203 console.log('memory cleared')
204
205 // get next input
206 if (rl.output.isTTY) {
207 rl.prompt()
208 }
209 break
210 default:
211 if (!expr) {
212 break
213 }
214 switch (mode) {
215 case 'evaluate':
216 // evaluate expression
217 try {
218 let node = math.parse(expr)
219 let res = node.evaluate(scope)
220
221 if (math.isResultSet(res)) {
222 // we can have 0 or 1 results in the ResultSet, as the CLI
223 // does not allow multiple expressions separated by a return
224 res = res.entries[0]
225 node = node.blocks
226 .filter(function (entry) { return entry.visible })
227 .map(function (entry) { return entry.node })[0]
228 }
229
230 if (node) {
231 if (math.isAssignmentNode(node)) {
232 const name = findSymbolName(node)
233 if (name !== null) {
234 scope.ans = scope[name]
235 console.log(name + ' = ' + format(scope[name]))
236 } else {
237 scope.ans = res
238 console.log(format(res))
239 }
240 } else if (math.isHelp(res)) {
241 console.log(res.toString())
242 } else {
243 scope.ans = res
244 console.log(format(res))
245 }
246 }
247 } catch (err) {
248 console.log(err.toString())
249 }
250 break
251
252 case 'string':
253 try {
254 const string = math.parse(expr).toString({ parenthesis: parenthesis })
255 console.log(string)
256 } catch (err) {
257 console.log(err.toString())
258 }
259 break
260
261 case 'tex':
262 try {
263 const tex = math.parse(expr).toTex({ parenthesis: parenthesis })
264 console.log(tex)
265 } catch (err) {
266 console.log(err.toString())
267 }
268 break
269 }
270 }
271
272 // get next input
273 if (rl.output.isTTY) {
274 rl.prompt()
275 }
276 })
277
278 rl.on('close', function () {
279 console.log()
280 process.exit(0)
281 })
282}
283
284/**
285 * Find the symbol name of an AssignmentNode. Recurses into the chain of
286 * objects to the root object.
287 * @param {AssignmentNode} node
288 * @return {string | null} Returns the name when found, else returns null.
289 */
290function findSymbolName (node) {
291 const math = getMath()
292 let n = node
293
294 while (n) {
295 if (math.isSymbolNode(n)) {
296 return n.name
297 }
298 n = n.object
299 }
300
301 return null
302}
303
304/**
305 * Output application version number.
306 * Version number is read version from package.json.
307 */
308function outputVersion () {
309 fs.readFile(path.join(__dirname, '/../package.json'), function (err, data) {
310 if (err) {
311 console.log(err.toString())
312 } else {
313 const pkg = JSON.parse(data)
314 const version = pkg && pkg.version ? pkg.version : 'unknown'
315 console.log(version)
316 }
317 process.exit(0)
318 })
319}
320
321/**
322 * Output a help message
323 */
324function outputHelp () {
325 console.log('math.js')
326 console.log('https://mathjs.org')
327 console.log()
328 console.log('Math.js is an extensive math library for JavaScript and Node.js. It features ')
329 console.log('real and complex numbers, units, matrices, a large set of mathematical')
330 console.log('functions, and a flexible expression parser.')
331 console.log()
332 console.log('Usage:')
333 console.log(' mathjs [scriptfile(s)|expression] {OPTIONS}')
334 console.log()
335 console.log('Options:')
336 console.log(' --version, -v Show application version')
337 console.log(' --help, -h Show this message')
338 console.log(' --tex Generate LaTeX instead of evaluating')
339 console.log(' --string Generate string instead of evaluating')
340 console.log(' --parenthesis= Set the parenthesis option to')
341 console.log(' either of "keep", "auto" and "all"')
342 console.log()
343 console.log('Example usage:')
344 console.log(' mathjs Open a command prompt')
345 console.log(' mathjs 1+2 Evaluate expression')
346 console.log(' mathjs script.txt Run a script file')
347 console.log(' mathjs script.txt script2.txt Run two script files')
348 console.log(' mathjs script.txt > results.txt Run a script file, output to file')
349 console.log(' cat script.txt | mathjs Run input stream')
350 console.log(' cat script.txt | mathjs > results.txt Run input stream, output to file')
351 console.log()
352 process.exit(0)
353}
354
355/**
356 * Process input and output, based on the command line arguments
357 */
358const scripts = [] // queue of scripts that need to be processed
359let mode = 'evaluate' // one of 'evaluate', 'tex' or 'string'
360let parenthesis = 'keep'
361let version = false
362let help = false
363
364process.argv.forEach(function (arg, index) {
365 if (index < 2) {
366 return
367 }
368
369 switch (arg) {
370 case '-v':
371 case '--version':
372 version = true
373 break
374
375 case '-h':
376 case '--help':
377 help = true
378 break
379
380 case '--tex':
381 mode = 'tex'
382 break
383
384 case '--string':
385 mode = 'string'
386 break
387
388 case '--parenthesis=keep':
389 parenthesis = 'keep'
390 break
391
392 case '--parenthesis=auto':
393 parenthesis = 'auto'
394 break
395
396 case '--parenthesis=all':
397 parenthesis = 'all'
398 break
399
400 // TODO: implement configuration via command line arguments
401
402 default:
403 scripts.push(arg)
404 }
405})
406
407if (version) {
408 outputVersion()
409} else if (help) {
410 outputHelp()
411} else if (scripts.length === 0) {
412 // run a stream, can be user input or pipe input
413 runStream(process.stdin, process.stdout, mode, parenthesis)
414} else {
415 fs.stat(scripts[0], function (e, f) {
416 if (e) {
417 console.log(getMath().evaluate(scripts.join(' ')).toString())
418 } else {
419 // work through the queue of scripts
420 scripts.forEach(function (arg) {
421 // run a script file
422 runStream(fs.createReadStream(arg), process.stdout, mode, parenthesis)
423 })
424 }
425 })
426}
427
428// helper function to safely check whether an object as a property
429// copy from the function in object.js which is ES6
430function hasOwnProperty (object, property) {
431 return object && Object.hasOwnProperty.call(object, property)
432}