UNPKG

12.4 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-2021 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
48const fs = require('fs')
49const path = require('path')
50const { createEmptyMap } = require('../lib/cjs/utils/map.js')
51let scope = createEmptyMap()
52
53const PRECISION = 14 // decimals
54
55/**
56 * "Lazy" load math.js: only require when we actually start using it.
57 * This ensures the cli application looks like it loads instantly.
58 * When requesting help or version number, math.js isn't even loaded.
59 * @return {*}
60 */
61function getMath () {
62 return require('../lib/cjs/defaultInstance.js').default
63}
64
65/**
66 * Helper function to format a value. Regular numbers will be rounded
67 * to 14 digits to prevent round-off errors from showing up.
68 * @param {*} value
69 */
70function format (value) {
71 const math = getMath()
72
73 return math.format(value, {
74 fn: function (value) {
75 if (typeof value === 'number') {
76 // round numbers
77 return math.format(value, PRECISION)
78 } else {
79 return math.format(value)
80 }
81 }
82 })
83}
84
85/**
86 * auto complete a text
87 * @param {String} text
88 * @return {[Array, String]} completions
89 */
90function completer (text) {
91 const math = getMath()
92 let matches = []
93 let keyword
94 const m = /[a-zA-Z_0-9]+$/.exec(text)
95 if (m) {
96 keyword = m[0]
97
98 // scope variables
99 for (const def in scope.keys()) {
100 if (def.indexOf(keyword) === 0) {
101 matches.push(def)
102 }
103 }
104
105 // commandline keywords
106 ['exit', 'quit', 'clear'].forEach(function (cmd) {
107 if (cmd.indexOf(keyword) === 0) {
108 matches.push(cmd)
109 }
110 })
111
112 // math functions and constants
113 const ignore = ['expr', 'type']
114 for (const func in math.expression.mathWithTransform) {
115 if (hasOwnProperty(math.expression.mathWithTransform, func)) {
116 if (func.indexOf(keyword) === 0 && ignore.indexOf(func) === -1) {
117 matches.push(func)
118 }
119 }
120 }
121
122 // units
123 const Unit = math.Unit
124 for (const name in Unit.UNITS) {
125 if (hasOwnProperty(Unit.UNITS, name)) {
126 if (name.indexOf(keyword) === 0) {
127 matches.push(name)
128 }
129 }
130 }
131 for (const name in Unit.PREFIXES) {
132 if (hasOwnProperty(Unit.PREFIXES, name)) {
133 const prefixes = Unit.PREFIXES[name]
134 for (const prefix in prefixes) {
135 if (hasOwnProperty(prefixes, prefix)) {
136 if (prefix.indexOf(keyword) === 0) {
137 matches.push(prefix)
138 } else if (keyword.indexOf(prefix) === 0) {
139 const unitKeyword = keyword.substring(prefix.length)
140 for (const n in Unit.UNITS) {
141 if (hasOwnProperty(Unit.UNITS, n)) {
142 if (n.indexOf(unitKeyword) === 0 &&
143 Unit.isValuelessUnit(prefix + n)) {
144 matches.push(prefix + n)
145 }
146 }
147 }
148 }
149 }
150 }
151 }
152 }
153
154 // remove duplicates
155 matches = matches.filter(function (elem, pos, arr) {
156 return arr.indexOf(elem) === pos
157 })
158 }
159
160 return [matches, keyword]
161}
162
163/**
164 * Run stream, read and evaluate input and stream that to output.
165 * Text lines read from the input are evaluated, and the results are send to
166 * the output.
167 * @param input Input stream
168 * @param output Output stream
169 * @param mode Output mode
170 * @param parenthesis Parenthesis option
171 */
172function runStream (input, output, mode, parenthesis) {
173 const readline = require('readline')
174 const rl = readline.createInterface({
175 input: input || process.stdin,
176 output: output || process.stdout,
177 completer: completer
178 })
179
180 if (rl.output.isTTY) {
181 rl.setPrompt('> ')
182 rl.prompt()
183 }
184
185 // load math.js now, right *after* loading the prompt.
186 const math = getMath()
187
188 // TODO: automatic insertion of 'ans' before operators like +, -, *, /
189
190 rl.on('line', function (line) {
191 const expr = line.trim()
192
193 switch (expr.toLowerCase()) {
194 case 'quit':
195 case 'exit':
196 // exit application
197 rl.close()
198 break
199 case 'clear':
200 // clear memory
201 scope = createEmptyMap()
202 console.log('memory cleared')
203
204 // get next input
205 if (rl.output.isTTY) {
206 rl.prompt()
207 }
208 break
209 default:
210 if (!expr) {
211 break
212 }
213 switch (mode) {
214 case 'evaluate':
215 // evaluate expression
216 try {
217 let node = math.parse(expr)
218 let res = node.evaluate(scope)
219
220 if (math.isResultSet(res)) {
221 // we can have 0 or 1 results in the ResultSet, as the CLI
222 // does not allow multiple expressions separated by a return
223 res = res.entries[0]
224 node = node.blocks
225 .filter(function (entry) { return entry.visible })
226 .map(function (entry) { return entry.node })[0]
227 }
228
229 if (node) {
230 if (math.isAssignmentNode(node)) {
231 const name = findSymbolName(node)
232 if (name !== null) {
233 const value = scope.get(name)
234 scope.set('ans', value)
235 console.log(name + ' = ' + format(value))
236 } else {
237 scope.set('ans', res)
238 console.log(format(res))
239 }
240 } else if (math.isHelp(res)) {
241 console.log(res.toString())
242 } else {
243 scope.set('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}