Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/task-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,14 @@ Besides the editor, the workspace provides a calculator, so arithmetic never
needs another tab or app. It evaluates one expression with `+ - * / // %`,
`^` for powers, `pi`, `e`, and the functions `sin cos tan asin acos atan
atan2 sinh cosh tanh exp ln log log2 sqrt cbrt pow hypot min max abs floor
ceil round deg rad`. A selector sets whether trigonometric functions take and
ceil round deg rad`. `//` rounds down and `%` takes the sign of the divisor, so
`-7 // 3` is -3, `-7 % 3` is 2, and `a == (a // b) * b + a % b` holds for
integers of every sign while the arithmetic stays exact. An infinite divisor
is the exception: `%` leaves a finite dividend unchanged, so `-1 % 10^400` is
-1. A fractional operand can miss the identity, because `//` rounds the quotient
before flooring it, so `1 // 0.1` is 10. A dividend of the other sign that is
tiny beside the divisor rounds to the divisor itself, so `-1e-20 % 3` is 3, as
with Python floats. A selector sets whether trigonometric functions take and
return radians or degrees, and in degrees the multiples of 90 are exact. A
result shows fifteen significant digits, except an integer up to 2^53, which
is shown whole. A
Expand Down
62 changes: 62 additions & 0 deletions tests/browser/calculator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,68 @@ test("arithmetic follows ordinary precedence and associativity", () => {
assert.equal(evaluate(text), value, text);
});

test("% is the remainder of the floored division // performs", () => {
for (const [text, value] of [
["-7 // 3", -3],
["-7 % 3", 2],
["7 // -3", -3],
["7 % -3", -2],
["-7 // -3", 2],
["-7 % -3", -1],
["-7.5 % 2", 0.5],
["(-7 // 3) * 3 + (-7 % 3)", -7],
])
assert.equal(evaluate(text), value, text);
for (const a of [-7, -6, -1, 0, 1, 6, 7])
for (const b of [-3, -2, 2, 3]) {
const text = `(${a} // ${b}) * ${b} + (${a} % ${b})`;
assert.equal(evaluate(text), a, text);
}
});

test("an exact multiple leaves no negative zero for atan2 to read", () => {
assert.equal(evaluate("-6 % 3"), 0);
assert.equal(evaluate("-6 % -3"), 0);
assert.equal(evaluate("atan2(-6 % 3, -1)"), Math.PI);
});

test("% keeps a finite result under an infinite divisor", () => {
for (const [text, value] of [
["-1 % 10^400", -1],
["1 % -1e999", 1],
["5 % 10^400", 5],
["-5 % -1e999", -5],
])
assert.equal(evaluate(text), value, text);
});

test("a tiny remainder of the other sign rounds to the divisor", () => {
for (const [text, value] of [
["-1e-20 % 3", 3],
["1e-20 % -3", -3],
])
assert.equal(evaluate(text), value, text);
});

test("% leaves a non-negative dividend and divisor alone", () => {
Comment thread
YunJ-Chang marked this conversation as resolved.
Resolved
for (const [text, value] of [
["7 % 3", 1],
["6 % 3", 0],
["0.1 % 1", 0.1],
["7.5 % 2", 1.5],
])
assert.equal(evaluate(text), value, text);
});

test("% still refuses a zero divisor", () => {
for (const text of ["5 % 0", "-5 % 0", "0 % 0"])
assert.throws(
() => evaluate(text),
{ message: "The result is not a finite number" },
text,
);
});

test("bad input names its problem and never runs code", () => {
for (const [text, message] of [
["", "Enter an expression"],
Expand Down
18 changes: 17 additions & 1 deletion web/calculator.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,22 @@ export const MAX_EXPRESSION_LENGTH = 200;

const RADIANS_PER_DEGREE = Math.PI / 180;

/// The remainder of the floored division `//` performs, so the result takes the
/// divisor's sign and `a == (a // b) * b + a % b` for integers. JavaScript's
/// `%` truncates instead. It adds the divisor only when the signs differ,
/// because `((a % b) + b) % b` would disturb `0.1 % 1`. A fractional operand
/// can miss the identity, because `//` rounds the quotient before flooring it.
/// An exact multiple gives 0, because `atan2` reads the sign of -0. An infinite
/// divisor keeps the plain remainder, so a finite result stays finite. A
/// dividend of the other sign that is tiny beside the divisor rounds to the
/// divisor itself: `-1e-20 % 3` is 3, as with Python floats.
function flooredRemainder(dividend, divisor) {
const remainder = dividend % divisor;
if (remainder === 0) return 0;
if (!Number.isFinite(divisor)) return remainder;
return remainder < 0 !== divisor < 0 ? remainder + divisor : remainder;
Comment thread
YunJ-Chang marked this conversation as resolved.
Resolved
Comment thread
YunJ-Chang marked this conversation as resolved.
Resolved
}

/// A trigonometric function of an angle in the mode's unit. In degrees the
/// multiples of 90 are exact, so `cos(90)` is 0 and `tan(90)` is refused,
/// where radians would give 6e-17 and 1.6e16.
Expand Down Expand Up @@ -128,7 +144,7 @@ export function evaluate(text, { angle = "rad" } = {}) {
if (take("*")) value *= unary();
else if (take("//")) value = Math.floor(value / unary());
else if (take("/")) value /= unary();
else if (take("%")) value %= unary();
else if (take("%")) value = flooredRemainder(value, unary());
else return value;
}
}
Expand Down
Loading