2
\$\begingroup\$

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.

\$\endgroup\$

    1 Answer 1

    1
    \$\begingroup\$

    Input validation

    Here, the validation of arguments comes after using them in a method call:

     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') } 

    That's not a good practice in general, because it's potentially passing garbage to slice, and too indirect, since the actual validation logic is not visible here.

    It would be better and easier to read if the validation logic was explicitly visible. Perhaps a validateArgs method that takes start, stop, step and either throws an exception or returns an array of 3 values that are valid.

    Usability

    The default value of step is null, but that's invalid. It would be better to make it 1.

    Don't repeat yourself

    This piece of code appears twice:

     const index = parseInt(Number(strProp)) // negative index lookup if (index == property && index >= -length && index < 0) { return target[index + length] } // prototype chain return target[property] 

    It would be better to move this to a helper method.

    Ordering of terms in conditions

    Instead of this:

     if (index == property && index >= -length && index < 0) { 

    I recommend this ordering of terms:

     if (index == property && -length <= index && index < 0) { 

    When the terms are in increasing order, the condition may be slightly easier to read. Since we're talking about Python, note that this condition would be written in Python as -length <= index < 0.

    Ternary as loop condition

    The ternary operator is often not easy to read. Also, the step > 0 will be evaluated in every iteration, even though it doesn't change.

     for (let index = start; step > 0 ? index < stop : index > stop; index += step) { yield [index, array[index]] } 

    It would be better to rewrite this either by lifting the step > 0 condition out of the loop and write two similar loops, or to use a generator for indexes and loop over that.

    \$\endgroup\$
    7
    • \$\begingroup\$I appreciate all of your feedback, I just wanted to ask to be sure you were aware of the "usability" point though. The default is null but within the scoped "private" slice function, null becomes 1 so it's still the de-facto default, I just didn't want 1 to be explicit in the notation string generated when it isn't explicitly specified. In light of that, do you still feel I should default the public function's step to 1?\$\endgroup\$CommentedJul 9, 2017 at 17:59
    • \$\begingroup\$@PatrickRoberts I meant that List.slice(1, 2) fails, and it would be nice if it defaulted step = 1. Even so, it would be good to omit the 1 from the notation string.\$\endgroup\$
      – janos
      CommentedJul 9, 2017 at 18:08
    • \$\begingroup\$are you sure it fails? I can't test at the moment, but I did try running with two arguments before and it worked fine for me.\$\endgroup\$CommentedJul 9, 2017 at 18:50
    • \$\begingroup\$@PatrickRoberts I copy-pasted your code in Chrome's JavaScript console. If I run Slice.list(1, 2) I get Uncaught RangeError: slice step cannot be zero. Am I missing something?\$\endgroup\$
      – janos
      CommentedJul 9, 2017 at 18:51
    • \$\begingroup\$Oh jeez that's a bug. Thank you for pointing that out, I had thought that "" != 0, and I could have sworn I had tested that.\$\endgroup\$CommentedJul 9, 2017 at 18:53

    Start asking to get answers

    Find the answer to your question by asking.

    Ask question

    Explore related questions

    See similar questions with these tags.