ixfx
Version:
Bundle of ixfx libraries
1,662 lines (1,650 loc) • 208 kB
JavaScript
import { n as __exportAll } from "./chunk-CaR5F9JI.js";
import { $ as addValueMutator, Bt as isPrimitive, Ct as zipKeyValue, Dt as isEqualValueDefault, Et as isEqualDefault, Lt as toStringDefault, Ot as isEqualValueIgnoreOrder, Q as addValueMutate, St as transformMap, X as addObjectEntriesMutate, Z as addValue$1, _t as some, at as findValue, bt as toArray$1, ct as getClosestIntegerKey, dt as hasAnyValue, et as deleteByValueCompareMutate, ft as hasKeyValue, gt as mergeByKey, h as intervalToMs, it as findEntryByValue, jt as defaultComparer, k as toStringAbbreviate, lt as getOrGenerate, mt as mapToObjectTransform, nt as findBySomeKey, ot as fromIterable, pt as mapToArray, rt as findEntryByPredicate, st as fromObject, tt as filterValues, ut as getOrGenerateSync, vt as sortByValue, xt as toObject, yt as sortByValueProperty, z as defaultKeyer } from "./src-BUqDa_u7.js";
import { A as resultIsError, C as numberTest, M as resultThrow, R as throwIfFailed, n as stringTest, u as nullUndefTest, v as integerTest } from "./src-C_hvyftg.js";
import { I as isEqualIgnoreOrder, V as containsDuplicateInstances, r as without } from "./src-CxEyGbiK.js";
import { n as SimpleEventEmitter } from "./src-CRR1VQls.js";
import { B as map, g as last, v as max$1, y as min$1, z as last$1 } from "./src-F3bdGIjS.js";
//#region ../packages/collections/src/circular-array.ts
/**
* A circular array keeps a maximum number of values, overwriting older values as needed. Immutable.
*
* `CircularArray` extends the regular JS array. Only use `add` to change the array if you want
* to keep the `CircularArray` behaviour.
*
* @example Basic functions
* ```js
* let a = new CircularArray(10);
* a = a.add(`hello`); // Because it's immutable, capture the return result of `add`
* a.isFull; // True if circular array is full
* a.pointer; // The current position in array it will write to
* ```
*
* Since it extends the regular JS array, you can access items as usual:
* @example Accessing
* ```js
* let a = new CircularArray(10);
* ... add some stuff ..
* a.forEach(item => // do something with item);
* ```
* @param capacity Maximum capacity before recycling array entries
* @return Circular array
*/
var CircularArray = class CircularArray extends Array {
#capacity;
#pointer;
constructor(capacity = 0) {
super();
resultThrow(integerTest(capacity, `positive`, `capacity`));
this.#capacity = capacity;
this.#pointer = 0;
}
/**
* Add to array
* @param value Thing to add
* @returns
*/
add(value) {
const ca = CircularArray.from(this);
ca[this.#pointer] = value;
ca.#capacity = this.#capacity;
if (this.#capacity > 0) ca.#pointer = this.#pointer + 1 === this.#capacity ? 0 : this.#pointer + 1;
else ca.#pointer = this.#pointer + 1;
return ca;
}
get pointer() {
return this.#pointer;
}
get isFull() {
if (this.#capacity === 0) return false;
return this.length === this.#capacity;
}
};
//#endregion
//#region ../packages/collections/src/sorted-array.ts
/**
* Returns first index of data in an ascended sorted array using a binary search.
* Returns -1 if data was not found.
* ```js
* indexOf([1,2,3], 3); // 2
* indexOf([1,2,3], 0); // -1, not found
* ```
*
* By default uses Javascript comparision semantics.
* Passing in `comparer` is needed when working with an array of objects.
* @param data Array of data
* @param sought Item to search for
* @param comparer Comparer (by default uses JS semantics)
* @returns Index of sought item or -1 if not found.
*/
function indexOf(data, sought, comparer = defaultComparer) {
const range = matchingRange(data, sought, comparer);
if (!range) return -1;
return range.startIndex;
}
/**
* Returns the lower bound index for an item in a sorted array using a binary search.
* This is the index at which the item would be inserted to maintain sorted order, and is to the left of any existing entries in the case of equal values.
*
* ```js
* const data = [2, 8, 16, 32];
* Sorted.lowerBound(data, 1); // 0
* Sorted.lowerBound(data, 2); // 0
* Sorted.lowerBound(data, 3); // 1
* Sorted.lowerBound(data, 320); // 4
* ```
*
* If item is past the end of the array, the length of the array is returned (which is the index at which the item would be inserted to maintain sorted order).
*
* @param sortedList Sorted list
* @param item Item
* @param comparer Comparer
* @returns Index
*/
function lowerBound(sortedList, item, comparer = defaultComparer) {
let left = 0;
let right = sortedList.length;
while (left < right) {
const mid = left + right >> 1;
if (comparer(sortedList[mid], item) < 0) left = mid + 1;
else right = mid;
}
return left;
}
/**
* Returns the upper bound index for an item in a sorted array using a binary search.
* This is the index at which the item would be inserted to maintain sorted order, and is to the right of any existing entries in the case of equal values.
* ```js
* const data = [2, 8, 16, 32];
* Sorted.upperBound(data, 1); // 0
* Sorted.upperBound(data, 2); // 1
* Sorted.upperBound(data, 3); // 1
* ```
* It the item is past the end of the array, the length of the array is returned (which is the index at which the item would be inserted to maintain sorted order).
* @param sortedList
* @param item
* @param comparer
*/
function upperBound(sortedList, item, comparer = defaultComparer) {
let left = 0;
let right = sortedList.length;
while (left < right) {
const mid = left + right >> 1;
if (comparer(sortedList[mid], item) <= 0) left = mid + 1;
else right = mid;
}
return left;
}
/**
* Returns the index range of `sortedList` where item(s) equal to `item` are found. Returns `undefined`
* if `item` was not found.
*
* ```js
* const data = [2, 8, 16, 16, 32];
* Sorted.matchingRange(data, 1); // _undefined_
* Sorted.matchingRange(data, 8); // { startIndex: 1, endIndex: 1 }
* Sorted.matchingRange(data, 16); // { startIndex: 2, endIndex: 3 }
* ```
* @param sortedList
* @param item
* @param comparer
* @returns Matching range of `item`, or _undefined_ if not found.
*/
function matchingRange(sortedList, item, comparer = defaultComparer) {
if (sortedList.length === 0) return;
const start = lowerBound(sortedList, item, comparer);
if (start >= sortedList.length || comparer(sortedList[start], item) !== 0) return;
return {
startIndex: start,
endIndex: upperBound(sortedList, item, comparer) - 1
};
}
/**
* Returns index to insert data into a sorted array using a binary search.
* Adds to the right of existing entries in the case of equal values.
*
* By default uses Javascript comparision semantics.
* Passing in `comparer` is needed when working with an array of objects.
* @param data
* @param toInsert
* @param comparer
*/
function insertionIndex(data, toInsert, comparer = defaultComparer) {
if (typeof comparer !== `function`) throw new TypeError(`Param 'comparer' is not a function`);
if (!Array.isArray(data)) throw new TypeError(`Param 'data' is not an array`);
return upperBound(data, toInsert, comparer);
}
//#endregion
//#region ../packages/collections/src/events/events-fns.ts
/**
* Sorts by start, such that 'start' values are ascending.
*
* Returns:
* 0 if A and B are have same start & end.
* positive if B is before A.
* negative if B is after A.
*
* If A and B have the same start point, they are secondarily sorted based on end time, with earlier end time considered "before" later end time.
*
* Use {@link CompareByStartOnly} to ignore end time and consider events equal if they share a `start`.
* @param a
* @param b
*/
const CompareByStart = (a, b) => {
if (typeof a === `undefined`) throw new TypeError(`Param 'a' is undefined`);
if (typeof b === `undefined`) throw new TypeError(`Param 'b' is undefined`);
if (a.start === b.start) return a.end - b.end;
return a.start - b.start;
};
const CompareByStartOnly = (a, b) => {
if (typeof a === `undefined`) throw new TypeError(`Param 'a' is undefined`);
if (typeof b === `undefined`) throw new TypeError(`Param 'b' is undefined`);
return a.start - b.start;
};
/**
* Sorts by end, such that 'end' values are ascending.
*
* Returns:
* 0 if A and B are have same start & end.
* Returns positive if B is before A.
* Returns negative if B is after A.
*
* If A and B share the same end, shorter items will come first (ie. those with higher start)
* @param a
* @param b
*/
const CompareByEnd = (a, b) => {
if (typeof a === `undefined`) throw new TypeError(`Param 'a' is undefined`);
if (typeof b === `undefined`) throw new TypeError(`Param 'b' is undefined`);
if (a.end === b.end) return a.start - b.start;
return a.end - b.end;
};
const CompareByEndOnly = (a, b) => {
if (typeof a === `undefined`) throw new TypeError(`Param 'a' is undefined`);
if (typeof b === `undefined`) throw new TypeError(`Param 'b' is undefined`);
return a.end - b.end;
};
/**
* Returns a new array of events ordered by their start time (ascending)
* @param events
* @returns Events ordered by start time
*/
function sortByStart(events) {
return events.toSorted(CompareByStart);
}
/**
* Returns a new array of events ordered by their end time (ascending)
* @param events
* @returns Events ordered by end time
*/
function sortByEnd(events) {
return events.toSorted(CompareByEnd);
}
/**
* Yields every item in `sortedEvents` that has the specified `start` value
*
* Return item is a wrapped object consisting of the event as well as its index.
* ```js
* const events = [ { start: 1, end: 2}, { start: 5, end: 10 }, { start: 10, end: 12 }];
* const matched = [...itemsWithStart(events, 5)];
* // matched is [{ event: { start: 5, end: 10 }, index: 1 }]
* ```
* @param sortedEvents Sorted events
* @param start Start position
*/
function* itemsWithStart(sortedEvents, start) {
if (!Array.isArray(sortedEvents)) throw new TypeError(`Param 'sortedEvents' is not an array`);
const range = matchingRange(sortedEvents, {
start,
end: start
}, CompareByStartOnly);
if (!range) return;
for (let i = range.startIndex; i <= range.endIndex; i++) yield {
event: sortedEvents[i],
index: i
};
}
/**
* Yields every item in `eventsByEnd` that has the specified `end` value.
* Return item is a wrapped object consisting of the event as well as its index.
*
* The function expects that the input array has been sorted using {@link sortByEnd}, and therefore
* sorted by ascending end value.
*
* ```js
* const events = [ { start: 1, end: 2}, { start: 5, end: 10 }, { start: 10, end: 12 }];
* const matched = [...itemsWithEnd(events, 10)];
* // matched is [{ event: { start: 5, end: 10 }, index: 1 }]
* ```
* @param eventsByEnd Events sorted with {@link sortByEnd}
* @param end End position
*/
function* itemsWithEnd(eventsByEnd, end) {
if (!Array.isArray(eventsByEnd)) throw new TypeError(`Param 'eventsByEnd' is not an array`);
const range = matchingRange(eventsByEnd, {
start: end,
end
}, CompareByEndOnly);
if (!range) return;
for (let i = range.startIndex; i <= range.endIndex; i++) yield {
event: eventsByEnd[i],
index: i
};
}
/**
* Converts a collection of `IndexedEventItem` back into an array of `EventItem`, placing items at their original index.
*
* ```js
* // Get all items that start at position 5
* const itemsAtPosition = [...itemsWithStart(sortedEvents, 5)];
*
* // Make this into an array:
* const items = arrayFromItems(itemsAtPosition);
* ```
*
* By default, the `index` field is used to construct the array. If `ignoreIndexes` is set to _true_,
* the the returned array is constructed in the order of the input items, ignoring the `index` field. This can be useful if you just want to extract the events from a generator without caring about their original position.
* @param items
* @returns EventItems
*/
function arrayFromItems(items, ignoreIndexes = false) {
const result = [];
if (ignoreIndexes) for (const item of items) result.push(item.event);
else for (const { event, index } of items) result[index] = event;
return result;
}
/**
* Yields all events that overlap with `point`.
* By default event end is considered exclusive, meaning that if `point == event.end`, it is not considered overlapping.
* If `endInclusive` is true, event end is considered inclusive, and the aforementioned would be considered ovlerapping.
*
* By default start is inclusive.
*
* @param sortedEvents
* @param point
* @param endInclusive Whether event end is considered inclusive for determining overlap (default:false)
* @param startInclusive Whether event start is considered inclusive for determining overlap (default:true)
*/
function* overlapping(sortedEvents, point, endInclusive = false, startInclusive = true) {
for (let index = 0; index < sortedEvents.length; index++) {
const event = sortedEvents[index];
const start = startInclusive ? point >= event.start : point > event.start;
if ((endInclusive ? point <= event.end : point < event.end) && start) yield {
event,
index
};
}
}
/**
* Inserts space within `sortedEvents`. It does this by shifting events forward.
*
* If `start` overlaps with existing item(s), `overlappingPolicy` is used:
* - 'ignore': Event duration is not changed
* - 'stretch': Events that overlap are stretched by `length`.
*
* When considering overlap, both end is exclusive and start are considered exclusive.
*
* Eg, if we have the event `{ start: 5, end: 10 }`.
* - insertSpace(data, 5, 1, `ignore`); // Would not be considered overlapping, but event would be shifted to { start: 6, end: 11 }
* - insertSpace(data, 10, 1, `ignore`); // Would not be considered overlapping, event would remain { start: 5, end: 10 }
* - insertSpace(data, 5, 1, `stretch`); // Would be considered overlapping, event shifted to { start: 6, end: 11 }
* - insertSpace(data, 6, 1, `stretch`); // Would be considered overlapping, event would be stretched to { start: 5, end: 11 }
* @param sortedEvents
* @param start
* @param length
* @param overlappingPolicy
*/
function insertSpace(sortedEvents, start, length, overlappingPolicy) {
let i = insertionIndex(sortedEvents, {
start,
end: start
}, CompareByStart);
if (overlappingPolicy === `stretch`) {
const events = [...sortedEvents];
for (const { event, index } of overlapping(sortedEvents, start, false, false)) events[index] = {
...event,
end: event.end + length
};
sortedEvents = events;
}
while (sortedEvents[i - 1]?.start === start) i--;
const pre = sortedEvents.slice(0, i);
const post = sortedEvents.slice(i).map((event) => translate(event, length));
const result = [...pre, ...post];
if (result.length !== sortedEvents.length) throw new Error(`Bug in insertSpace: result length ${result.length} does not match original length ${sortedEvents.length}`);
return result;
}
/**
* Punches a hole in `sortedEvents` which overlap `hole`.
* It does this by splitting/trimming events, or removing an event entirely it is fully covered by the hole.
*
* This will never shift events in time.
* @param sortedEvents
* @param hole
*/
function holepunch(sortedEvents, hole) {
throwIfFailed(isValid(hole));
const result = [];
for (const e of sortedEvents) {
const c = compareRange(e, hole);
if (c === `none`) {
result.push(e);
continue;
}
if (c === `equal`) continue;
if (c === `full-border`) {
if (e.start < hole.start) result.push({
...e,
end: hole.start
});
else if (e.end > hole.end) result.push({
...e,
start: hole.end
});
continue;
}
if (c === `full`) {
result.push({
...e,
end: hole.start
});
result.push({
...e,
start: hole.end
});
continue;
}
if (c === `partial`) {
if (e.start < hole.start) result.push({
...e,
end: hole.start
});
else if (e.end > hole.end) result.push({
...e,
start: hole.end
});
continue;
}
}
return result;
}
/**
* Returns _true_ if `item` has zero duration (start and end are the same), _false_ otherwise.
* ```js
* isEmpty({ start: 1, end: 1 }); // true
* isEmpty({ start: 1, end: 2 }); // false
* ```
* @param item
* @returns _true_ if `item` is empty.
*/
function isEmpty$2(item) {
return item.start === item.end;
}
/**
* Creates an `EventItem` from an `EventItemAsDuration` by calculating the end as start + duration.
* ```js
* fromDuration({ start: 1, duration: 2 }); // { start: 1, end: 3 }
* ```
*
* Copies additional properties to the return result.
* @param item
* @returns EventItem
*/
function fromDuration(item) {
return {
...item,
start: item.start,
end: item.start + item.duration
};
}
/**
* Creats an `EventItemAsDuration` from an `EventItem` by calculating the duration as end - start.
* ```js
* toDuration({ start: 1, end: 3 }); // { start: 1, duration: 2 }
* ```
*
* Copies additional properties to the return result.
* @param item
* @returns EventItemAsDuration
*/
function toDuration(item) {
return {
...item,
start: item.start,
duration: item.end - item.start
};
}
/**
* Returns the intervals between pairs of events.
*
* If `sortedEvents` has less than two events, yields nothing
* @param sortedEvents
*/
function* intervals(sortedEvents) {
if (sortedEvents.length <= 1) return;
for (let index = 1; index < sortedEvents.length; index++) {
const prev = sortedEvents[index - 1];
const current = sortedEvents[index];
yield {
indexA: index - 1,
indexB: index,
a: prev,
b: current,
startInterval: current.start - prev.start,
endInterval: current.end - prev.end,
betweenInterval: current.start - prev.end
};
}
}
function isValid(item) {
if (!isEventItem(item)) return {
success: false,
error: `Item is not a valid EventItem`
};
if (item.end < item.start) return {
success: false,
error: `Invalid EventItem. End (${item.end}) is before start (${item.start})`
};
return {
success: true,
value: item
};
}
function isEventItem(item) {
if (typeof item !== `object`) return false;
if (item === null || item === void 0) return false;
return `start` in item && `end` in item && typeof item.start === `number` && typeof item.end === `number`;
}
/**
* Removes `toRemove` from `sortedEvents`.
*
* Consider {@link holepunch} if you want to create an empty hole in the events and maintain overall length of event series.
*
* After removing:
* - 'nothing': Gap is left, other items not affected
* - 'shuffle-following': Events after 'toRemove' are shifted back by duration of `toRemove`, maintaining their spacing after that
* - 'shuffle-leading': Events before 'toRemove' are shifted forward by duration of `toRemove`, maintaining their spacing before that
* - 'slice-following': Events after 'toRemove' are shifted back to start at `toRemoved.start`, maintaining their spacing after that
* - 'slice-leading': Events before 'toRemove' are shifted forward to end at `toRemoved.end`, maintaining their spacing before that
* @param sortedEvents
* @param toRemove
*/
function remove$2(sortedEvents, toRemove, andThen) {
const i = indexOf(sortedEvents, toRemove, CompareByStart);
if (i === -1) return sortedEvents;
if (andThen === `shuffle-following`) return [...sortedEvents.slice(0, i), ...sortedEvents.slice(i + 1).map((event) => translate(event, toRemove.start - toRemove.end))];
else if (andThen === `slice-following`) {
const shiftAmount = toRemove.start - sortedEvents[i + 1].start;
return [...sortedEvents.slice(0, i), ...sortedEvents.slice(i + 1).map((event) => translate(event, shiftAmount))];
} else if (andThen === `shuffle-leading`) return [...sortedEvents.slice(0, i).map((event) => translate(event, toRemove.end - toRemove.start)), ...sortedEvents.slice(i + 1)];
else if (andThen === `slice-leading`) {
const shiftAmount = toRemove.end - sortedEvents[i - 1].end;
return [...sortedEvents.slice(0, i).map((event) => translate(event, shiftAmount)), ...sortedEvents.slice(i + 1)];
}
return sortedEvents.toSpliced(i, 1);
}
/**
* Splits `event` into two events by either a percentange of duration or by a specific start position.
*
* If `options` has a `percentage` field, the split point is calculated as `event.start + (event.end - event.start) * percentage`.
* If `options` has a `start` field, the split point is simply that value.
*
* ```js
* splitEvent({ start: 0, end: 10 }, { percentage: 0.5 }); // [{ start: 0, end: 5 }, { start: 5, end: 10 }]
* splitEvent({ start: 0, end: 10 }, { start: 3 }); // [{ start: 0, end: 3 }, { start: 3, end: 10 }]
* ```
*
* Any other properties on `event` are copied to split events.
* @param event Input event
* @param options How to split
* @returns Split event
*/
function splitEvent(event, options) {
if (`percentage` in options) {
const splitPoint = event.start + (event.end - event.start) * options.percentage;
return [{
...event,
start: event.start,
end: splitPoint
}, {
...event,
start: splitPoint,
end: event.end
}];
} else {
const splitPoint = options.start;
return [{
...event,
start: event.start,
end: splitPoint
}, {
...event,
start: splitPoint,
end: event.end
}];
}
}
/**
* Applies `fn` to both `start` and `end` fields, returning a new event.
*
* Existing data on `event` is maintained.
*
* ```js
* applyToPosition( { start:1.2, end:2.4 }, v => Math.round(v)); // { start:1, end:2 }
* applyToPosition( { start:1, end:2 }, v => v*2); // { start:2, end:4 }
* ```
*
* Use {@link translate} if you just want to add an amount to start and end, instead of applying a custom function.
* @param event Input event
* @param fn Function to run over start and end
* @returns New event with `fn` applied to start and end
*/
function applyToPositions(event, fn) {
return {
...event,
start: fn(event.start),
end: fn(event.end)
};
}
/**
* Translates an event by adding `amount` to both `start` and `end`, returning a new event.
* ```js
* translate( { start:1, end:2 }, 3); // { start:4, end:5 }
* translate( { start:1, end:2 }, -1); // { start:0, end:1 }
* ```
*
* Existing data on `event` is maintained.
*
* Use {@link applyToPositions} if you want to apply a custom function to the start and end, instead of just adding an amount.
* @param event
* @param amount
* @returns New EventItem
*/
function translate(event, amount) {
return {
...event,
start: event.start + amount,
end: event.end + amount
};
}
/**
* Returns how `b` overlaps with `a`.
*
* Returns:
* - `none` if `b` does not overlap with `a`
* - `equal` if `b` has the same start and end as `a`
* - `full` if `b` is fully contained within `a` and `a` does not share a start/end
* - `full-border` if `b` is fully contained within `a` and `a` shares a start/end
* - `partial` if `b` overlaps with `a` but is not fully contained within it
*
* ```js
* compareRange({ start:2, end:4 }, { start:0, end:1 }); // 'none'
* compareRange({ start:2, end:4 }, { start: 2, end:4 }); // 'equal'
* compareRange({ start:2, end:4 }, { start: 3, end: 3 }); // 'full'
* compareRange({ start:2, end:4 }, { start: 3, end: 4 }); // 'full-border'
* compareRange({ start:2, end:4 }, { start: 1, end: 3 }); // 'partial'
* ```
* @param a
* @param b
*/
function compareRange(a, b) {
if (b.start > a.end) return `none`;
if (b.end < a.start) return `none`;
if (b.start === a.start && b.end === a.end) return `equal`;
if (b.start > a.start && b.end < a.end) return `full`;
if (b.start >= a.start && b.end <= a.end) return `full-border`;
return `partial`;
}
/**
* Lays out events end-to-end, removing gaps between them and having the first start at 0.
* Duration of events is maintained.
* @param sortedEvents
*/
function defragment(sortedEvents, options = {}) {
let time = options.startAt ?? 0;
const gap = options.gap ?? 0;
const results = [];
for (const event of sortedEvents) {
results.push({
...event,
start: time,
end: time + (event.end - event.start)
});
time = results.at(-1).end + gap;
}
return results;
}
function createFromStarts(starts, duration, idPrefix = `event`) {
return starts.map((start, i) => ({
start,
end: start + duration,
id: `${idPrefix}-${i}`
}));
}
/**
* Gets the range of `events`: the smallest 'start' and the largest 'end'.
*
* If there are gaps between events, this is still included in the range. Use {@link sumDuration}
* to add up the duration of all events as if they are stacked end-to-end.
* @param events
* @returns Range of events
*/
function computeRange(events) {
let minStart = Number.MAX_SAFE_INTEGER;
let maxEnd = Number.MIN_SAFE_INTEGER;
for (const event of events) {
if (event.start < minStart) minStart = event.start;
if (event.end > maxEnd) maxEnd = event.end;
}
return {
start: minStart,
end: maxEnd
};
}
/**
* Returns the total duration of all events. Doesn't take into account
* the spacing between events, just sums the duration of each one.
*
* Use {@link computeRange} if you want to calculate the min and max starting points.
* @param events
* @returns Duration
*/
function sumDuration(events) {
return events.reduce((sum, event) => sum + (event.end - event.start), 0);
}
//#endregion
//#region ../packages/collections/src/stack/StackFns.ts
const trimStack = (opts, stack, toAdd) => {
const potentialLength = stack.length + toAdd.length;
const policy = opts.discardPolicy ?? `additions`;
const capacity = opts.capacity ?? potentialLength;
const toRemove = potentialLength - capacity;
if (opts.debug) console.log(`Stack.push: stackLen: ${stack.length} potentialLen: ${potentialLength} toRemove: ${toRemove} policy: ${policy}`);
switch (policy) {
case `additions`:
if (opts.debug) console.log(`Stack.push:DiscardAdditions: stackLen: ${stack.length} slice: ${potentialLength - capacity} toAddLen: ${toAdd.length}`);
if (stack.length === opts.capacity) return stack;
else return [...stack, ...toAdd.slice(0, toAdd.length - toRemove)];
case `newer`: {
if (toAdd.length >= capacity) return toAdd.slice(toAdd.length - capacity);
const keepFromOld = capacity - toAdd.length;
return [...stack.slice(0, keepFromOld), ...toAdd];
}
case `older`: return [...stack, ...toAdd].slice(toRemove);
default: throw new Error(`Unknown discard policy ${policy}`);
}
};
const push = (opts, stack, ...toAdd) => {
const potentialLength = stack.length + toAdd.length;
return opts.capacity && potentialLength > opts.capacity ? trimStack(opts, stack, toAdd) : [...stack, ...toAdd];
};
const pop = (opts, stack) => {
if (stack.length === 0) throw new Error(`Stack is empty`);
return stack.slice(0, -1);
};
/**
* Peek at the top of the stack (end of array)
*
* @typeParam V - Type of stored items
* @param {StackOpts} opts
* @param {V[]} stack
* @returns {(V | undefined)}
*/
const peek$1 = (opts, stack) => stack.at(-1);
const isEmpty$1 = (opts, stack) => stack.length === 0;
const isFull$1 = (opts, stack) => {
if (opts.capacity) return stack.length >= opts.capacity;
return false;
};
//#endregion
//#region ../packages/collections/src/stack/StackMutable.ts
/**
* Creates a stack. Mutable. Use {@link StackImmutable} for an immutable alternative.
*
* @example Basic usage
* ```js
* // Create
* const s = new StackMutable();
* // Add one or more items
* s.push(1, 2, 3, 4);
*
* // See what's on top
* s.peek; // 4
*
* // Remove the top-most, and return it
* s.pop(); // 4
*
* // Now there's a new top-most element
* s.peek; // 3
* ```
*/
var StackMutable = class {
opts;
data;
/**
* Create a new StackMutable.
*
* @param opts Options
* @param data Initial data to use
*/
constructor(opts = {}, data = []) {
this.opts = opts;
this.data = data;
}
/**
* Push data onto the stack.
* If `toAdd` is empty, nothing happens
* @param toAdd Data to add
* @returns Length of stack
*/
push(...toAdd) {
if (toAdd.length === 0) return this.data.length;
this.data = push(this.opts, this.data, ...toAdd);
return this.data.length;
}
/**
* Iterate from bottom to top. Note that `forEachFromTop` iterates from top to bottom, so this is the reverse.
* @param fn
*/
forEach(fn) {
this.data.forEach(fn);
}
/**
* Iterate from top to bottom. Note that `forEach` iterates from bottom to top, so this is the reverse.
* @param fn
*/
forEachFromTop(fn) {
[...this.data].reverse().forEach(fn);
}
/**
* Pop the top-most item from the stack, and return it. If the stack is empty, returns _undefined_.
* @returns
*/
pop() {
const v = peek$1(this.opts, this.data);
this.data = pop(this.opts, this.data);
return v;
}
/**
* Returns _true_ if the stack is empty, _false_ otherwise.
*/
get isEmpty() {
return isEmpty$1(this.opts, this.data);
}
/**
* Returns _true_ if the stack is full, _false_ otherwise. Note that a stack is only full if a `maxSize` option was provided at construction time.
*/
get isFull() {
return isFull$1(this.opts, this.data);
}
/**
* Returns the top-most item on the stack, without modifying the stack. If the stack is empty, returns _undefined_.
*/
get peek() {
return peek$1(this.opts, this.data);
}
/**
* Returns the number of items currently on the stack.
*/
get length() {
return this.data.length;
}
};
/**
* Creates a stack. Mutable. Use {@link Stacks.immutable} for an immutable alternative.
*
* @example Basic usage
* ```js
* // Create
* const s = Stacks.mutable();
* // Add one or more items
* s.push(1, 2, 3, 4);
*
* // See what's on top
* s.peek; // 4
*
* // Remove the top-most, and return it
* s.pop(); // 4
*
* // Now there's a new top-most element
* s.peek; // 3
* ```
*/
const mutable$3 = (opts = {}, ...startingItems) => new StackMutable({ ...opts }, [...startingItems]);
//#endregion
//#region ../packages/collections/src/queue/queue-fns.ts
const debug = (opts, message) => {
opts.debug && console.log(`queue:${message}`);
};
const trimQueue = (opts, queue, toAdd) => {
const potentialLength = queue.length + toAdd.length;
const capacity = opts.capacity ?? potentialLength;
const toRemove = potentialLength - capacity;
const policy = opts.discardPolicy ?? `additions`;
switch (policy) {
case `additions`:
if (queue.length === 0) return toAdd.slice(0, toAdd.length - toRemove);
if (queue.length === opts.capacity) return queue;
else return [...queue, ...toAdd.slice(0, toRemove - 1)];
case `newer`: if (toRemove >= queue.length) {
if (queue.length === 0) return [...toAdd.slice(0, capacity - 1), toAdd.at(-1)];
return toAdd.slice(Math.max(0, toAdd.length - capacity), Math.min(toAdd.length, capacity) + 1);
} else {
const countToAdd = Math.max(1, toAdd.length - queue.length);
const toAddFinal = toAdd.slice(toAdd.length - countToAdd, toAdd.length);
return [...queue.slice(0, Math.min(queue.length, capacity - 1)), ...toAddFinal];
}
case `older`: return [...queue, ...toAdd].slice(toRemove);
default: throw new Error(`Unknown overflow policy ${policy}`);
}
};
/**
* Adds to the back of the queue (last array index)
* Last item of `toAdd` will potentially be the new end of the queue (depending on capacity limit and overflow policy)
* @typeParam V - Type of values
* @param {QueueOpts} opts
* @param {V[]} queue
* @param {...V[]} toAdd
* @returns {V[]}
*/
const enqueue = (opts, queue, ...toAdd) => {
if (opts === void 0) throw new Error(`opts parameter undefined`);
const potentialLength = queue.length + toAdd.length;
const overSize = opts.capacity && potentialLength > opts.capacity;
const toReturn = overSize ? trimQueue(opts, queue, toAdd) : [...queue, ...toAdd];
if (opts.capacity && toReturn.length !== opts.capacity && overSize) throw new Error(`Bug! Expected return to be at capacity. Return len: ${toReturn.length} capacity: ${opts.capacity} opts: ${JSON.stringify(opts)}`);
if (!opts.capacity && toReturn.length !== potentialLength) throw new Error(`Bug! Return length not expected. Return len: ${toReturn.length} expected: ${potentialLength} opts: ${JSON.stringify(opts)}`);
return toReturn;
};
const dequeue = (opts, queue) => {
if (queue.length === 0) throw new Error(`Queue is empty`);
return queue.slice(1);
};
/**
* Returns front of queue (oldest item), or undefined if queue is empty
*
* @typeParam V - Type of values stored
* @param {QueueOpts} opts
* @param {V[]} queue
* @returns {(V | undefined)}
*/
const peek = (opts, queue) => queue[0];
const isEmpty = (opts, queue) => queue.length === 0;
const isFull = (opts, queue) => {
if (opts.capacity) return queue.length >= opts.capacity;
return false;
};
//#endregion
//#region ../packages/collections/src/queue/queue-mutable.ts
/**
* Mutable queue that fires events when manipulated.
*
* Queues are useful if you want to treat 'older' or 'newer'
* items differently. _Enqueing_ adds items at the back of the queue, while
* _dequeing_ removes items from the front (ie. the oldest).
*
* ```js
* const q = Queues.mutable(); // Create
* q.enqueue(`a`, `b`); // Add two strings
* const front = q.dequeue(); // `a` is at the front of queue (oldest)
* ```
*
* @example Cap size to 5 items, throwing away newest items already in queue.
* ```js
* const q = Queues.mutable({capacity: 5, discardPolicy: `newer`});
* ```
*
* Events can be used to monitor data flows.
* * 'enqueue': fires when item(s) are added
* * 'dequeue': fires when an item is dequeued from front
* * 'removed': fires when an item is dequeued, queue is cleared or .removeWhere is used to trim queue
*
* Each of the event handlers return the state of the queue as the 'finalData'
* field.
*
* ```js
* q.addEventListener(`enqueue`, e => {
* // e.added, e.finalData
* });
* q.addEventListener(`removed`, e => {
* // e.removed, e.finalData
* });
* q.addEventListener(`dequeue`, e=> {
* // e.removed, e.finalData
* })
* ```
* @typeParam V - Data type of items
*/
var QueueMutable = class extends SimpleEventEmitter {
options;
data;
eq;
constructor(opts = {}, data = []) {
super();
if (opts === void 0) throw new Error(`opts parameter undefined`);
this.options = opts;
this.data = data;
this.eq = opts.eq ?? isEqualDefault;
}
clear() {
const copy = [...this.data];
this.data = [];
this.fireEvent(`removed`, {
finalData: this.data,
removed: copy
});
this.onClear();
}
/**
* Called when all data is cleared
*/
onClear() {}
at(index) {
if (index >= this.data.length) throw new Error(`Index outside bounds of queue`);
const v = this.data.at(index);
if (v === void 0) throw new Error(`Index appears to be outside range of queue`);
return v;
}
enqueue(...toAdd) {
this.data = enqueue(this.options, this.data, ...toAdd);
const length = this.data.length;
this.onEnqueue(this.data, toAdd);
return length;
}
onEnqueue(result, attemptedToAdd) {
this.fireEvent(`enqueue`, {
added: attemptedToAdd,
finalData: result
});
}
dequeue() {
const v = peek(this.options, this.data);
if (v === void 0) return;
this.data = dequeue(this.options, this.data);
this.fireEvent(`dequeue`, {
removed: v,
finalData: this.data
});
this.onRemoved([v], this.data);
return v;
}
onRemoved(removed, finalData) {
this.fireEvent(`removed`, {
removed,
finalData
});
}
/**
* Removes values that match `predicate`.
* @param predicate
* @returns Returns number of items removed.
*/
removeWhere(predicate) {
const countPre = this.data.length;
const toRemove = this.data.filter((v) => predicate(v));
if (toRemove.length === 0) return 0;
this.data = this.data.filter((element) => !predicate(element));
this.onRemoved(toRemove, this.data);
return countPre - this.data.length;
}
/**
* Return a copy of the array
* @returns
*/
toArray() {
return [...this.data];
}
get isEmpty() {
return isEmpty(this.options, this.data);
}
get isFull() {
return isFull(this.options, this.data);
}
get length() {
return this.data.length;
}
get peek() {
return peek(this.options, this.data);
}
};
/**
* Creates a new QueueMutable
* @param options
* @param startingItems
* @returns
*/
function mutable$2(options = {}, ...startingItems) {
return new QueueMutable({ ...options }, [...startingItems]);
}
//#endregion
//#region ../packages/collections/src/queue/priority-mutable.ts
/**
* Simple priority queue implementation.
* Higher numbers mean higher priority.
*
* ```js
* const pm = new PriorityMutable();
*
* // Add items with a priority (higher numeric value = higher value)
* pm.enqueueWithPriority(`hello`, 4);
* pm.enqueueWithPriotity(`there`, 1);
*
* ```
*/
var PriorityMutable = class extends QueueMutable {
constructor(opts = {}) {
if (opts.eq === void 0) opts = {
...opts,
eq: (a, b) => {
return isEqualDefault(a.item, b.item);
}
};
super(opts);
}
/**
* Adds an item with a given priority
* @param item Item
* @param priority Priority (higher numeric value means higher priority)
*/
enqueueWithPriority(item, priority) {
resultThrow(numberTest(priority, `positive`));
super.enqueue({
item,
priority
});
}
changePriority(item, priority, addIfMissing = false, eq) {
if (item === void 0) throw new Error(`Item cannot be undefined`);
let toDelete;
for (const d of this.data) if (eq) {
if (eq(d.item, item)) {
toDelete = d;
break;
}
} else if (this.eq(d, {
item,
priority: 0
})) {
toDelete = d;
break;
}
if (toDelete === void 0 && !addIfMissing) throw new Error(`Item not found in priority queue. Item: ${JSON.stringify(item)}`);
if (toDelete !== void 0) this.removeWhere((item) => toDelete === item);
this.enqueueWithPriority(item, priority);
}
dequeueMax() {
const m = last(max$1(this.data, (a, b) => a.priority >= b.priority));
if (m === void 0) return;
this.removeWhere((item) => item === m);
return m.item;
}
dequeueMin() {
const m = last(max$1(this.data, (a, b) => a.priority >= b.priority));
if (m === void 0) return;
this.removeWhere((item) => item.item === m);
return m.item;
}
peekMax() {
const m = last(max$1(this.data, (a, b) => a.priority >= b.priority));
if (m === void 0) return;
return m.item;
}
peekMin() {
const m = last(min$1(this.data, (a, b) => a.priority >= b.priority));
if (m === void 0) return;
return m.item;
}
};
/**
* Creates a {@link PriorityMutable} queue.
*
* Options:
* * eq: Equality function
* * capacity: limit on number of items
* * discardPolicy: what to do if capacity is reached
* @param opts
* @returns
*/
function priority(opts = {}) {
return new PriorityMutable(opts);
}
//#endregion
//#region ../packages/collections/src/map/map-immutable-fns.ts
/**
* Adds an array o [k,v] to the map, returning a new instance
* @param map Initial data
* @param data Data to add
* @returns New map with data added
*/
const addArray = (map, data) => {
const x = new Map(map.entries());
for (const d of data) {
if (d[0] === void 0) throw new Error(`key cannot be undefined`);
if (d[1] === void 0) throw new Error(`value cannot be undefined`);
x.set(d[0], d[1]);
}
return x;
};
/**
* Adds objects to the map, returning a new instance
* @param map Initial data
* @param data Data to add
* @returns A new map with data added
*/
const addObjects = (map, data) => {
const x = new Map(map.entries());
for (const d of data) {
if (d.key === void 0) throw new Error(`key cannot be undefined`);
if (d.value === void 0) throw new Error(`value cannot be undefined`);
x.set(d.key, d.value);
}
return x;
};
/**
* Returns true if map contains key
*
* @example
* ```js
* if (has(map, `London`)) ...
* ```
* @param map Map to search
* @param key Key to find
* @returns True if map contains key
*/
const has$1 = (map, key) => map.has(key);
/**
* Adds data to a map, returning the new map.
*
* Can add items in the form of [key,value] or {key, value}.
* @example These all produce the same result
* ```js
* map.set(`hello`, `samantha`);
* map.add([`hello`, `samantha`]);
* map.add({key: `hello`, value: `samantha`})
* ```
* @param map Initial data
* @param data One or more data to add in the form of [key,value] or {key, value}
* @returns New map with data added
*/
const add$1 = (map, ...data) => {
if (map === void 0) throw new Error(`map parameter is undefined`);
if (data === void 0) throw new Error(`data parameter i.s undefined`);
if (data.length === 0) return map;
const firstRecord = data[0];
return typeof firstRecord.key !== `undefined` && typeof firstRecord.value !== `undefined` ? addObjects(map, data) : addArray(map, data);
};
/**
* Sets data in a copy of the initial map
* @param map Initial map
* @param key Key
* @param value Value to set
* @returns New map with data set
*/
const set = (map, key, value) => {
const x = new Map(map.entries());
x.set(key, value);
return x;
};
/**
* Delete a key from the map, returning a new map
* @param map Initial data
* @param key
* @returns New map with data deleted
*/
const del = (map, key) => {
const x = new Map(map.entries());
x.delete(key);
return x;
};
//#endregion
//#region ../packages/collections/src/map/map.ts
/**
* Returns an {@link IMapImmutable}.
* Use {@link Maps.mutable} as a mutable alternatve.
*
* @example Basic usage
* ```js
* // Creating
* let m = map();
* // Add
* m = m.set("name", "sally");
* // Recall
* m.get("name");
* ```
*
* @example Enumerating
* ```js
* for (const [key, value] of map.entries()) {
* console.log(`${key} = ${value}`);
* }
* ```
*
* @example Overview
* ```js
* // Create
* let m = map();
* // Add as array or key & value pair
* m = m.add(["name" , "sally"]);
* m = m.add({ key: "name", value: "sally" });
* // Add using the more typical set
* m = m.set("name", "sally");
* m.get("name"); // "sally";
* m.has("age"); // false
* m.has("name"); // true
* m.isEmpty; // false
* m = m.delete("name");
* m.entries(); // Iterator of key value pairs
* ```
*
* Since it is immutable, `add()`, `delete()` and `clear()` return a new version with change.
*
* @param dataOrMap Optional initial data in the form of an array of `{ key: value }` or `[ key, value ]`
*/
const immutable$3 = (dataOrMap) => {
if (dataOrMap === void 0) return immutable$3([]);
if (Array.isArray(dataOrMap)) return immutable$3(add$1(/* @__PURE__ */ new Map(), ...dataOrMap));
const data = dataOrMap;
return {
add: (...itemsToAdd) => {
return immutable$3(add$1(data, ...itemsToAdd));
},
set: (key, value) => {
return immutable$3(set(data, key, value));
},
get: (key) => data.get(key),
delete: (key) => immutable$3(del(data, key)),
clear: () => immutable$3(),
has: (key) => data.has(key),
entries: () => data.entries(),
values: () => data.values(),
isEmpty: () => data.size === 0
};
};
//#endregion
//#region ../packages/collections/src/map/number-map.ts
/**
* Simple map for numbers.
*
* Keys not present in map return the `defaultValue` given in the constructor
* ```js
* // All keys default to zero.
* const map = new Maps.NumberMap();
* map.get(`hello`); // 0
* ```
*
* To check if a key is present, use `has`:
* ```js
* map.has(`hello`); // false
* ```
*
* Math:
* ```js
* // Adds 1 by default to value of `hello`
* map.add(`hello`); // 1
* map.multiply(`hello`, 2); // 2
*
* // Reset key to default value
* map.reset(`hello`); // 0
* ```
*
* Different default value:
* ```js
* const map = new Maps.NumberMap(10);
* map.get(`hello`); // 10
* ```
*
* Regular `set` works, overriding the value to whatever is given:
* ```js
* map.set(`hello`, 5);
* map.add(`hello`, 2); // 7
* ```
*/
var NumberMap = class extends Map {
defaultValue;
/**
* Creates a NumberMap with default value of 0
*/
constructor(defaultValue = 0) {
super();
this.defaultValue = defaultValue;
}
/**
* Gets the value at a key. If not found, returns the default value
* @param key
* @returns
*/
get(key) {
const v = super.get(key);
if (v === void 0) return this.defaultValue;
return v;
}
/**
* Resets the key's value to the default value
* @param key
* @returns
*/
reset(key) {
super.set(key, this.defaultValue);
return this.defaultValue;
}
/**
* Multiplies the value of `key` by `amount`. If key is not found, it is treated as the default value.
* The new value is set and returned.
* @param key
* @param amount
* @returns
*/
multiply(key, amount) {
let value = super.get(key) ?? this.defaultValue;
value *= amount;
super.set(key, value);
return value;
}
/**
* Divides the value of `key` by `amount`. If key is not found, it is treated as the default value.
* The new value is set and returned.
* @param key
* @param amount
* @returns
*/
divide(key, amount) {
let value = super.get(key) ?? this.defaultValue;
value /= amount;
super.set(key, value);
return value;
}
/**
* Applies a function to all values
* ```js
* // Round all the values
* map.mapValue((value,key)=> Math.round(value));
* ```
*/
mapValue(fn) {
for (const [key, value] of this.entries()) {
const newValue = fn(value, key);
super.set(key, newValue);
}
}
/**
* Returns the largest value in the map. If the map is empty, returns `NaN`.
* ```js
* // Eg find all the keys corresponding to the maximum value
* const largestKeys = [...map.keysByValue(map.findValueMax())];
* ```
* @returns
*/
findValueMax() {
if (this.size === 0) return NaN;
let maxValue = Number.MIN_VALUE;
for (const value of this.values()) if (value > maxValue) maxValue = value;
return maxValue;
}
/**
* Returns the smallest value in the map. If the map is empty, returns `NaN`.
*
* ```js
* // Eg find all the keys corresponding to the minimum value
* const smallestKeys = [...map.keysByValue(map.findValueMin())];
* ```
* @returns
*/
findValueMin() {
if (this.size === 0) return NaN;
let minValue = Number.MAX_SAFE_INTEGER;
for (const value of this.values()) if (value < minValue) minValue = value;
return minValue;
}
/**
* Iterates over all keys that have a corresponding value
* @param v
*/
*keysByValue(v) {
for (const [key, value] of this.entries()) if (value === v) yield key;
}
/**
* Iterates over entries, sorted by value. By default ascending order.
*/
*entriesSorted(sorter) {
const entries = [...this.entries()];
if (sorter) entries.sort(sorter);
else entries.sort((a, b) => a[1] - b[1]);
for (const entry of entries) yield entry;
}
/**
* Iterates over all keys that have a value matching `fn`.
* ```js
* // Iterate over all keys that store a value greater than 1
* const greaterThanOne = (v) => v > 1;
* for (const key of map.filterKeysByValue(greaterThanOne)) {
* }
* ```
* @param fn Predicate to test values
*/
*filterKeysByValue(fn) {
for (const [key, value] of this.entries()) if (fn(value)) yield key;
}
/**
* Deletes a set of keys
*/
deleteKeys(keys) {
let deleted = 0;
for (const key of keys) if (super.delete(key)) deleted++;
return deleted;
}
/**
* Adds an amount to `key`'s value. If `key` is not found, it is treated as the default value. The new value is set and returned.
* @param key
* @param amount
* @returns
*/
add(key, amount = 1) {
let value = super.get(key) ?? this.defaultValue;
value += amount;
super.set(key, value);
return value;
}
/**
* Subtracts an amount from `key`'s value. If `key` is not found, it is treated as the default value. The new value is set and returned.
* @param key
* @param amount
* @returns
*/
subtract(key, amount = 1) {
let value = super.get(key) ?? this.defaultValue;
value -= amount;
super.set(key, value);
return value;
}
};
//#endregion
//#region ../packages/collections/src/table.ts
/**
* Stores values in a table of rows (vertical) and columns (horizontal)
*/
var Table = class {
rows = [];
rowLabels = [];
colLabels = [];
/**
* Keep track of widest row
*/
columnMaxLength = 0;
/**
* Gets the label for a given column index,
* returning _undefined_ if not found.
*
* Case-sensitive
* @param label Label to seek
* @returns Index of column, or _undefined_ if not found
*/
getColumnLabelIndex(label) {
for (const [index, l] of this.colLabels.entries()) if (l === label) return index;
}
/**
* Gets the label for a given row index,
* returning _undefined_ if not found.
*
* Case-sensitive
* @param label Label to seek
* @returns Index of row, or _undefined_ if not found
*/
getRowLabelIndex(label) {
for (const [index, l] of this.rowLabels.entries()) if (l === label) return index;
}
/**
* Dumps the values of the table to the console
*/
print() {
console.table([...this.rowsWithLabelsObject()]);
}
/**
* Return a copy of table as nested array
*
* ```js
* const t = new Table();
* // add stuff
* // ...
* const m = t.asArray();
* for (const row of m) {
* for (const colValue of row) {
* // iterate over all column values for this row
* }
* }
* ```
*
* Alternative: get value at row Y and column X
* ```js
* const value = m[y][x];
* ```
* @returns
*/
asArray() {
const r = [];
for (const row of this.rows) if (row === void 0) r.push([]);
else r.push([...row]);
return r;
}
/**
* Return the number of rows
*/
get rowCount() {
return this.rows.length;
}
/**
* Return the maximum number of columns in any row
*/
get columnCount() {
return this.columnMaxLength;
}
/**
* Iterates over the table row-wise, in object format.
* @see {@link rowsWithLabelsArray} to get rows in array format
*/
*rowsWithLabelsObject() {
for (let index = 0; index < this.rows.length; index++) yield this.getRowWithLabelsObject(index);
}
/**
* Iterates over each row, including the labels if available
* @see {@link rowsWithLabelsObject} to get rows in object format
*/
*rowsWithLabelsArray() {
for (let index = 0; index < this.rows.length; index++) yield this.getRowWithLabelsArray(index);
}
/**
* Assign labels to columns
* @param labels
*/
labelColumns(...labels) {
this.colLabels = labels;
}
/**
* Assign label to a specific column
* First column has an index of 0
* @param columnIndex
* @param label
*/
labelColumn(columnIndex, label) {
this.colLabels[columnIndex] = label;
}
/**
* Label rows
* @param labels Labels
*/
labelRows(...labels) {
this.rowLabels = labels;
}
/**
* Assign label to a specific row
* First row has an index of 0
* @param rowIndex
* @param label
*/
labelRow(rowIndex, label) {
this.rowLabels[rowIndex] = label;
}
/**
* Adds a new row
* @param data Columns
*/
appendRow(...data) {
this.columnMaxLength = Math.max(this.columnMaxLength, da