Skip to content

Instantly share code, notes, and snippets.

@rbuckton
Last active August 23, 2019 16:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rbuckton/174b02d2a43573627201f8057701044c to your computer and use it in GitHub Desktop.
Save rbuckton/174b02d2a43573627201f8057701044c to your computer and use it in GitHub Desktop.
Index and Interval types and syntax
// 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))`
@hax
Copy link

hax commented Aug 22, 2019

The signal of C# Index constructor is (value: int, fromEnd: bool). I understand new Index(1, 'end') is clearer than new Index(1, true), but consider Index.fromStart/fromEnd as recommended API, I would rather keep consistence with C# API in this special case, and I feel idx.isFromEnd() is easier to understand than idx.anchor.

@hax
Copy link

hax commented Aug 22, 2019

I also worry about the name "Interval", programmers may be confused it with "setInterval", can we find a better name?

@rbuckton
Copy link
Author

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