Here is an implementation in JavaScript that attempts to resemble Python-like arrays by mimicking slice comprehension for both getters and setters, as well as negative indexing from the end of the list. I've called it List
just because; this name has no particular importance to me, so suggestions on better naming for this are also welcome:
const List = (function () { // IIFE // proxy handler const listHandler = { get(target, property) { const strProp = String(property) const generator = slice(strProp) // slice comprehension if (generator !== null) { return List.from(new Map(generator(target)).values()) } const length = target.length const index = parseInt(Number(strProp)) // negative index lookup if (index == property && index >= -length && index < 0) { return target[index + length] } // prototype chain return target[property] }, set(target, property, value) { const strProp = String(property) const length = target.length const generator = slice(strProp) // slice comprehension if (generator !== null) { // can't use `in` on primitives if (!(Symbol.iterator in Object(value))) { throw new TypeError('can only assign an iterable') } const [start, stop, step] = generator.indices(length) // non-extended slice if (step === 1) { target.splice(start, stop - start, ...value) // extended slice with matching iterable length } else { if (isNaN(value.length)) { throw new TypeError('cannot assign iterator to extended slice') } if (Math.floor((stop - start) / step) !== value.length) { throw new RangeError('mismatching length of iterable and slice') } let iteration = 0 for (const [index] of generator(target)) { target[index] = value[iteration++] } } return value } const index = parseInt(Number(strProp)) // negative index lookup if (index == property && index >= -length && index < 0) { return target[index + length] = value } // fallback return target[property] = value } } // expose class definition return class List extends Array { static from(arrayLike) { return Array.from.apply(this, arguments) } static of() { return Array.of.apply(this, arguments) } static slice(start, stop = null, step = null) { if (arguments.length === 0) { throw new TypeError('expected at least 1 argument, got 0') } if (arguments.length > 3) { throw new TypeError('expected at most 3 arguments, got ' + arguments.length) } const generator = slice([ start === null ? '' : start, stop === null ? '' : stop, step === null ? '' : step ].join(':')) if (generator === null) { throw new TypeError('arguments must be numeric or null') } return generator } constructor() { super(...arguments) return new Proxy(this, listHandler) } } function indices([start, stop, step], length) { step = !step.trim() || isNaN(step) ? 1 : Number(step) if (step > 0) { start = !start.trim() || isNaN(start) ? 0 : Number(start) stop = !stop.trim() || isNaN(stop) ? length : Math.max(start, Number(stop)) start = Math.max(-length, Math.min(start, length)) stop = Math.max(-length, Math.min(stop, length)) } else { start = !start.trim() || isNaN(start) ? length - 1 : Number(start) stop = !stop.trim() || isNaN(stop) ? -length - 1 : Math.min(start, Number(stop)) start = Math.max(-length - 1, Math.min(start, length - 1)) stop = Math.max(-length - 1, Math.min(stop, length - 1)) } if (start < 0) start += length if (stop < 0) stop += length return [start, stop, step] } function slice(property) { const result = property.match(/^(-?\d*):(-?\d*):?(-?\d*)$/) if (result === null) { return result } const range = result.slice(1, 4) if (range[2] == 0) { throw new RangeError('slice step cannot be zero') } function* entries(array) { const [start, stop, step] = indices(range, array.length) for (let index = start; step > 0 ? index < stop : index > stop; index += step) { yield [index, array[index]] } } entries.valueOf = function valueOf() { return range.slice() } entries.toString = function toString() { return property } entries.indices = indices.bind(entries, range) return entries } })() // end IIFE // Demo Here let list = new List(1, 2, 3, 4, 5) let slice = List.slice(-2, -5, -1) console.log('list:', list.join()) console.log('list[-2]:', list[-2]) console.log('slice:', slice.toString()) console.log('slice.indices(list.length):', slice.indices(list.length).join()) console.log('list[slice]:', list[slice].join()) console.log('list["-2:-5:-1"]:', list['-2:-5:-1'].join()) list[slice] = 'bcd' console.log('list[slice] = "bcd":', list.join())
My goal is a consistent and readable implementation that follows the Python spec as closely as JavaScriptly possible, meaning I still want the behavior to somewhat resemble JavaScript.
For example instead of throwing RangeError
for accessing or assigning indices out of range, I resolve the property access or assignment on the target
object like normal arrays would allow. Details like that are for the sake of following expected JavaScript conventions.
Any feedback is welcome and appreciated.