Skip to content

Commit 3bad096

Browse files
committed
Add support for operator overloading
1 parent 6ebd225 commit 3bad096

File tree

3 files changed

+182
-3
lines changed

3 files changed

+182
-3
lines changed

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,95 @@ functions.time = function() {
221221
222222
</details>
223223
224+
### Operator Overloading
225+
226+
<details>
227+
228+
<summary>Click to read about operator overloading</summary>
229+
230+
In Code Grid formulas, infix operators (such as `+`) are compatible with the
231+
same types as the corresponding JavaScript operator. For example, it is equally
232+
valid to do `"x" + "y"` or `3 + 2` in formulas, since addition works on both
233+
strings and numbers in JavaScript. Using the same infix operator (`+`) for
234+
different operations on different types is called "operator overloading."
235+
236+
Code Grid allows users to extend infix operations to work on more complex types
237+
through advanced operator overloading. To make a type use a custom operation for
238+
an infix operator, define a method with the same name as that operator. That's
239+
it. For binary operations, the implementing method should take one argument. For
240+
unary operations, the method should take no arguments.
241+
242+
For example, we could implement vectors that support element-wise addition:
243+
244+
``` javascript
245+
class Vector {
246+
constructor(a) {
247+
this.elements = a;
248+
}
249+
250+
toString() {
251+
return "<" + this.elements.join(", ") + ">";
252+
}
253+
254+
["+"](v) {
255+
return new Vector(this.elements.map((x, i) => x + v.elements[i]));
256+
}
257+
}
258+
259+
functions.v = (...a) => new Vector(a);
260+
```
261+
262+
Then, the following would be a valid Code Grid formula that would evaluate to
263+
`<1, 2, 3>`, even though adding vector objects in JavaScript would throw an
264+
error:
265+
266+
```
267+
=v(0, 3, 1) + v(1, -1, 2)
268+
```
269+
270+
We could also implement overloading of the unary `~` operator to switch the sign
271+
of all vector elements by adding the following method to the `Vector` class:
272+
273+
``` javascript
274+
class Vector {
275+
// ...
276+
277+
// ~<1, -1, 3> => <-1, 1, -3>
278+
["~"]() {
279+
return new Vector(this.elements.map(x => -x));
280+
}
281+
282+
// ...
283+
}
284+
```
285+
286+
Consider operations between different types. For example, if we want to
287+
implement vector-scalar subtraction, we will need to handle `<vector> - scalar`
288+
as well as `scalar - <vector>`.
289+
290+
When evaluating an infix operation `x op y`, Code Grid first tries `x.op(y)`,
291+
then `x.op.forward(y)`, then `y.op.backward(x)`, finally falling back on the
292+
default operator implementation if nothing else works. In this example, we will
293+
implement `<vector> - scalar` in the `forward` method, and `scalar - <vector>`
294+
in the `backward` method:
295+
296+
``` javascript
297+
class Vector {
298+
// ...
299+
300+
["-"] = {
301+
// <v> - s
302+
forward: (s) => new Vector(this.elements.map(x => x - s));
303+
// s - <v>
304+
backward: (s) => new Vector(this.elements.map(x => s - x));
305+
}
306+
307+
// ...
308+
}
309+
```
310+
311+
</details>
312+
224313
# How Code Grid Works
225314
226315
## Code Table of Contents

src/formula.js

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,15 @@ class BinaryOperation extends Expression {
114114
.forEach((op) => {
115115
const x = args.shift();
116116
const y = args.shift();
117-
args.unshift(BinaryOperation.operations[op](x, y));
117+
if (typeof x[op] === "function") {
118+
args.unshift(x[op](y));
119+
} else if (typeof x[op]?.forward === "function") {
120+
args.unshift(x[op].forward(y));
121+
} else if (typeof y[op]?.reverse === "function") {
122+
args.unshift(y[op].reverse(x));
123+
} else {
124+
args.unshift(BinaryOperation.operations[op](x, y));
125+
}
118126
});
119127
// Note that args is a singleton list
120128
return args;
@@ -137,12 +145,18 @@ class UnaryOperation extends Expression {
137145

138146
constructor(operator, operand) {
139147
super();
140-
this.operator = UnaryOperation.operations[operator];
148+
this.operator = operator;
141149
this.operand = operand;
142150
}
143151

144152
compute(globals, sheet, r, c) {
145-
const thunk = (x) => [this.operator(x)];
153+
const thunk = (x) => {
154+
if (typeof x[this.operator] === "function") {
155+
return [x[this.operator]()];
156+
} else {
157+
return [UnaryOperation.operations[this.operator](x)];
158+
}
159+
};
146160
const refs = [this.operand.compute(globals, sheet, r, c)];
147161
return new ExpressionValue(undefinedArgsToIdentity(thunk), refs);
148162
}

test/sheet-and-formula.test.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,3 +568,79 @@ test("Delete columns in the middle of a sheet", async () => {
568568
[7, 9, 16],
569569
]);
570570
});
571+
572+
test("Operator overloading", async () => {
573+
evalCode(`
574+
class Vector {
575+
constructor(a) {
576+
this.elements = a;
577+
this["+"].reverse = (x) => this["+"](x);
578+
}
579+
580+
toString() {
581+
return "<" + this.elements.join(", ") + ">";
582+
}
583+
584+
["*"](v) {
585+
return this.elements
586+
.map((x, i) => x * v.elements[i])
587+
.reduce((a, x) => a + x, 0);
588+
}
589+
590+
["+"](s) {
591+
return new Vector(this.elements.map(x => x + s));
592+
}
593+
594+
["-"] = {
595+
forward: (s) => new Vector(this.elements.map(x => x - s)),
596+
backward: (s) => new Vector(this.elements.map(x => s - x)),
597+
}
598+
}
599+
600+
functions.v = (...a) => new Vector(a);
601+
`);
602+
const state = createSheet([
603+
["=V(1, 2, 3)", "=v(4, 5, 7)", "=RC0 * RC1"],
604+
["=5 + R[-1]C + -2", "=R[-1]C - 5 - -1", undefined],
605+
]);
606+
let expected = [
607+
["<1, 2, 3>", "<4, 5, 7>", "35"],
608+
["<7, 8, 10>", "<0, 1, 3>", undefined],
609+
];
610+
await Promise.all(
611+
expected
612+
.map((row, i) =>
613+
row.map((cell, j) =>
614+
expect
615+
.poll(() => state.currentSheet.cells[i][j].get()?.toString())
616+
.toEqual(cell),
617+
),
618+
)
619+
.flat(),
620+
);
621+
});
622+
623+
test("Operator overloading with monkeypatching", async () => {
624+
evalCode(`
625+
String.prototype["*"] = function(n) {
626+
return this.repeat(n);
627+
}
628+
629+
Number.prototype["*"] = function(s) {
630+
return s?.repeat?.(this);
631+
}
632+
633+
String.prototype["~"] = function() {
634+
return this
635+
.split("")
636+
.map(x => x == x.toLocaleUpperCase()
637+
? x.toLocaleLowerCase()
638+
: x.toLocaleUpperCase()
639+
).join("");
640+
}
641+
`);
642+
const state = createSheet([[`="test " * 3`, `=3 * RC[-1]`, `=~~~"TeSt"`]]);
643+
await expectSheet(state.currentSheet, [
644+
["test ".repeat(3), "test ".repeat(9), "tEsT"],
645+
]);
646+
});

0 commit comments

Comments
 (0)