function-composer
Advanced tools
Comparing version 0.2.0 to 0.3.0
@@ -10,3 +10,3 @@ var compose = require('../function-composer'); | ||
// register a test for this new type | ||
compose.tests['Person'] = function (x) { | ||
compose.types['Person'] = function (x) { | ||
return x instanceof Person; | ||
@@ -13,0 +13,0 @@ }; |
@@ -22,5 +22,12 @@ /** | ||
// order types | ||
// object will be ordered last as other types may be an object too. | ||
// anytype (*) will be ordered last, and then object, as other types may be | ||
// an object too. | ||
function compareTypes(a, b) { | ||
return a === 'Object' ? 1 : b === 'Object' ? -1 : 0 | ||
if (a === '*') return 1; | ||
if (b === '*') return -1; | ||
if (a === 'Object') return 1; | ||
if (b === 'Object') return -1; | ||
return 0; | ||
} | ||
@@ -34,45 +41,138 @@ | ||
/** | ||
* Compose a function from sub-functions each handling a single type signature. | ||
* @param {string} [name] An optional name for the function | ||
* @param {Object.<string, function>} signatures | ||
* A map with the type signature as key and the sub-function as value | ||
* @return {function} Returns the composed function | ||
* Collection with function definitions (local shortcuts to functions) | ||
* @constructor | ||
*/ | ||
function compose(name, signatures) { | ||
if (!signatures) { | ||
signatures = name; | ||
name = null; | ||
} | ||
function Defs() {} | ||
var normalized = {}; // normalized function signatures | ||
var defs = { // function definitions (local shortcuts to functions) | ||
signature: [], | ||
test: [], | ||
convert: [] | ||
}; | ||
function addDef(fn, type) { | ||
var index = defs[type].indexOf(fn); | ||
if (index == -1) { | ||
index = defs[type].length; | ||
defs[type].push(fn); | ||
} | ||
return type + index; | ||
/** | ||
* Add a function definition. | ||
* @param {function} fn | ||
* @param {string} [type='fn'] | ||
* @returns {string} Returns the function name, for example 'fn0' or 'signature2' | ||
*/ | ||
Defs.prototype.add = function (fn, type) { | ||
type = type || 'fn'; | ||
if (!this[type]) this[type] = []; | ||
var index = this[type].indexOf(fn); | ||
if (index == -1) { | ||
index = this[type].length; | ||
this[type].push(fn); | ||
} | ||
return type + index; | ||
}; | ||
// analise all signatures | ||
var argumentCount = 0; | ||
var parameters = {}; | ||
Object.keys(signatures).forEach(function (signature) { | ||
var fn = signatures[signature]; | ||
var params = (signature !== '') ? signature.split(',').map(function (param) { | ||
/** | ||
* Create code lines for all definitions | ||
* @param [name='defs'] | ||
* @returns {Array} Returns the code lines containing all function definitions | ||
*/ | ||
Defs.prototype.code = function (name) { | ||
var me = this; | ||
var code = []; | ||
name = name || 'defs'; | ||
Object.keys(this).forEach(function (type) { | ||
var def = me[type]; | ||
def.forEach(function (def, index) { | ||
code.push('var ' + type + index + ' = ' + name + '[\'' + type + '\'][' + index + '];'); | ||
}); | ||
}); | ||
return code; | ||
}; | ||
/** | ||
* A function signature | ||
* @param {string | Array.<string>} params Array with the type(s) of each parameter, | ||
* or a comma separated string with types | ||
* @param {function} fn The actual function | ||
* @constructor | ||
*/ | ||
function Signature(params, fn) { | ||
if (typeof params === 'string') { | ||
this.params = (params !== '') ? params.split(',').map(function (param) { | ||
return param.trim(); | ||
}) : []; | ||
var normSignature = params.join(','); | ||
normalized[normSignature] = fn; | ||
argumentCount = Math.max(argumentCount, params.length); | ||
} | ||
else { | ||
this.params = params; | ||
} | ||
this.fn = fn; | ||
} | ||
// get the entry for this number of arguments | ||
var obj = parameters[params.length]; | ||
// TODO: implement function Signature.split | ||
// TODO: implement function Signature.merge | ||
// TODO: implement function Signature.toString | ||
/** | ||
* split all raw signatures into an array with splitted params | ||
* @param {Object.<string, function>} rawSignatures | ||
* @return {Array.<{params: Array.<string>, fn: function}>} Returns splitted signatures | ||
*/ | ||
function splitSignatures(rawSignatures) { | ||
return Object.keys(rawSignatures).map(function (params) { | ||
var fn = rawSignatures[params]; | ||
return new Signature(params, fn); | ||
}); | ||
// TODO: split params containing an '|' into multiple entries | ||
} | ||
/** | ||
* create a map with normalized signatures as key and the function as value | ||
* @param {Array.<{params: Array.<string>, fn: function}>} signatures An array with splitted signatures | ||
* @return {{}} Returns a map with normalized signatures | ||
*/ | ||
function normalizeSignatures(signatures) { | ||
var normalized = {}; | ||
signatures.map(function (entry) { | ||
var signature = entry.params.join(','); | ||
if (signature in normalized) { | ||
throw new Error('Error: signature "' + signature + '" defined twice'); | ||
} | ||
normalized[signature] = entry.fn; | ||
}); | ||
return normalized | ||
} | ||
/** | ||
* create an array with for every parameter an array with possible types | ||
* @param {Array.<{params: Array.<string>, fn: function}>} signatures An array with splitted signatures | ||
* @return {Array.<Array.<string>>} Returns an array with allowed types per parameter | ||
*/ | ||
function splitTypes(signatures) { | ||
var types = []; | ||
signatures.forEach(function (entry) { | ||
entry.params.forEach(function (param, i) { | ||
if (!types[i]) { | ||
types[i] = []; | ||
} | ||
if (types[i].indexOf(param) == -1) { | ||
types[i].push(param); | ||
} | ||
}); | ||
}); | ||
return types; | ||
} | ||
/** | ||
* create a recursive tree for traversing the number and type of parameters | ||
* @param {Array.<{params: Array.<string>, fn: function}>} signatures An array with splitted signatures | ||
* @returns {{}} | ||
*/ | ||
function createParamsTree(signatures) { | ||
var tree = {}; | ||
signatures.forEach(function (entry) { | ||
var params = entry.params.concat([]); | ||
// get the tree entry for the current number of arguments | ||
var obj = tree[params.length]; | ||
if (!obj) { | ||
obj = parameters[params.length] = { | ||
obj = tree[params.length] = { | ||
signature: [], | ||
@@ -97,5 +197,26 @@ fn: null, | ||
obj.fn = fn; | ||
// add the function as leaf | ||
obj.fn = entry.fn; | ||
}); | ||
return tree; | ||
} | ||
/** | ||
* Compose a function from sub-functions each handling a single type signature. | ||
* @param {string} [name] An optional name for the function | ||
* @param {Object.<string, function>} signatures | ||
* A map with the type signature as key and the sub-function as value | ||
* @return {function} Returns the composed function | ||
*/ | ||
function compose(name, signatures) { | ||
// handle arguments | ||
if (!signatures) { | ||
signatures = name; | ||
name = null; | ||
} | ||
var defs = new Defs(); | ||
var structure = splitSignatures(signatures); | ||
function switchTypes(signature, args, prefix) { | ||
@@ -105,3 +226,3 @@ var code = []; | ||
if (signature.fn !== null) { | ||
var def = addDef(signature.fn, 'signature'); | ||
var def = defs.add(signature.fn, 'signature'); | ||
code.push(prefix + 'return ' + def + '(' + args.join(', ') +'); // signature: ' + signature.signature); | ||
@@ -113,9 +234,22 @@ } | ||
.sort(compareTypes) | ||
.forEach(function (type) { | ||
.forEach(function (type, index) { | ||
var arg = 'arg' + args.length; | ||
var def = addDef(getTest(type), 'test') + '(' + arg + ')'; | ||
code.push(prefix + 'if (' + def + ') { // type: ' + type); | ||
code = code.concat(switchTypes(signature.types[type], args.concat(arg), prefix + ' ')); | ||
code.push(prefix + '}'); | ||
var before; | ||
var after; | ||
var nextPrefix = prefix + ' '; | ||
if (type == '*') { | ||
before = (index > 0 ? 'else {' : ''); | ||
after = (index > 0 ? '}' : ''); | ||
if (index == 0) {nextPrefix = prefix;} | ||
} | ||
else { | ||
var def = defs.add(getTest(type), 'test') + '(' + arg + ')'; | ||
before = 'if (' + def + ') { // type: ' + type; | ||
after = '}'; | ||
} | ||
if (before) code.push(prefix + before); | ||
code = code.concat(switchTypes(signature.types[type], args.concat(arg), nextPrefix)); | ||
if (after) code.push(prefix + after); | ||
}); | ||
@@ -135,4 +269,4 @@ | ||
var arg = 'arg' + args.length; | ||
var test = addDef(getTest(conversion.from), 'test') + '(' + arg + ')'; | ||
var convert = addDef(conversion.convert, 'convert') + '(' + arg + ')'; | ||
var test = defs.add(getTest(conversion.from), 'test') + '(' + arg + ')'; | ||
var convert = defs.add(conversion.convert, 'convert') + '(' + arg + ')'; | ||
@@ -144,3 +278,2 @@ code.push(prefix + 'if (' + test + ') { // type: ' + conversion.from + ', convert to ' + conversion.to); | ||
}); | ||
} | ||
@@ -151,14 +284,16 @@ | ||
var args = []; | ||
for (var i = 0; i < argumentCount; i++) { | ||
args.push('arg' + i); | ||
var types = splitTypes(structure); | ||
var params = []; | ||
for (var i = 0; i < types.length; i++) { // we can't use .map here, some entries may be undefined | ||
params.push('arg' + i); | ||
} | ||
var code = []; | ||
var counts = Object.keys(parameters); | ||
code.push('return function ' + (name || '') + '(' + args.join(', ') + ') {'); | ||
counts | ||
var tree = createParamsTree(structure); | ||
var paramCounts = Object.keys(tree); | ||
code.push('return function ' + (name || '') + '(' + params.join(', ') + ') {'); | ||
paramCounts | ||
.sort(compareNumbers) | ||
.forEach(function (count, index) { | ||
var signature = parameters[count]; | ||
var signature = tree[count]; | ||
var args = []; | ||
@@ -170,3 +305,3 @@ var statement = (index == 0) ? 'if' : 'else if'; | ||
code.push(' }'); | ||
if (index == counts.length - 1) { | ||
if (index == paramCounts.length - 1) { | ||
code.push(' else {'); | ||
@@ -182,15 +317,10 @@ code.push(' throw new TypeError(\'Wrong number of arguments\');'); // TODO: output the allowed numbers | ||
factory.push('(function (defs) {'); | ||
Object.keys(defs).forEach(function (type) { | ||
defs[type].forEach(function | ||
(def, index) { | ||
factory.push('var ' + type + index + ' = defs[\'' + type + '\'][' + index + '];'); | ||
}); | ||
}); | ||
factory = factory.concat(defs.code('defs')); | ||
factory = factory.concat(code); | ||
factory.push( '})'); | ||
factory.push('})'); | ||
var fn = eval(factory.join('\n'))(defs); | ||
// attach the original functions | ||
fn.signatures = normalized; | ||
// attach the signatures with sub-functions to the constructed function | ||
fn.signatures = normalizeSignatures(structure); // normalized signatures | ||
@@ -201,3 +331,3 @@ return fn; | ||
// data type tests | ||
compose.tests = { | ||
compose.types = { | ||
'null': function (x) {return x === null}, | ||
@@ -215,5 +345,5 @@ 'boolean': function (x) {return typeof x === 'boolean'}, | ||
function getTest(type) { | ||
var test = compose.tests[type]; | ||
var test = compose.types[type]; | ||
if (!test) { | ||
var matches = Object.keys(compose.tests) | ||
var matches = Object.keys(compose.types) | ||
.filter(function (t) { | ||
@@ -220,0 +350,0 @@ return t.toLowerCase() == type.toLowerCase(); |
@@ -1,1 +0,1 @@ | ||
(function(root,factory){if(typeof define==="function"&&define.amd){define([],factory)}else if(typeof exports==="object"){module.exports=factory()}else{root.compose=factory()}})(this,function(){"use strict";function compareTypes(a,b){return a==="Object"?1:b==="Object"?-1:0}function compareNumbers(a,b){return a>b}function compose(name,signatures){if(!signatures){signatures=name;name=null}var normalized={};var defs={signature:[],test:[],convert:[]};function addDef(fn,type){var index=defs[type].indexOf(fn);if(index==-1){index=defs[type].length;defs[type].push(fn)}return type+index}var argumentCount=0;var parameters={};Object.keys(signatures).forEach(function(signature){var fn=signatures[signature];var params=signature!==""?signature.split(",").map(function(param){return param.trim()}):[];var normSignature=params.join(",");normalized[normSignature]=fn;argumentCount=Math.max(argumentCount,params.length);var obj=parameters[params.length];if(!obj){obj=parameters[params.length]={signature:[],fn:null,types:{}}}while(params.length>0){var param=params.shift();if(!obj.types[param]){obj.types[param]={signature:obj.signature.concat(param),fn:null,types:{}}}obj=obj.types[param]}obj.fn=fn});function switchTypes(signature,args,prefix){var code=[];if(signature.fn!==null){var def=addDef(signature.fn,"signature");code.push(prefix+"return "+def+"("+args.join(", ")+"); // signature: "+signature.signature)}else{Object.keys(signature.types).sort(compareTypes).forEach(function(type){var arg="arg"+args.length;var def=addDef(getTest(type),"test")+"("+arg+")";code.push(prefix+"if ("+def+") { // type: "+type);code=code.concat(switchTypes(signature.types[type],args.concat(arg),prefix+" "));code.push(prefix+"}")});var added={};compose.conversions.filter(function(conversion){return signature.types[conversion.to]&&!signature.types[conversion.from]}).forEach(function(conversion){if(!added[conversion.from]){added[conversion.from]=true;var arg="arg"+args.length;var test=addDef(getTest(conversion.from),"test")+"("+arg+")";var convert=addDef(conversion.convert,"convert")+"("+arg+")";code.push(prefix+"if ("+test+") { // type: "+conversion.from+", convert to "+conversion.to);code=code.concat(switchTypes(signature.types[conversion.to],args.concat(convert),prefix+" "));code.push(prefix+"}")}})}return code}var args=[];for(var i=0;i<argumentCount;i++){args.push("arg"+i)}var code=[];var counts=Object.keys(parameters);code.push("return function "+(name||"")+"("+args.join(", ")+") {");counts.sort(compareNumbers).forEach(function(count,index){var signature=parameters[count];var args=[];var statement=index==0?"if":"else if";code.push(" "+statement+" (arguments.length == "+count+") {");code=code.concat(switchTypes(signature,args," "));code.push(" }");if(index==counts.length-1){code.push(" else {");code.push(" throw new TypeError('Wrong number of arguments');");code.push(" }")}});code.push(" throw new TypeError('Wrong function signature');");code.push("}");var factory=[];factory.push("(function (defs) {");Object.keys(defs).forEach(function(type){defs[type].forEach(function(def,index){factory.push("var "+type+index+" = defs['"+type+"']["+index+"];")})});factory=factory.concat(code);factory.push("})");var fn=eval(factory.join("\n"))(defs);fn.signatures=normalized;return fn}compose.tests={"null":function(x){return x===null},"boolean":function(x){return typeof x==="boolean"},number:function(x){return typeof x==="number"},string:function(x){return typeof x==="string"},"function":function(x){return typeof x==="function"},Array:function(x){return Array.isArray(x)},Date:function(x){return x instanceof Date},RegExp:function(x){return x instanceof RegExp},Object:function(x){return typeof x==="object"}};function getTest(type){var test=compose.tests[type];if(!test){var matches=Object.keys(compose.tests).filter(function(t){return t.toLowerCase()==type.toLowerCase()}).map(function(t){return'"'+t+'"'});throw new Error('Unknown type "'+type+'"'+(matches.length?". Did you mean "+matches.join(", or ")+"?":""))}return test}compose.conversions=[];return compose}); | ||
!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?module.exports=t():e.compose=t()}(this,function(){"use strict";function compareTypes(e,t){return"*"===e?1:"*"===t?-1:"Object"===e?1:"Object"===t?-1:0}function compareNumbers(e,t){return e>t}function Defs(){}function Signature(e,t){this.params="string"==typeof e?""!==e?e.split(",").map(function(e){return e.trim()}):[]:e,this.fn=t}function splitSignatures(e){return Object.keys(e).map(function(t){var n=e[t];return new Signature(t,n)})}function normalizeSignatures(e){var t={};return e.map(function(e){var n=e.params.join(",");if(n in t)throw new Error('Error: signature "'+n+'" defined twice');t[n]=e.fn}),t}function splitTypes(e){var t=[];return e.forEach(function(e){e.params.forEach(function(e,n){t[n]||(t[n]=[]),-1==t[n].indexOf(e)&&t[n].push(e)})}),t}function createParamsTree(e){var t={};return e.forEach(function(e){var n=e.params.concat([]),r=t[n.length];for(r||(r=t[n.length]={signature:[],fn:null,types:{}});n.length>0;){var o=n.shift();r.types[o]||(r.types[o]={signature:r.signature.concat(o),fn:null,types:{}}),r=r.types[o]}r.fn=e.fn}),t}function compose(name,signatures){function switchTypes(e,t,n){var r=[];if(null!==e.fn){var o=defs.add(e.fn,"signature");r.push(n+"return "+o+"("+t.join(", ")+"); // signature: "+e.signature)}else{Object.keys(e.types).sort(compareTypes).forEach(function(o,s){var u,c,a="arg"+t.length,i=n+" ";if("*"==o)u=s>0?"else {":"",c=s>0?"}":"",0==s&&(i=n);else{var f=defs.add(getTest(o),"test")+"("+a+")";u="if ("+f+") { // type: "+o,c="}"}u&&r.push(n+u),r=r.concat(switchTypes(e.types[o],t.concat(a),i)),c&&r.push(n+c)});var s={};compose.conversions.filter(function(t){return e.types[t.to]&&!e.types[t.from]}).forEach(function(o){if(!s[o.from]){s[o.from]=!0;var u="arg"+t.length,c=defs.add(getTest(o.from),"test")+"("+u+")",a=defs.add(o.convert,"convert")+"("+u+")";r.push(n+"if ("+c+") { // type: "+o.from+", convert to "+o.to),r=r.concat(switchTypes(e.types[o.to],t.concat(a),n+" ")),r.push(n+"}")}})}return r}signatures||(signatures=name,name=null);for(var defs=new Defs,structure=splitSignatures(signatures),types=splitTypes(structure),params=[],i=0;i<types.length;i++)params.push("arg"+i);var code=[],tree=createParamsTree(structure),paramCounts=Object.keys(tree);code.push("return function "+(name||"")+"("+params.join(", ")+") {"),paramCounts.sort(compareNumbers).forEach(function(e,t){var n=tree[e],r=[],o=0==t?"if":"else if";code.push(" "+o+" (arguments.length == "+e+") {"),code=code.concat(switchTypes(n,r," ")),code.push(" }"),t==paramCounts.length-1&&(code.push(" else {"),code.push(" throw new TypeError('Wrong number of arguments');"),code.push(" }"))}),code.push(" throw new TypeError('Wrong function signature');"),code.push("}");var factory=[];factory.push("(function (defs) {"),factory=factory.concat(defs.code("defs")),factory=factory.concat(code),factory.push("})");var fn=eval(factory.join("\n"))(defs);return fn.signatures=normalizeSignatures(structure),fn}function getTest(e){var t=compose.types[e];if(!t){var n=Object.keys(compose.types).filter(function(t){return t.toLowerCase()==e.toLowerCase()}).map(function(e){return'"'+e+'"'});throw new Error('Unknown type "'+e+'"'+(n.length?". Did you mean "+n.join(", or ")+"?":""))}return t}return Defs.prototype.add=function(e,t){t=t||"fn",this[t]||(this[t]=[]);var n=this[t].indexOf(e);return-1==n&&(n=this[t].length,this[t].push(e)),t+n},Defs.prototype.code=function(e){var t=this,n=[];return e=e||"defs",Object.keys(this).forEach(function(r){var o=t[r];o.forEach(function(t,o){n.push("var "+r+o+" = "+e+"['"+r+"']["+o+"];")})}),n},compose.types={"null":function(e){return null===e},"boolean":function(e){return"boolean"==typeof e},number:function(e){return"number"==typeof e},string:function(e){return"string"==typeof e},"function":function(e){return"function"==typeof e},Array:function(e){return Array.isArray(e)},Date:function(e){return e instanceof Date},RegExp:function(e){return e instanceof RegExp},Object:function(e){return"object"==typeof e}},compose.conversions=[],compose}); |
# History | ||
## 2014-11-05, version 0.3.0 | ||
- Implemented support for anytype arguments (denoted with `*`). | ||
## 2014-10-23, version 0.2.0 | ||
@@ -5,0 +10,0 @@ |
{ | ||
"name": "function-composer", | ||
"version": "0.2.0", | ||
"version": "0.3.0", | ||
"description": "Compose functions with multiple type signatures", | ||
@@ -26,3 +26,3 @@ "author": "Jos de Jong <wjosdejong@gmail.com> (https://github.com/josdejong)", | ||
"scripts": { | ||
"minify": "uglifyjs function-composer.js -o function-composer.min.js", | ||
"minify": "uglifyjs function-composer.js -o function-composer.min.js -c -m", | ||
"test": "mocha test --recursive", | ||
@@ -29,0 +29,0 @@ "coverage": "istanbul cover _mocha -- test --recursive; echo \"\nCoverage report is available at ./coverage/lcov-report/index.html\"" |
@@ -14,3 +14,3 @@ function-composer | ||
# Load | ||
## Load | ||
@@ -22,3 +22,3 @@ Install via npm: | ||
# Usage | ||
## Usage | ||
@@ -57,3 +57,3 @@ Example usage: | ||
# Performance | ||
## Performance | ||
@@ -67,6 +67,8 @@ Type checking input arguments adds some overhead to a function. For very small | ||
# API | ||
## API | ||
## Construction | ||
### Construction | ||
A function is constructed as: | ||
```js | ||
@@ -77,6 +79,7 @@ compose(signatures: Object.<string, function>) : function | ||
## Properties | ||
### Properties | ||
- `compose.tests: Object` | ||
A map with type checking tests. Add custom types like: | ||
- `compose.types: Object` | ||
A map with the object types as key and a type checking test as value. | ||
Custom types can be added like: | ||
@@ -88,3 +91,3 @@ ```js | ||
compose.tests['Person'] = function (x) { | ||
compose.types['Person'] = function (x) { | ||
return x instanceof Person; | ||
@@ -107,7 +110,32 @@ }; | ||
### Types | ||
# Roadmap | ||
function-composer has the following built-in types: | ||
## Version 1 | ||
- `null` | ||
- `boolean` | ||
- `number` | ||
- `string` | ||
- `function` | ||
- `Array` | ||
- `Date` | ||
- `RegExp` | ||
- `Object` | ||
- `*` (anytype) | ||
### Output | ||
The functions generated with `compose({...})` have: | ||
- A `toString()` function which returns well readable code, giving insight in | ||
what the function exactly does. | ||
- A property `signatures`, which holds a map with the (normalized) | ||
signatures as key and the original sub-functions as value. | ||
## Roadmap | ||
### Version 1 | ||
- Extend function signatures: | ||
@@ -118,5 +146,8 @@ - Any type arguments like `'*, boolean'` | ||
- Multiple types per argument like `number | string, number'` | ||
- Detailed error messages. | ||
- Create a good benchmark, to get insight in the overhead. | ||
- Allow conversions not to be able to convert any input (for example string to | ||
number is not always possible). | ||
## Version 2 | ||
### Version 2 | ||
@@ -129,7 +160,13 @@ - Extend function signatures: | ||
## Test | ||
# Minify | ||
To test the library, run: | ||
npm test | ||
## Minify | ||
To generate the minified version of the library, run: | ||
npm run minify |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Native code
Supply chain riskContains native code (e.g., compiled binaries or shared libraries). Including native code can obscure malicious behavior.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
39380
770
164
1