import * as utils from "./utils.js";
import * as cutils from "./clone-utils.js";
import * as generics from "./AllGenerics.js";
class IndexedNames {
constructor(names) {
this._names = names;
this._lookup = new Map;
}
names() {
return this._names;
}
nameToIndexUncached(name) {
if (this._lookup.has(name)) {
return this._lookup.get(name);
}
return this._names.indexOf(name);
}
nameToIndex(name, { error = true } = {}) {
if (this._lookup.has(name)) {
return this._lookup.get(name);
}
for (var i = this._lookup.size; i < this._names.length; i++) {
const current = this._names[i];
if (this._lookup.has(current)) {
continue; // only keep the first instance of a duplicated name.
}
this._lookup.set(current, i);
if (this._names[i] == name) {
return i;
}
}
if (error) {
throw new Error("no matching name for '" + name + "' in this " + this.constructor.className);
} else {
return -1;
}
}
indexToName(i) {
return this._names[i];
}
append(name, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (!inPlace) {
target._names = target._names.slice();
target._lookup = new Map; // always making a new map to avoid sharing lookup tables between instances with different names.
}
target._names.push(name);
return target;
}
set(i, name, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (!inPlace) {
target._names = target._names.slice();
target._lookup = new Map; // always making a new map to avoid sharing lookup tables between instances with different names.
} else {
if (target._lookup.size > i) { // if the lookup never got to that point, we don't have to wipe it.
target._lookup = new Map;
}
}
target._names[i] = name;
return target;
}
delete(i, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (!inPlace) {
target._names = target._names.slice();
target._lookup = new Map; // always making a new map to avoid sharing lookup tables between instances with different names.
} else {
if (target._lookup.size > i) { // i.e., if the lookup never got to that point, we don't have to wipe it.
target._lookup = new Map;
}
}
target._names.splice(i, 1);
return target;
}
_bioconductor_CLONE({ deepCopy = true }) {
let output = new this.constructor;
output._names = this._names;
output._lookup = this._lookup;
return output;
}
}
/**
* An R-style list with optional names.
* Callers can get/set individual elements by positional index or name.
* Operations like slicing and combining will apply to both the values and names.
*
* The List defines methods for the following generics:
*
* - {@linkcode LENGTH}
* - {@linkcode SLICE}
* - {@linkcode COMBINE}
* - {@linkcode CLONE}
*
* We explicitly allow duplicates in the names to avoid errors when slicing or combining.
* Otherwise, it would be impossible to construct a slice with duplicate indices or to combine multiple `List` instances with shared names.
*
* Constructors of List subclasses should create an empty instance of the subclass when called with no arguments.
*/
export class List {
/**
* @param {Array|Map|Object} values - Elements of the List.
* For Maps or objects, the values (in order of iteration) are used as the List elements.
* @param {Object} [options={}] - Further options.
* @param {?Array} [options.names=null] - An array of strings containing the names of the List elements.
* If provided, this should be of the same length as `values`.
* If `values` is a Map or object, `names` should have the same keys.
* If `values` is an array, the names may contain duplicate strings.
* If `null` and `values` is an array, the List will be unnamed.
*/
constructor(values, { names = null } = {}) {
if (arguments.length == 0) {
return;
}
if (values instanceof Array) {
if (names !== null) {
if (names.length != values.length) {
throw new Error("'names' and 'values' should have the same length");
}
for (const n of names) {
if (typeof n != "string") {
throw new Error("'names' should be an array of strings");
}
}
names = new IndexedNames(names);
}
this._values = values;
this._names = names;
} else if (values instanceof Map) {
let arr = [];
if (names == null) {
names = [];
for (const [n, v] of values) {
if (typeof n != "string") {
throw new Error("keys of 'values' should be strings");
}
names.push(n);
arr.push(v);
}
} else {
if (names.length != values.size) {
throw new Error("size of 'values' should be equal to length of 'names'");
}
for (const n of names) {
if (typeof n != "string") {
throw new Error("'names' should be an array of strings");
}
if (!values.has(n)) {
throw new Error("missing name '" + n + "' in 'values'");
}
arr.push(values.get(n));
}
}
this._values = arr;
this._names = new IndexedNames(names);
} else {
let arr = [];
if (names == null) {
names = [];
for (const [n, v] of Object.entries(values)) {
names.push(n);
arr.push(v);
}
} else {
if (names.length != Object.keys(values).length) {
throw new Error("size of 'values' should be equal to length of 'names'");
}
for (const n of names) {
if (typeof n != "string") {
throw new Error("'names' should be an array of strings");
}
if (!(n in values)) {
throw new Error("missing name '" + n + "' in 'values'");
}
arr.push(values[n]);
}
}
this._values = arr;
this._names = new IndexedNames(names);
}
}
/**
* @return {?Array} Array of names of the List elements, or `null` if the List is unnamed.
*/
names() {
if (this._names == null) {
return null;
} else {
return this._names.names();
}
}
/**
* @return {Array} Array containing the List elements.
*/
values() {
return this._values;
}
/**
* @return {number} Length of the list.
*/
length() {
return this._values.length;
}
static className = "List";
/***********************************************/
#check_index(i) {
if (i < 0 || i >= this._values.length) {
throw new Error(" index '" + String(i) + "' out of range for this " + this.constructor.className);
}
}
/**
* @param {number} i - Index of the List element to retrieve.
* This should be non-negative and less than {@linkcode List#length length}.
* @return The `i`-th List element.
*/
getByIndex(i) {
this.#check_index(i);
return this._values[i];
}
/**
* @param {string} name - Name of the List element to retrieve.
* This should be present in {@linkcode List#names names}.
* @return The List element corresponding to `name`.
* If duplicates of `name` are present in the list, the first occurrence is returned.
*/
getByName(name) {
if (this._names === null) {
throw new Error("no available names in this " + this.constructor.className);
}
let candidate = this._names.nameToIndex(name);
return this._values[candidate];
}
/**
* @param {string|number} i - Index or name of the List element to retrieve.
* Numbers are passed to {@linkcode List#getByIndex getByIndex} and strings are passed to {@linkcode List#getByName getByName}.
* @return The List element at/for `i`.
*/
get(i) {
if (typeof i == "number") {
return this.getByIndex(i);
} else {
return this.getByName(i);
}
}
/**
* @param {string} name - Name of a List element.
* @return {number} Index of the name in {@linkcode List#names names}.
* If duplicate names are present, the first occurrence is returned.
*/
nameToIndex(name) {
return this._names.nameToIndex(name);
}
/***********************************************/
/**
* @param {number} i - Index of the List element to set.
* This should be non-negative and no greater than {@linkcode List#length length}.
* If `i` is less than `length`, the `i`-th element is replaced by `x`.
* If `i` is equal to `length`, `x` is appended to the end of the list.
* @param {*} x - Value of a List element.
* @param {Object} [options={}] - Further options.
* @param {?string} [options.name=null] - Name for the List element at `i`.
* If `i` is less than `length`, the name of the `i`-th element is replaced by `name`.
* If `i` is equal to `length`, the name of the newly-appended element is set to `name`.
* If the List did not previously have any names, the names of all other elements are set to an empty string.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} The List after setting the `i`-th element to `x`.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
setByIndex(i, x, { name = null, inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (!inPlace) {
target._values = target._values.slice();
}
if (i < 0 || i > this._values.length) {
throw new Error(" index '" + String(i) + "' out of range for this " + this.constructor.className);
}
if (i == target._values.length) {
target._values.push(x);
if (name == null) {
if (target._names != null) {
target._names = target._names.append("", { inPlace });
}
} else {
if (typeof name != "string") {
throw new Error("'name' should be a string");
}
if (target._names == null) {
const new_names = new Array(target._values.length).fill("");
new_names[i] = name;
target._names = new IndexedNames(new_names);
} else {
target._names = target._names.append(name, { inPlace });
}
}
} else {
target._values[i] = x;
if (name !== null) {
if (target._names === null) {
const new_names = new Array(target._values.length).fill("");
new_names[i] = name;
target._names = new IndexedNames(new_names);
} else {
target._names = target._names.set(i, name, { inPlace });
}
}
}
return target;
}
/**
* @param {number} name - Name of the List element to set.
* If this already exists in {@linkcode List#names names}, the corresponding element is replaced by `x`.
* Otherwise, `x` is appended to the List with the name `name`.
* If the List did not previously have any names, the names of all other elements are set to an empty string.
* @param {*} x - Value of a List element.
* @param {Object} [options={}] - Further options.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} The List after setting the `name`d entry to `x`.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
setByName(name, x, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (!inPlace) {
target._values = target._values.slice();
}
if (target._names !== null) {
let candidate = target._names.nameToIndex(name, { error: false });
if (candidate < 0) {
target._values.push(x);
target._names = target._names.append(name, { inPlace });
} else {
target._values[candidate] = x;
}
} else {
const new_names = new Array(target._values.length).fill("");
new_names.push(name);
target._names = new IndexedNames(new_names);
target._values.push(x);
}
return target;
}
/**
* @param {string|number} i - Index or name of the list element to set.
* Numbers are passed to {@linkcode List#setByIndex setByIndex} and strings are passed to {@linkcode List#setByName setByName}.
* @param {*} x - Value of a List element.
* @param {Object} [options={}] - Further options.
* @param {?string} [options.name=null] - See the argument of the same name in {@linkcode List#setByName setByName}.
* Only used if `i` is a number.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} The List after setting the `i`-th element to `x`.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
set(i, x, { name = null, inPlace = false } = {}) {
if (typeof i == "number") {
return this.setByIndex(i, x, { name, inPlace });
} else {
return this.setByName(i, x, { inPlace });
}
}
/**
* @param {?Array} names - Array of strings of length equal to {@linkcode List#length length}.
* This may contain duplicates.
* Alternatively `null`, to remove existing names.
* @param {Object} [options={}] - Further options.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} The List after replacing the names with `names`.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
setNames(names, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (names !== null) {
if (names.length != this._values.length) {
throw new Error("'names' and 'values' should have the same length");
}
for (const n of names) {
if (typeof n != "string") {
throw new Error("'names' should be an array of strings");
}
}
}
target._names = new IndexedNames(names);
return target;
}
/***********************************************/
/**
* @param {number} i - Index of the List element to delete.
* This should be non-negative and no less than {@linkcode List#length length}.
* @param {Object} [options={}] - Further options.
* @param {?string} [options.name=null] - See the argument of the same name in {@linkcode List#setByName setByName}.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} The List after deleting the `i`-th element.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
deleteByIndex(i, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (!inPlace) {
target._values = target._values.slice();
}
this.#check_index(i);
target._values.splice(i, 1);
if (target._names !== null) {
target._names = target._names.delete(i, { inPlace });
}
return target;
}
/**
* @param {number} name - Name of the List element to delete.
* This should already exist in {@linkcode List#names names}.
* @param {?string} [options.name=null] - See the argument of the same name in {@linkcode List#setByName setByName}.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} The List after deleting the `name`d element.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
deleteByName(name, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
if (!inPlace) {
target._values = target._values.slice();
}
if (target._names == null) {
throw new Error("no available names in this " + this.constructor.className);
}
// Don't cache as we're going to reset the lookup immediately, so it would be needlessly inefficient.
let candidate = this._names.nameToIndexUncached(name);
if (candidate < 0) {
throw new Error("no matching name for '" + name + "' in this " + this.constructor.className);
}
target._values.splice(candidate, 1);
if (target._names !== null) {
target._names = target._names.delete(candidate, { inPlace });
}
return target;
}
/**
* @param {string|number} i - Index or name of the List element to delete.
* Numbers are passed to {@linkcode List#deleteByIndex deleteByIndex} and strings are passed to {@linkcode List#deleteByName deleteByName}.
* @param {Object} [options={}] - Further options.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} The List after deleting the `i`-th element.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
delete(i, { inPlace = false } = {}) {
if (typeof i == "number") {
return this.deleteByIndex(i, { inPlace });
} else {
return this.deleteByName(i, { inPlace });
}
}
/***********************************************/
/**
* @param {number} start - Index of the first element in the slice.
* @param {number} end - Index past the last element in the slice.
* @param {Object} [options={}] - Further options.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} A List that is sliced to `[start, end)`.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
sliceRange(start, end, { inPlace = false } = {}) {
let target = cutils.setterTarget(this, inPlace);
target._values = target._values.slice(start, end);
if (this._names !== null) {
target._names = new IndexedNames(target._names.names().slice(start, end));
}
return target;
}
/**
* @param {Array} indices - Array of numbers or strings specifying the List elements to retain in the slice.
* Numbers are interpreted as positional indices while strings are interpreted as names.
* @param {Object} [options={}] - Further options.
* @param {boolean} [options.inPlace=false] - Whether to modify this List instance in place.
* If `false`, a new instance is returned.
*
* @return {List} A List containing the specified elements in `indices`.
* If `inPlace = true`, this is a reference to the current instance, otherwise a new instance is created and returned.
*/
sliceIndices(indices, { inPlace = false } = {}) {
let new_names = [];
let new_values = [];
for (let i of indices) {
if (typeof i == "string") {
if (this._names == null) {
throw new Error("no available names in this " + this.constructor.className);
}
i = this._names.nameToIndex(i);
} else {
this.#check_index(i);
}
new_values.push(this._values[i]);
if (this._names !== null) {
new_names.push(this._names.indexToName(i));
}
}
let target = cutils.setterTarget(this, inPlace);
target._values = new_values;
if (this._names !== null) {
target._names = new IndexedNames(new_names);
}
target._lookup = new Map;
return target;
}
/***********************************************/
/**
* @return {iterator} An iterable iterator that can be used in, e.g., `for...of` constructs to loop over the List.
* The list values are directly returned during iteration, i.e., names are ignored.
*/
[Symbol.iterator]() {
let counter = 0;
let all_values = this._values;
return {
next: function() {
if (counter < all_values.length) {
let val = all_values[counter];
counter++;
return { done: false, value: val };
} else {
return { done: true };
}
},
[Symbol.iterator]() {
return this;
},
}
}
/***********************************************/
_bioconductor_LENGTH() {
return this.length();
}
_bioconductor_SLICE(i, { allowView = false }) {
let sliced = this.sliceIndices(i);
let output = new this.constructor;
output._values = sliced._values;
output._names = sliced._names;
return output;
}
_bioconductor_CLONE({ deepCopy = true }) {
let output = new this.constructor;
output._values = cutils.cloneField(this._values, deepCopy);
output._names = cutils.cloneField(this._names, deepCopy);
return output;
}
_bioconductor_COMBINE(objects) {
let all_values = this._values.slice();
let all_names = null;
if (this._names !== null) {
all_names = this.names().slice();
}
for (let x of objects) {
if (!(x instanceof List)) {
x = new List(x);
}
const xvals = x.values();
for (const y of xvals) {
all_values.push(y);
}
const xnames = x.names();
if (xnames === null) {
if (all_names !== null) {
for (const y of xvals) {
all_names.push("");
}
}
} else {
if (all_names === null) {
all_names = new Array(all_values.length - xvals.length).fill("");
}
for (const yn of xnames) {
all_names.push(yn);
}
}
}
let output = new this.constructor;
output._values = all_values;
output._names = new IndexedNames(all_names);
return output;
}
}