Last active
December 8, 2024 09:03
-
-
Save nicholaswmin/5c9949d0efed3b18560781e3242354c8 to your computer and use it in GitHub Desktop.
majestically tiny, stack-based calculator in ES6+
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
/* | |
tiny stack-based calculator | |
- If you're looking for a fancier expression calculator, | |
this isn't it. What you're looking for is a "Shunting Yard" | |
implementation. This is intentionally simple. | |
@nicholaswmin, MIT License | |
-- Usage: -- | |
import calc from './calc.js' | |
calc('3 + 10') // 13 | |
calc('5 + 20') // 25 | |
calc('10 * (15 + 3)') // SyntaxError: oopsie me too boujie for this, hihihi | |
*/ | |
export default calc = exp => { | |
const symbols = /(?:\-?[\d\.]+)|[-\+\*\/]|\s+/g | |
if (exp !== exp.match(symbols).join('')) | |
throw SyntaxError(`unparsable expression: ${exp}`) | |
const squish = str => str.trim() | |
const tokens = exp.match(symbols).map(squish).filter(Boolean) | |
const floats = tokens.map(parseFloat) | |
const queued = [] | |
for (let i = 0; i < tokens.length; i++) | |
floats[i] === floats[i] ? queued.push(floats[i]) : ({ | |
'+': () => 0, | |
'-': () => queued.push(floats[++i] * -1), | |
'*': () => queued.push(queued.pop() * floats[++i]), | |
'/': () => queued.push(queued.pop() / floats[++i]) | |
})[tokens[i]]() | |
return queued.reduce((sum, t) => sum + t) | |
} |
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
/* | |
Unit-tests for `calc` | |
run with: `node --test calc.test.js` | |
> requires nodejs v22+ | |
@nicholaswmin, MIT License | |
> note: | |
> ... there has to be a proper & formal verification method for calculators, | |
> here I'm throwing spaghetti on the wall and checking if it sticks, | |
> but here goes: | |
*/ | |
import test from 'node:test' | |
import calc from '../calc.js' // <- update this, obv. | |
/* get potential fuckups outta the way first */ | |
test('Edge cases', async t => { | |
await t.test('whitespace at start', async t => { | |
await t.test('returns a correct result', async t => { | |
t.assert.strictEqual(calc('-10 + -5 '), -15) | |
}) | |
}) | |
await t.test('whitespace at end', async t => { | |
await t.test('returns correct result', async t => { | |
t.assert.strictEqual(calc(' -10 + -5'), -15) | |
}) | |
}) | |
await t.test('numeric value is a negative number', async t => { | |
await t.test('returns correct result', async t => { | |
t.assert.strictEqual(calc(' -10 + -5'), -15) | |
}) | |
}) | |
}) | |
/* Arithmetic Operations: Mixed */ | |
test('Mixed operation types', async t => { | |
await t.test('foo + bar - baz * qux / quux', async t => { | |
const result = calc('-10 + -5 - -0 * 5 / 10') | |
await t.test('correct', async t => { | |
t.assert.strictEqual(result, -15) | |
}) | |
}) | |
await t.test('foo - bar + baz * qux / quux', async t => { | |
const result = calc('-10 - -5 + -0 * 5 / 10') | |
await t.test('correct', async t => { | |
t.assert.strictEqual(result, -5) | |
}) | |
}) | |
await t.test('foo * bar + baz - qux / quux', async t => { | |
const result = calc('-10 * -5 + -0 - 5 / 10') | |
await t.test('correct', async t => { | |
t.assert.strictEqual(result, 49.5) | |
}) | |
}) | |
await t.test('foo / bar * baz + qux - quux', async t => { | |
const result = calc('-10 / -5 * -10 + -5 - -10') | |
await t.test('correct', async t => { | |
t.assert.strictEqual(result, -15) | |
}) | |
}) | |
}) | |
/* Uniform operations */ | |
test('Uniform operation type', async t => { | |
t.test('Addition', async t => { | |
await t.test('returns a number', async t => { | |
const type = typeof calc('-10 + -5') | |
t.assert.strictEqual(type, 'number') | |
}) | |
await t.test('arity 2', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 + -5'), -15) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 + -10'), -15) | |
}) | |
}) | |
}) | |
await t.test('arity 3', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 + -5 + -10'), -25) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 + -10 + -5'), -20) | |
}) | |
}) | |
}) | |
}) | |
await t.test('Subtraction', async t => { | |
await t.test('returns a number', async t => { | |
const type = typeof calc('-10 - -5') | |
t.assert.strictEqual(type, 'number') | |
}) | |
await t.test('arity 2', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 - -5'), -5) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 - -10'), 5) | |
}) | |
}) | |
}) | |
await t.test('arity 3', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 - -5 - -10'), 5) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 - -10 - -5'), 10) | |
}) | |
}) | |
}) | |
}) | |
await t.test('Multiplication', async t => { | |
await t.test('returns a number', async t => { | |
const type = typeof calc('-10 * -5') | |
t.assert.strictEqual(type, 'number') | |
}) | |
await t.test('arity 2', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 * -5'), 50) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 * -10'), 50) | |
}) | |
}) | |
}) | |
await t.test('arity 3', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 * -5 * -10'), -500) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 * -10 * -5'), -250) | |
}) | |
}) | |
}) | |
}) | |
await t.test('Division', async t => { | |
await t.test('returns a number', async t => { | |
const type = typeof calc('-10 / -5') | |
t.assert.strictEqual(type, 'number') | |
}) | |
await t.test('arity 2', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 / -5'), 2) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 / -10'), 0.5) | |
}) | |
}) | |
}) | |
await t.test('arity 3', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-10 * -5 * -10'), -500) | |
}) | |
await t.test('reverse operand order', async t => { | |
await t.test('correct', async t => { | |
t.assert.strictEqual(calc('-5 * -10 * -5'), -250) | |
}) | |
}) | |
}) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
test report