types.js/Array.js
Alex A. Naanou 9815415827 added onstop(..) callback to all stoppable funcs...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
2022-12-29 03:34:31 +03:00

584 lines
16 KiB
JavaScript

/**********************************************************************
*
*
*
* 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 stoppable = require('ig-stoppable')
var generator = require('./generator')
/*********************************************************************/
// NOTE: this is used in a similar fashion to Python's StopIteration...
var STOP =
module.STOP =
stoppable.STOP
//---------------------------------------------------------------------
// Mixins...
// Wrap .map(..) / .filter(..) / .reduce(..) / .. to support STOP...
//
// NOTE: internally these are implemented as for-of loops (./generator.js)
var stoppableList = function(iter){
return function(func){
return [...this.iter()[iter](...arguments)] } }
var stoppableValue = function(iter, no_return=false){
return function(func){
var res = this.iter()[iter](...arguments)
return no_return ?
undefined
: res } }
// 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...
// XXX add time-based chunk iteration...
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...
: [] },
// XXX not sure about the handler API here yet...
iter: function*(lst=[], handler=undefined){
yield* lst.iter(
...(handler ?
[handler]
: [])) },
})
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[Math.max(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 }) },
// Remove sprse slots form start/end/both ends of array...
//
trim: function(){
var l = this.length
var i = 0
while(!(i in this) && i < l){ i++ }
var j = 0
while(!(l-j-1 in this) && j < l){ j++ }
return this.slice(i, j == 0 ? l : -j) },
trimStart: function(){
var l = this.length
var i = 0
while(!(i in this) && i < l){ i++ }
return this.slice(i) },
trimEnd: function(){
var l = this.length
var j = 0
while(!(l-j-1 in this) && j < l){ j++ }
return this.slice(0, j == 0 ? l : -j) },
// 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<this.length; i++){
if(this[i] != other[i]){
return false } }
return true },
// Compare two Arrays as sets...
//
// NOTE: this will ignore order and repeating elments...
setCmp: function(other){
return this === other
|| (new Set([...this, ...other])).length
== (new Set(this)).length },
// Sort as the other array...
//
// Sort as array placing the sorted items at head...
// .sortAs(array)
// .sortAs(array, 'head')
// -> 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 }) },
/*/ XXX EXPERIMENTAL...
// ...if this is successful then merge this with .sortAs(..)
sort: function(cmp){
return (arguments.length == 0 || typeof(cmp) == 'function') ?
object.parentCall(ArrayProtoMixin.data.sort, this, ...arguments)
: this.sortAs(...arguments) },
//*/
// 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:
// {
// <item>: <index>,
// ...
// }
//
// 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)] = 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([
// [<item>, <index>],
// ...
// ])
//
// 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), 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) },
// Insert new values between elements of an array
//
// .between(value)
// -> array
//
// .between(func)
// -> array
//
// func([a, b], from_index, to_index, array)
// -> elem
//
between: function(func){
var res = []
// NOTE: we skip the last element...
for(var i=0; i < this.length; i+=1){
var pair = new Array(2)
i in this ?
res.push(pair[0] = this[i])
: (res.length += 1)
if(i+1 >= this.length){
break }
i+1 in this
&& (pair[1] = this[i+1])
res.push(
typeof(func) == 'function' ?
func.call(this, pair, i, res.length, this)
: func) }
return res },
// get iterator over array...
//
// Array.iter()
// Array.iter([ .. ])
// -> iterator
//
// array.iter()
// -> iterator
//
// XXX should this take an argument and be like map??
// XXX this should handle throwing STOP!!!
// ...might also ne a good idea to isolate the STOP mechanics
// into a spearate module/package...
iter: stoppable(
function*(handler=undefined, onstop){
if(handler){
var i = 0
for(var e of this){
var res = handler.call(this, e, i++)
// treat non-iterables as single elements...
if(typeof(res) == 'object'
&& Symbol.iterator in res){
yield* res
} else {
yield res } }
} else {
yield* this }},
// handle stops is onstop(..) is defined...
function(res, _, onstop){
typeof(onstop) == 'function'
&& onstop.call(this,
...(res === STOP ?
[]
: [res])) }),
// Stoppable iteration...
//
// NOTE: internally these are generators...
smap: stoppableList('map'),
sfilter: stoppableList('filter'),
sreduce: stoppableValue('reduce'),
sreduceRight: stoppableValue('reduceRight'),
sforEach: stoppableValue('map', true),
// 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 })