Last active
August 23, 2019 16:47
-
-
Save rbuckton/174b02d2a43573627201f8057701044c to your computer and use it in GitHub Desktop.
Index and Interval types and syntax
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Symbols: | |
// @@geti - An inverted-get. When present on an object used in an element access, calls this method rather than coercing | |
// the object to a String. | |
// ex: `a[b]` --> `b[@@geti](a)` | |
// @@seti - An inverted-set. When present on an object used in an element access assignment, calls this method rather | |
// than coercing the object to a String. | |
// ex: `a[b] = 1` --> `b[@@seti](a, 1)` | |
// @@indexedGet - Gets a value from an object in relation to a given `Index` instance. | |
// @@indexedSet - Sets a value on an object in relation to a given `Index` instance. | |
// @@slice - Gets values from an object in relation to a given `Interval` instance. | |
// @@index - Calculates an ordinal position based on a given length. | |
// @@interval - Calculates the ordinal start and end positions, and step value based on a given length. | |
function isIndex(value) { | |
return value >= 0 | |
&& isFinite(value) | |
&& value === (value | 0); | |
} | |
function asIndexObject(value) { | |
return value instanceof Index ? value : | |
typeof value === "number" ? new Index(Math.abs(value), value < 0) : | |
undefined; | |
} | |
class Index { | |
#value; | |
#isFromEnd; | |
constructor(value, isFromEnd = false) { | |
if (typeof value !== "number") throw new TypeError(); | |
if (!isIndex(value)) throw new RangeError(); | |
this.#value = value; | |
this.#isFromEnd = !!isFromEnd; | |
} | |
static get start() { | |
return new Index(0, "start"); | |
} | |
static get end() { | |
return new Index(0, "end"); | |
} | |
get value() { | |
return this.#value; | |
} | |
get isFromEnd() { | |
return this.#isFromEnd; | |
} | |
[Symbol.geti](obj) { | |
return obj[Symbol.indexedGet](this); | |
} | |
[Symbol.seti](obj, value) { | |
return obj[Symbol.indexedSet](this, value); | |
} | |
[Symbol.index](length) { | |
return this.getIndex(length); | |
} | |
getIndex(length) { | |
if (typeof length !== "number") throw new TypeError(); | |
if (!isIndex(length)) throw new RangeError(); | |
return this.#isFromEnd ? length - this.#value : this.#value; | |
} | |
toString() { | |
return this.#isFromEnd ? `^${this.#value}` : `${this.#value}`; | |
} | |
static fromStart(value) { | |
return new Index(value, /*isFromEnd*/ false); | |
} | |
static fromEnd(value) { | |
return new Index(value, /*isFromEnd*/ true); | |
} | |
} | |
class Interval { | |
#start; | |
#end; | |
#step; | |
constructor(start, end, step = 1) { | |
start = asIndexObject(start); | |
end = asIndexObject(end); | |
if (!start) throw new TypeError(); | |
if (!end) throw new TypeError(); | |
if (!isIndex(step) || step < 1) throw new RangeError(); | |
this.#start = start; | |
this.#end = end; | |
this.#step = step; | |
} | |
static get all() { | |
return new Interval(Index.start, Index.end); | |
} | |
get start() { return this.#start; } | |
get end() { return this.#end; } | |
get step() { return this.#step; } | |
[Symbol.geti](obj) { | |
return obj[Symbol.slice](this); | |
} | |
[Symbol.range](length) { | |
return this.getIndices(length); | |
} | |
getIndices(length) { | |
if (typeof length !== "number") throw new TypeError(); | |
if (!isIndex(length)) throw new RangeError(); | |
const start = this.#start.getIndex(length); | |
const end = this.#end.getIndex(length); | |
const step = end < start ? -this.#step : this.#step; | |
return [start, end, step]; | |
} | |
[Symbol.iterator]() { | |
return this.values(); | |
} | |
* values(length = 0) { | |
const [start, end, step] = this.getIndices(length); | |
for (let i = start; step < 0 ? i > end : i < end; i += step) { | |
yield i; | |
} | |
} | |
toString() { | |
return this.#step === 1 ? `${this.#start}:${this.#end}` : `${this.#start}:${this.#end}:${this.#step}`; | |
} | |
static startAt(index) { | |
return new Range(asIndexObject(index), Index.end); | |
} | |
static endAt(index) { | |
return new Range(Index.start, asIndexObject(index)); | |
} | |
} | |
Object.defineProperties(Array.prototype, { | |
[Symbol.indexedGet]: { | |
enumerable: false, | |
configurable: true, | |
writable: true, | |
value(index) { | |
return this[index[Symbol.index](this.length)]; | |
} | |
}, | |
[Symbol.indexedSet]: { | |
enumerable: false, | |
configurable: true, | |
writable: true, | |
value(index, value) { | |
this[index[Symbol.index](this.length)] = value; | |
return true; | |
} | |
}, | |
[Symbol.slice]: { | |
enumerable: false, | |
configurable: true, | |
writable: true, | |
value(interval) { | |
const [start, end, step] = interval[Symbol.interval](this.length); | |
const result = []; | |
for (let i = start; step < 0 ? i > end : i < end; i += step) { | |
result.push(this[i]); | |
} | |
return result; | |
} | |
} | |
}); | |
let m1 = ^1; | |
// let m1 = Index.fromEnd(1); | |
let r = (0:^1); | |
// let r = (Index.fromStart(0):Index.fromEnd(1)); | |
// let r = new Interval(Index.fromStart(0), Index.fromEnd(1)); | |
let ar = ["a", "b", "c", "d"]; | |
ar[^1] === "d"; | |
ar[m1] === "d"; | |
ar[0:^1] === ["a", "b", "c"]; | |
ar[r] === ["a", "b", "c"]; | |
// `ar[^1]` --> `^1[@@geti](ar)` --> `ar[@@indexedGet](^1)` | |
// `ar[0:^1]` --> `(0:^1)[@@geti](ar)` --> `ar[@@slice]((0:^1))` |
I also worry about the name "Interval", programmers may be confused it with "setInterval", can we find a better name?
I mentioned in tc39/proposal-slice-notation#19 (comment) that Interval
is an appropriate term as defined in mathematics: https://en.wikipedia.org/wiki/Interval_(mathematics). I'm not opposed to choosing a different name if it were to become necessary, but I think the inconsistency of Interval
vs setInterval
is minor and akin to Map
vs Array.prototype.map
.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The signal of C#
Index
constructor is (value: int, fromEnd: bool). I understandnew Index(1, 'end')
is clearer thannew Index(1, true)
, but considerIndex.fromStart/fromEnd
as recommended API, I would rather keep consistence with C# API in this special case, and I feelidx.isFromEnd()
is easier to understand thanidx.anchor
.