import * as util from './utils.js'
/**
* Subclass of Array with convenience methods used by NetLogo.
* Typically the items in the array are Objects but can be any type.
*/
class AgentArray extends Array {
/**
* Magic to return AgentArrays rather than AgentList
* or other AgentArray subclasses when using AA methods
* [Symbol.species](https://goo.gl/Zsxwxd)
*
* @readonly
*/
static get [Symbol.species]() {
return AgentArray
}
/**
* Convert an existing Array to an AgentArray "in place".
* Use array.slice() if a new array is wanted
*
* @param {Array} array Array to convert to AgentArray
* @returns {AgentArray} array converted to AgentArray
*/
static fromArray(array) {
const aarray = Object.setPrototypeOf(array, AgentArray.prototype)
return aarray
}
/**
* Creates an instance of AgentArray. Simply pass-through to super()
* now, but may add initialization code later.
* @param {*} args Zero or more items in Array
* @example
* let aa = new AgentArray({x:0,y:0}, {x:0,y:1}, {x:1,y:0})
* //=> [{ x: 0, y: 0 }, { x: 0, y: 1 }, { x: 1, y: 0 }]
*/
constructor(...args) {
super(...args)
// maybe do some initialization later
}
/**
* See {@link World} and [MyClass's foo property]{@link World#bboxTransform}.
* Convert this AgentArray to Array in-place
*
* @returns {Array} This AgentArray converted to Array
*/
toArray() {
Object.setPrototypeOf(this, Array.prototype)
return this
}
// NL: Return true if reporter true for all of this set's objects
// Use Array.every(). Also Array.some()
// all (reporter) { return this.every(reporter) }
// // Return !isEmpty()
// any () { return this.length !== 0 }
// NL: Return AgentArray with reporter(agent) true. Use Array.filter()
// with (reporter) { return this.filter(reporter) }
/**
* Return true if there are no items in this Array
*
* @returns {boolean}
* @example
* new AgentArray().isEmpty()
* //=> true
* @example
* aa.isEmpty()
* //=> false
*/
isEmpty() {
return this.length === 0
}
/**
* Return first item in this array. Returns undefined if empty.
*
* @returns {any}
* @example
* aa.first()
* //=> { x: 0, y: 0 }
*/
first() {
return this[0]
}
/**
* Return last item in this array. Returns undefined if empty.
*
* @returns {any}
* @example
* aa.last()
* //=> { x: 1, y: 0 }
*/
last() {
return this[this.length - 1]
}
/**
* Return at index. Returns undefined if empty.
* Wrap the index to be within the array.
*
* @returns {any}
* @example
* aa.atIndex(aa.length)
* //=> { x: 0, y: 0 }
*/
atIndex(i) {
if (this.length === 0) return undefined
const index = util.mod(i, this.length)
return this[index]
}
/**
* Return true if fcn(element) returns true for each element in this array.
* Same as Array.every, using NetLogo's name
*
* @param {Function} fcn fcn(element) return boolean
* @returns {boolean} true if fcn returns true for all elements
*/
all(fcn) {
return this.every(fcn)
}
/**
* Return array of property values from this array's objects.
* Array type is specified, defaults to AgentArray
*
* @param {String} key Property name
* @param {Array} [type=AgentArray] Type of array (Array, Uint8Array, ...)
* @returns {Array} Array of given type
* @example
* aa.props('x')
* //=> [0, 0, 1]
* @example
* aa.props('y')
* //=> [0, 1, 0]
*/
props(key, type = AgentArray) {
const result = new type(this.length)
for (let i = 0; i < this.length; i++) {
result[i] = this[i][key]
}
return result
}
// Creates an OofA for several sets of props.
// Obj is key, arrayType pairs: x: Float32Array
// Result is this.props(key, arrayType) for each key
/**
* Creates an Object of Arrays, one Array per each property in obj.
* Obj is key, arrayType pairs: x: Float32Array
* This is advanced, used for web workers, very large data sets, and remote communication
*
* @param {Object} obj Object of prop, array type pairs
* @returns {Object}
* @example
* aa.typedSample({x: Uint8Array, y: Uint8Array})
* //=> {x: new Uint8Array([0, 0, 1]), y: new Uint8Array([0, 1, 0])}
*/
typedSample(obj) {
// const length = this.length
const result = {}
// note: use util's forLoop, does not iterate over this agent array.
util.forLoop(obj, (val, key) => {
result[key] = this.props(key, val)
})
return result
}
// Return AgentArray of results of the function fcn
// Similar to "props" but can return computation over all keys
// Odd: as.props('type') twice as fast as as.results(p => p.type)?
// results(fcn) {
// const result = new AgentArray(this.length)
// for (let i = 0; i < this.length; i++) {
// result[i] = fcn(this[i])
// }
// return result
// }
/**
* Return new AgentArray of the unique values of this array
*
* @returns {AgentArray}
*/
uniq() {
// return AgentArray.fromArray(Array.from(new Set(this)))
return AgentArray.from(new Set(this))
}
/**
* Call fcn(agent, index, array) for each item in AgentArray.
* Index & array optional.
* Array assumed not mutable.
* Note: 5x+ faster than this.forEach(fcn)
*
* @param {Function} fcn fcn(agent, [index], [array])
* @returns {this} Return this for chaining.
*/
forLoop(fcn) {
for (let i = 0, len = this.length; i < len; i++) {
fcn(this[i], i, this)
}
return this
}
/**
* Call fcn(agent, [ i, AgentArray ]) for each agent in AgentArray.
* where i = agent's array index and AgentArray is this array
* Array can shrink. If it grows, will not visit beyond original length.
* If it either shrinks or grows, it will console.log a message
* "ask" is NetLogo term.
*
* @param {Function} fcn fcn(agent, [index], [array])
*/
ask(fcn) {
const length = this.length
// for (let i = 0; i < length || i < this.length; i++) {
for (let i = 0; i < Math.min(length, this.length); i++) {
fcn(this[i], i, this)
}
if (length != this.length) {
const name = this.name || this.constructor.name
const direction = this.length < length ? 'decreasing' : 'increasing'
util.warn(`AgentArray.ask array mutation: ${name}: ${direction}`)
}
// return this
}
/**
* Return all elements returning f(obj, [index, array]) true, as in ask()
* NetLogo term, simply calls this.filter(fcn)
*
* @param {Function} fcn fcn(agent, [index], [array])
* @returns {AgentArray}
* @description
* Use: turtles.with(t => t.foo > 20).ask(t => t.bar = true)
*/
with(fcn) {
return this.filter(fcn)
}
// Return all other than me.
other(t) {
return this.filter(o => o !== t)
}
// Return an AgentArray of values for each object in AgentArray
//
// If fcn is a string, it will return that property value.
// Otherwise it returns the value returned from fcn(obj)
getValues(fcn) {
const props = new AgentArray()
if (util.isString(fcn)) {
this.forLoop(obj => props.push(obj[fcn]))
} else {
this.forLoop(obj => props.push(fcn(obj)))
}
return props
}
// Return count of agents with reporter(agent) true
count(reporter) {
return this.reduce((prev, o) => prev + (reporter(o) ? 1 : 0), 0)
}
sum(key) {
return this.reduce((prev, o) => prev + (key ? o[key] : o), 0)
}
avg(key) {
return this.sum(key) / this.length
}
min(key) {
return this.reduce(
(prev, o) => Math.min(prev, key ? o[key] : o),
Infinity
)
}
max(key) {
return this.reduce(
(prev, o) => Math.max(prev, key ? o[key] : o),
-Infinity
)
}
extent(key) {
return [this.min(key), this.max(key)]
}
histogram(key, bins = 10, min = this.min(key), max = this.max(key)) {
const binSize = (max - min) / bins
const aa = new AgentArray(bins)
aa.fill(0)
this.ask(a => {
const val = key ? a[key] : a
if (val < min || val > max) {
util.warn(`histogram bounds error: ${val}: ${min}-${max}`)
} else {
let bin = Math.floor((val - min) / binSize)
if (bin === bins) bin-- // val is max, round down
aa[bin]++
}
})
// Object.assign(aa, {bins, min, max, binSize, key})
aa.parameters = { key, bins, min, max, binSize, arraySize: this.length }
// console.log(key, bins, min, max, binSize, aa)
return aa
}
/**
* Create copy of this AgentArray
* @returns AgentArray
*/
clone() {
return this.slice(0) // Returns an AgentArray rather than Array!
}
// Randomize the AgentArray in place. Use clone first if new AgentArray needed.
// Return "this" for chaining.
shuffle() {
return util.shuffle(this)
}
// Return this AgentArray sorted by the reporter in ascending/descending order.
// If reporter is a string, convert to a fcn returning that property.
// Use clone if you don't want to mutate this array.
/**
* Return this AgentArray sorted by the reporter in ascending/descending order.
* If reporter is a string, convert to a fcn returning that property.
*
* @param {function} reporter
* @param {boolean} [ascending=true]
* @returns {AgentArray}
*/
sortBy(reporter, ascending = true) {
util.sortObjs(this, reporter, ascending)
return this
}
// Remove an item from an array. Binary search if f given
// Array unchanged if item not found.
remove(o, f) {
const i = this.agentIndex(o, f)
if (i !== -1) {
this.splice(i, 1)
} else {
console.log(`remove: ${o.id} not in AgentArray`)
}
return this // chaining
}
insert(o, f) {
const i = this.sortedIndex(o, f)
if (this[i] === o) {
console.log(`insert: item ${o.id} already in AgentArray`)
return
}
this.splice(i, 0, o) // copyWithin?
}
// Binary search:
// Return array index of item, where array is sorted.
// If item not found, return index for item for array to remain sorted.
// f is used to return an integer for sorting, defaults to identity.
// If f is a string, it is the object property to sort by.
// Adapted from underscore's _.sortedIndex.
sortedIndex(item, f = util.identityFcn) {
if (util.isString(f)) f = util.propFcn(f)
const value = f(item)
// Why not array.length - 1? Because we can insert 1 after end of array.
// let [low, high] = [0, array.length]
let low = 0
let high = this.length
while (low < high) {
const mid = (low + high) >>> 1 // floor (low+high)/2
if (f(this[mid]) < value) {
low = mid + 1
} else {
high = mid
}
}
return low
}
// Return index of value in array with given property or -1 if not found.
// Binary search if property isnt null
// Property can be string or function.
// Use property = identity to compare objs directly.
agentIndex(item, property) {
if (!property) return this.indexOf(item)
const i = this.sortedIndex(item, property)
return this[i] === item ? i : -1
}
// True if item is in array. Binary search if f given
contains(item, f) {
return this.agentIndex(item, f) >= 0
}
// Return a random agent. Return undefined if empty.
oneOf() {
return util.oneOf(this)
}
// Return a random agent, not equal to agent
otherOneOf(agent) {
return util.otherOneOf(this, agent)
}
// Return n other random agents from this array
// otherNOf (n, agent) { return util.otherNOf(n, this, agent) }
otherNOf(n, item) {
if (this.length < n) throw Error('AgentArray: otherNOf: length < N')
return this.clone().remove(item).shuffle().slice(0, n)
}
// Return the first agent having the min/max of given value of f(agent).
// If reporter is a string, convert to a fcn returning that property
minOrMaxOf(min, reporter, valueToo = false) {
if (this.isEmpty()) throw Error('min/max OneOf: empty array')
if (typeof reporter === 'string') reporter = util.propFcn(reporter)
let o = null
let val = min ? Infinity : -Infinity
for (let i = 0; i < this.length; i++) {
const a = this[i]
const aval = reporter(a)
if ((min && aval < val) || (!min && aval > val)) {
;[o, val] = [a, aval]
}
}
return valueToo ? [o, val] : o
}
// The min version of the above
minOneOf(reporter) {
return this.minOrMaxOf(true, reporter)
}
// The max version of the above
maxOneOf(reporter) {
return this.minOrMaxOf(false, reporter)
}
// Like the pair above, but return both the object and its value in an array.
// const [obj, value] = minValOf(...)
minValOf(reporter) {
return this.minOrMaxOf(true, reporter, true)
}
maxValOf(reporter) {
return this.minOrMaxOf(false, reporter, true)
}
// Return n random agents as AgentArray.
// See [Fisher-Yates-Knuth shuffle](https://goo.gl/fWNFf)
// for better approach for large n.
nOf(n) {
if (n > this.length)
throw Error(`nOf: n larger than AgentArray: ${n} ${this.length}`)
if (n === this.length) return this
const result = new AgentArray()
while (result.length < n) {
const o = this.oneOf()
if (!(o in result)) result.push(o)
}
return result
}
// Return a new AgentArray of the n min/max agents of the value of reporter,
// in ascending order.
// If reporter is a string, convert to a fcn returning that property
// NOTE: we do not manage ties, see NetLogo docs.
minOrMaxNOf(min, n, reporter) {
if (n > this.length) {
throw Error('min/max nOf: n larger than AgentArray')
}
const as = this.clone().sortBy(reporter)
return min ? as.slice(0, n) : as.slice(as.length - n)
}
minNOf(n, reporter) {
return this.minOrMaxNOf(true, n, reporter)
}
maxNOf(n, reporter) {
return this.minOrMaxNOf(false, n, reporter)
}
}
// // Return shallow copy of a portion of this AgentArray
// // [See Array.slice](https://goo.gl/Ilgsok)
// // Default is to clone entire AgentArray
// cloneRange(begin = 0, end = this.length) {
// return this.slice(begin, end) // Returns an AgentArray rather than Array!
// }
export default AgentArray