/********************************************************************** * * * * XXX move .zip(..) here from diff.js * XXX do we need .at(..) / .to(..) methods here and in Map/Set/...??? * * **********************************************/ /* c8 ignore next 2 */ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ var object = require('ig-object') var generator = require('./generator') /*********************************************************************/ // NOTE: this is used in a similar fashion to Python's StopIteration... var STOP = object.STOP //--------------------------------------------------------------------- // Mixins... // Equivalent to .map(..) / .filter(..) / .reduce(..) / .. with support for // STOP... // // NOTE: these add almost no overhead to the iteration. // NOTE: these will not return a partial result if stopped. // // XXX should these return a partial result on STOP? var wrapIterFunc = function(iter){ return function(func){ try { return this[iter](...arguments) } catch(err){ if(err === STOP){ return } else if( err instanceof STOP){ return err.value } throw err } } } // Equivalent to .map(..) / .filter(..) / .reduce(..) that process the // contents in chunks asynchronously... // // .mapChunks(func) // .mapChunks(chunk_size, func) // .mapChunks([item_handler, chunk_handler]) // .mapChunks(chunk_size, [item_handler, chunk_handler]) // -> promise(list) // // .filterChunks(func) // .filterChunks(chunk_size, func) // .filterChunks([item_handler, chunk_handler]) // .filterChunks(chunk_size, [item_handler, chunk_handler]) // -> promise(list) // // .reduceChunks(func, res) // .reduceChunks(chunk_size, func, res) // .reduceChunks([item_handler, chunk_handler], res) // .reduceChunks(chunk_size, [item_handler, chunk_handler], res) // -> promise(res) // // // chunk_handler(chunk, result, offset) // // // chunk_size can be: // 20 - chunk size // '20' - chunk size // '20C' - number of chunks // // // STOP can be thrown in func or chunk_handler at any time to // abort iteration, this will resolve the promise. // // // The main goal of this is to not block the runtime while processing a // very long array by interrupting the processing with a timeout... // // XXX should these return a partial result on STOP? // XXX add generators: // .map(..) / .filter(..) / .reduce(..) // ...the basis here should be the chunks, i.e. each cycle should // go through a chunk... // ...the mixin can be generic, i.e. applicable to Array, and // other stuff... var makeChunkIter = function(iter, wrapper){ wrapper = wrapper || function(res, func, array, e){ return func.call(this, e[1], e[0], array) } return function(size, func, ...rest){ var that = this var args = [...arguments] size = (args[0] instanceof Function || args[0] instanceof Array) ? (this.CHUNK_SIZE || 50) : args.shift() size = typeof(size) == typeof('str') ? // number of chunks... (size.trim().endsWith('c') || size.trim().endsWith('C') ? Math.round(this.length / (parseInt(size) || 1)) || 1 : parseInt(size)) : size var postChunk func = args.shift() ;[func, postChunk] = func instanceof Array ? func : [func] rest = args // special case... // no need to setTimeout(..) if smaller than size... if(this.length <= size){ try { // handle iteration... var res = this[iter](func, ...rest) // handle chunk... postChunk && postChunk.call(this, this, res, 0) return Promise.all(res) // handle STOP... } catch(err){ if(err === STOP){ return Promise.resolve() } else if( err instanceof STOP){ return Promise.resolve(err.value) } throw err } } var res = [] var _wrapper = wrapper.bind(this, res, func, this) return new Promise(function(resolve, reject){ var next = function(chunks){ setTimeout(function(){ var chunk, val try { // handle iteration... res.push( val = (chunk = chunks.shift())[iter](_wrapper, ...rest)) // handle chunk... postChunk && postChunk.call(that, chunk.map(function([i, v]){ return v }), val, chunk[0][0]) // handle STOP... } catch(err){ if(err === STOP){ return resolve() } else if( err instanceof STOP){ return resolve(err.value) } throw err } // stop condition... chunks.length == 0 ? resolve(res.flat(2)) : next(chunks) }, 0) } next(that // split the array into chunks... .reduce(function(res, e, i){ var c = res.slice(-1)[0] c.length >= size ? // initial element in chunk... res.push([[i, e]]) // rest... : c.push([i, e]) return res }, [[]])) }) } } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - var ArrayMixin = module.ArrayMixin = object.Mixin('ArrayMixin', 'soft', { STOP: object.STOP, // zip(array, array, ...) // -> [[item, item, ...], ...] // // zip(func, array, array, ...) // -> [func(i, [item, item, ...]), ...] // zip: function(func, ...arrays){ var i = arrays[0] instanceof Array ? 0 : arrays.shift() if(func instanceof Array){ arrays.splice(0, 0, func) func = null } // build the zip item... // NOTE: this is done this way to preserve array sparseness... var s = arrays .reduce( function(res, a, j){ //a.length > i i in a && (res[j] = a[i]) return res }, new Array(arrays.length)) return arrays // check that at least one array is longer than i... .reduce(function(res, a){ return Math.max(res, i, a.length) }, 0) > i ? // collect zip item... [func ? func(i, s) : s] // get next... .concat(this.zip(func, i+1, ...arrays)) // done... : [] }, iter: function*(lst=[]){ yield* lst.iter() }, }) var ArrayProtoMixin = module.ArrayProtoMixin = object.Mixin('ArrayProtoMixin', 'soft', { // A faster version of .indexOf(..) // // NOTE: this is not faster when looking for an item not in this, // for some reason the native .includes(..) and .indexOf(..) // search for non existant elements about an order of magnirude // faster than if it existed... // ...the funny thing is that at least on Crome .lastIndexOf(..) // is about as fast as this for an item in about the same relative // location... // NOTE: this will get depricated as soon as JS redoes its .indexOf(..) index: function(value){ for(var i = 0; i < this.length && this[i] !== value; i++){} return i == this.length ? -1 : i }, // first/last element access short-hands... // // .first() // .last() // -> elem // // .first(value) // .last(value) // -> array // // NOTE: setting a value will overwrite an existing first/last value. // NOTE: for an empty array both .first(..)/.last(..) will return undefined // when getting a value and set the 0'th value when setting... // NOTE: decided to keep these as methods and not props because methods // have one advantage: they can be chained // ...while you can't chain assignment unless you wrap it in .run(..) first: function(value){ return arguments.length > 0 ? ((this[0] = value), this) : this[0]}, last: function(value){ return arguments.length > 0 ? ((this[this.length - 1 || 0] = value), this) : this[this.length - 1]}, // Roll left/right (in-place)... // // NOTE: to .rol(..) left just pass a negative n value... // NOTE: we can't use ...[..] for sparse arrays as the will expand undefined // inplace of empty positions, this is thereason the .splice(..) // implementation was replaced by a less clear (but faster) // .copyWithin(..) version... rol: function(n=1){ var l = this.length n = (n >= 0 ? n : l - n) % l if(n != 0){ this.length += n this.copyWithin(l, 0, n) this.splice(0, n) } return this }, // Compact a sparse array... // // NOTE: this will not compact in-place. compact: function(){ return this .filter(function(){ return true }) }, // like .length but for sparse arrays will return the element count... get len(){ // NOTE: if we don't do .slice() here this can count array // instance attributes... // NOTE: .slice() has an added menifit here of removing any // attributes from the count... return Object.keys(this.slice()).length }, // Return a new array with duplicate elements removed... // // NOTE: order is preserved... unique: function(normalize){ return normalize ? [...new Map(this.map(function(e){ return [normalize(e), e] })).values()] // NOTE: we are calling .compact() here to avoid creating undefined // items from empty slots in sparse arrays... : [...new Set(this.compact())] }, tailUnique: function(normalize){ return this .slice() .reverse() .unique(normalize) .reverse() }, // Compare two arrays... // // NOTE: this is diffectent from Object.match(..) in that this compares // self to other (internal) while match compares two entities // externally. // XXX not sure if we need the destinction in name, will have to // come back to this when refactoring diff.js -- all three have // to be similar... cmp: function(other){ if(this === other){ return true } if(this.length != other.length){ return false } for(var i=0; i sorted // // Sort as array placing the sorted items at tail... // .sortAs(array, 'tail') // -> sorted // // This will sort the intersecting items in the head keeping the rest // of the items in the same relative order... // // NOTE: if an item is in the array multiple times only the first index // is used... // // XXX should this extend/patch .sort(..)??? // ...currently do not see a clean way to do this without extending // and replacing Array or directly re-wrapping .sort(..)... sortAs: function(other, place='head'){ place = place == 'tail' ? -1 : 1 // NOTE: the memory overhead here is better than the time overhead // when using .indexOf(..)... other = other.toMap() var orig = this.toMap() return this.sort(function(a, b){ var i = other.get(a) var j = other.get(b) return i == null && j == null ? orig.get(a) - orig.get(b) : i == null ? place : j == null ? -place : i - j }) }, // Same as .sortAs(..) but will not change indexes of items not in other... // // Example: // ['a', 3, 'b', 1, 2, 'c'] // .inplaceSortAs([1, 2, 3, 3]) // -> ['a', 1, 'b', 2, 3, 'c'] // inplaceSortAs: function(other){ // sort only the intersection... var sorted = this .filter(function(e){ return other.includes(e) }) .sortAs(other) // "zip" the sorted items back into this... this.forEach(function(e, i, l){ other.includes(e) && (l[i] = sorted.shift()) }) return this }, // Convert an array to object... // // Format: // { // : , // ... // } // // NOTE: items should be strings, other types will get converted to // strings and thus may mess things up. // NOTE: this will forget repeating items... // NOTE: normalize will slow things down... toKeys: function(normalize){ return normalize ? this.reduce(function(r, e, i){ r[normalize(e)] = i return r }, {}) : this.reduce(function(r, e, i){ r[e] = i return r }, {}) }, // Convert an array to a map... // // This is similar to Array.prototype.toKeys(..) but does not restrict // value type to string. // // Format: // Map([ // [, ], // ... // ]) // // NOTE: this will forget repeating items... // NOTE: normalize will slow things down... toMap: function(normalize){ return normalize ? this .reduce(function(m, e, i){ m.set(normalize(e), i) return m }, new Map()) : this .reduce(function(m, e, i){ m.set(e, i) return m }, new Map()) }, // XXX would be nice for this to use the instance .zip(..) in recursion... // ...this might be done by reversign the current implementation, i.e. // for instance .zip(..) to be the main implementation and for // Array.zip(..) to be a proxy to that... zip: function(func, ...arrays){ return func instanceof Array ? this.constructor.zip(this, func, ...arrays) : this.constructor.zip(func, this, ...arrays) }, // get iterator over array... // // Array.iter() // Array.iter([ .. ]) // -> iterator // // array.iter() // -> iterator // // XXX should this take an argument and be like map?? iter: function*(){ yield* this }, // Stoppable iteration... // smap: wrapIterFunc('map'), sfilter: wrapIterFunc('filter'), sreduce: wrapIterFunc('reduce'), sforEach: wrapIterFunc('forEach'), // Chunk iteration... // CHUNK_SIZE: 50, mapChunks: makeChunkIter('map'), filterChunks: makeChunkIter('map', function(res, func, array, e){ return !!func.call(this, e[1], e[0], array) ? [e[1]] : [] }), reduceChunks: makeChunkIter('reduce', function(total, func, array, res, e){ return func.call(this, total.length > 0 ? total.pop() : res, e[1], e[0], array) }), }) ArrayMixin(Array) ArrayProtoMixin(Array.prototype) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })