mirror of
https://github.com/flynx/pWiki.git
synced 2025-10-28 09:30:07 +00:00
547 lines
16 KiB
JavaScript
Executable File
547 lines
16 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
* 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) },
|
|
|
|
// 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){
|
|
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 }}),
|
|
|
|
|
|
// 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 })
|