mathjs
Version:
Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser with support for symbolic computation, comes with a large set of built-in functions and constants, and offers an integrated solution to work with dif
326 lines (315 loc) • 12.3 kB
JavaScript
import { isBigNumber, isComplex, isFraction, isMatrix, isUnit } from '../../utils/is.js';
import { isFactory, stripOptionalNotation } from '../../utils/factory.js';
import { hasOwnProperty, lazy } from '../../utils/object.js';
import { contains } from '../../utils/array.js';
import { ArgumentsError } from '../../error/ArgumentsError.js';
export function importFactory(typed, load, math, importedFactories) {
/**
* Import functions from an object or a module.
*
* This function is only available on a mathjs instance created using `create`.
*
* Syntax:
*
* math.import(functions)
* math.import(functions, options)
*
* Where:
*
* - `functions: Object`
* An object with functions or factories to be imported.
* - `options: Object` An object with import options. Available options:
* - `override: boolean`
* If true, existing functions will be overwritten. False by default.
* - `silent: boolean`
* If true, the function will not throw errors on duplicates or invalid
* types. False by default.
* - `wrap: boolean`
* If true, the functions will be wrapped in a wrapper function
* which converts data types like Matrix to primitive data types like Array.
* The wrapper is needed when extending math.js with libraries which do not
* support these data type. False by default.
*
* Examples:
*
* import { create, all } from 'mathjs'
* import * as numbers from 'numbers'
*
* // create a mathjs instance
* const math = create(all)
*
* // define new functions and variables
* math.import({
* myvalue: 42,
* hello: function (name) {
* return 'hello, ' + name + '!'
* }
* })
*
* // use the imported function and variable
* math.myvalue * 2 // 84
* math.hello('user') // 'hello, user!'
*
* // import the npm module 'numbers'
* // (must be installed first with `npm install numbers`)
* math.import(numbers, {wrap: true})
*
* math.fibonacci(7) // returns 13
*
* @param {Object | Array} functions Object with functions to be imported.
* @param {Object} [options] Import options.
*/
function mathImport(functions, options) {
var num = arguments.length;
if (num !== 1 && num !== 2) {
throw new ArgumentsError('import', num, 1, 2);
}
if (!options) {
options = {};
}
function flattenImports(flatValues, value, name) {
if (Array.isArray(value)) {
value.forEach(item => flattenImports(flatValues, item));
} else if (typeof value === 'object') {
for (var _name in value) {
if (hasOwnProperty(value, _name)) {
flattenImports(flatValues, value[_name], _name);
}
}
} else if (isFactory(value) || name !== undefined) {
var flatName = isFactory(value) ? isTransformFunctionFactory(value) ? value.fn + '.transform' // TODO: this is ugly
: value.fn : name;
// we allow importing the same function twice if it points to the same implementation
if (hasOwnProperty(flatValues, flatName) && flatValues[flatName] !== value && !options.silent) {
throw new Error('Cannot import "' + flatName + '" twice');
}
flatValues[flatName] = value;
} else {
if (!options.silent) {
throw new TypeError('Factory, Object, or Array expected');
}
}
}
var flatValues = {};
flattenImports(flatValues, functions);
for (var name in flatValues) {
if (hasOwnProperty(flatValues, name)) {
// console.log('import', name)
var value = flatValues[name];
if (isFactory(value)) {
// we ignore name here and enforce the name of the factory
// maybe at some point we do want to allow overriding it
// in that case we can implement an option overrideFactoryNames: true
_importFactory(value, options);
} else if (isSupportedType(value)) {
_import(name, value, options);
} else {
if (!options.silent) {
throw new TypeError('Factory, Object, or Array expected');
}
}
}
}
}
/**
* Add a property to the math namespace
* @param {string} name
* @param {*} value
* @param {Object} options See import for a description of the options
* @private
*/
function _import(name, value, options) {
// TODO: refactor this function, it's to complicated and contains duplicate code
if (options.wrap && typeof value === 'function') {
// create a wrapper around the function
value = _wrap(value);
}
// turn a plain function with a typed-function signature into a typed-function
if (hasTypedFunctionSignature(value)) {
value = typed(name, {
[value.signature]: value
});
}
if (typed.isTypedFunction(math[name]) && typed.isTypedFunction(value)) {
if (options.override) {
// give the typed function the right name
value = typed(name, value.signatures);
} else {
// merge the existing and typed function
value = typed(math[name], value);
}
math[name] = value;
delete importedFactories[name];
_importTransform(name, value);
math.emit('import', name, function resolver() {
return value;
});
return;
}
if (math[name] === undefined || options.override) {
math[name] = value;
delete importedFactories[name];
_importTransform(name, value);
math.emit('import', name, function resolver() {
return value;
});
return;
}
if (!options.silent) {
throw new Error('Cannot import "' + name + '": already exists');
}
}
function _importTransform(name, value) {
if (value && typeof value.transform === 'function') {
math.expression.transform[name] = value.transform;
if (allowedInExpressions(name)) {
math.expression.mathWithTransform[name] = value.transform;
}
} else {
// remove existing transform
delete math.expression.transform[name];
if (allowedInExpressions(name)) {
math.expression.mathWithTransform[name] = value;
}
}
}
function _deleteTransform(name) {
delete math.expression.transform[name];
if (allowedInExpressions(name)) {
math.expression.mathWithTransform[name] = math[name];
} else {
delete math.expression.mathWithTransform[name];
}
}
/**
* Create a wrapper a round an function which converts the arguments
* to their primitive values (like convert a Matrix to Array)
* @param {Function} fn
* @return {Function} Returns the wrapped function
* @private
*/
function _wrap(fn) {
var wrapper = function wrapper() {
var args = [];
for (var i = 0, len = arguments.length; i < len; i++) {
var arg = arguments[i];
args[i] = arg && arg.valueOf();
}
return fn.apply(math, args);
};
if (fn.transform) {
wrapper.transform = fn.transform;
}
return wrapper;
}
/**
* Import an instance of a factory into math.js
* @param {function(scope: object)} factory
* @param {Object} options See import for a description of the options
* @param {string} [name=factory.name] Optional custom name
* @private
*/
function _importFactory(factory, options) {
var name = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : factory.fn;
if (contains(name, '.')) {
throw new Error('Factory name should not contain a nested path. ' + 'Name: ' + JSON.stringify(name));
}
var namespace = isTransformFunctionFactory(factory) ? math.expression.transform : math;
var existingTransform = (name in math.expression.transform);
var existing = hasOwnProperty(namespace, name) ? namespace[name] : undefined;
var resolver = function resolver() {
// collect all dependencies, handle finding both functions and classes and other special cases
var dependencies = {};
factory.dependencies.map(stripOptionalNotation).forEach(dependency => {
if (contains(dependency, '.')) {
throw new Error('Factory dependency should not contain a nested path. ' + 'Name: ' + JSON.stringify(dependency));
}
if (dependency === 'math') {
dependencies.math = math;
} else if (dependency === 'mathWithTransform') {
dependencies.mathWithTransform = math.expression.mathWithTransform;
} else if (dependency === 'classes') {
// special case for json reviver
dependencies.classes = math;
} else {
dependencies[dependency] = math[dependency];
}
});
var instance = /* #__PURE__ */factory(dependencies);
if (instance && typeof instance.transform === 'function') {
throw new Error('Transforms cannot be attached to factory functions. ' + 'Please create a separate function for it with exports.path="expression.transform"');
}
if (existing === undefined || options.override) {
return instance;
}
if (typed.isTypedFunction(existing) && typed.isTypedFunction(instance)) {
// merge the existing and new typed function
return typed(existing, instance);
}
if (options.silent) {
// keep existing, ignore imported function
return existing;
} else {
throw new Error('Cannot import "' + name + '": already exists');
}
};
// TODO: add unit test with non-lazy factory
if (!factory.meta || factory.meta.lazy !== false) {
lazy(namespace, name, resolver);
// FIXME: remove the `if (existing &&` condition again. Can we make sure subset is loaded before subset.transform? (Name collision, and no dependencies between the two)
if (existing && existingTransform) {
_deleteTransform(name);
} else {
if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) {
lazy(math.expression.mathWithTransform, name, () => namespace[name]);
}
}
} else {
namespace[name] = resolver();
// FIXME: remove the `if (existing &&` condition again. Can we make sure subset is loaded before subset.transform? (Name collision, and no dependencies between the two)
if (existing && existingTransform) {
_deleteTransform(name);
} else {
if (isTransformFunctionFactory(factory) || factoryAllowedInExpressions(factory)) {
lazy(math.expression.mathWithTransform, name, () => namespace[name]);
}
}
}
// TODO: improve factories, store a list with imports instead which can be re-played
importedFactories[name] = factory;
math.emit('import', name, resolver);
}
/**
* Check whether given object is a type which can be imported
* @param {Function | number | string | boolean | null | Unit | Complex} object
* @return {boolean}
* @private
*/
function isSupportedType(object) {
return typeof object === 'function' || typeof object === 'number' || typeof object === 'string' || typeof object === 'boolean' || object === null || isUnit(object) || isComplex(object) || isBigNumber(object) || isFraction(object) || isMatrix(object) || Array.isArray(object);
}
function hasTypedFunctionSignature(fn) {
return typeof fn === 'function' && typeof fn.signature === 'string';
}
function allowedInExpressions(name) {
return !hasOwnProperty(unsafe, name);
}
function factoryAllowedInExpressions(factory) {
return factory.fn.indexOf('.') === -1 &&
// FIXME: make checking on path redundant, check on meta data instead
!hasOwnProperty(unsafe, factory.fn) && (!factory.meta || !factory.meta.isClass);
}
function isTransformFunctionFactory(factory) {
return factory !== undefined && factory.meta !== undefined && factory.meta.isTransformFunction === true || false;
}
// namespaces and functions not available in the parser for safety reasons
var unsafe = {
expression: true,
type: true,
docs: true,
error: true,
json: true,
chain: true // chain method not supported. Note that there is a unit chain too.
};
return mathImport;
}