<div id="main" class="main ui">
<h1 class="ui centered inverted header">Simple React Calculator</h1>
<div id="content" class="ui segment"></div>
</div>
<div id="signature" class="ui three column centered grid">
<div class="ui raised inverted segment column">
<p class="ui small centered header">a project by parc6502 (Ibby) - <a href="http://github.com/parc6502">Github</a> | <a href="http://ibby.me">Website</a></p>
</div>
</div>
body {
background-color: #343a40;
}
#main {
margin-top: 3em;
}
#content {
width: 700px;
margin: 3em auto;
}
#calculatorScreen {
height: 60px;
}
#signature {
width: 100%;
}
/**
* I've separated the React code, which is essentially the UI controller, and the calculator algorithms.
*The calculator algorithms were all placed in a module-pattern IIFE at the bottom of the page with only the calculate method exposed
* (these would usually be in their own file).
**/
/* React-Related Code */
const operatorDictionary = {
add: '+',
subtract: '-',
multiply: 'x',
divide: '÷',
};
class CalculatorBody extends React.Component {
INITIAL_STATE = {
currentNumber: '',
currentCalculation: [],
ans: null,
};
state = {
this.INITIAL_STATE
};
handleNumberClick = number => {
var { ans, currentNumber, currentCalculation } = this.state;
var ansOnDisplay = ans !== null && currentNumber === '' && currentCalculation.length === 0;
var numberClickedZeroOnDisplay = (currentNumber === '0' || currentNumber === '') && number !== ".";
var decimalClickedZeroOnDisplay = currentNumber === '' && number === ".";
var decimalClickedDecimalInCurrentNumber = number === "." && currentNumber.includes('.');
if (decimalClickedDecimalInCurrentNumber) return;
if (ansOnDisplay || numberClickedZeroOnDisplay) {
this.setState({
currentNumber: `${number}`,
});
}
else if (decimalClickedZeroOnDisplay) {
this.setState({
currentNumber: `0${number}`,
});
}
else {
this.setState(prevState => {
return {
currentNumber: `${prevState.currentNumber}${number}`,
}
});
}
};
handleOperatorClick = operator => {
var { currentCalculation, currentNumber, ans } = this.state;
var consecutiveOperatorClick = Object.keys(operatorDictionary)
.includes(currentCalculation[currentCalculation.length-1]);
var ansOnDisplay = ans !== null && currentNumber === '' && currentCalculation.length === 0;
if (currentNumber !== '') {
// this.addToDisplay(operatorDictionary[operator]);
this.setState(prevState => {
return {
currentNumber: '',
currentCalculation: prevState.currentCalculation.concat([Number(prevState.currentNumber), operator])
}
});
}
else if (consecutiveOperatorClick) {
// console.log(this.state.display);
// var newDisplay = this.state.display.slice(0,-1) + operatorDictionary[operator];
// this.setDisplay(newDisplay);
this.setState(prevState => {
var newCalculation = prevState.currentCalculation.slice(0, -1).concat([operator]);
return {
currentCalculation: newCalculation,
}
})
}
else if (ansOnDisplay) {
// this.addToDisplay(operatorDictionary[operator]);
this.setState(prevState => {
return {
currentCalculation: prevState.currentCalculation.concat([prevState.ans, operator])
}
});
}
};
handleEqualsClick = () => {
if (this.state.currentNumber) {
let answer = calculator.calculate(this.state.currentCalculation.concat(Number(this.state.currentNumber)));
if (isFinite(answer)) {
this.setState({
currentNumber: '',
currentCalculation: [],
ans: answer,
});
}
else {
this.handleMathError();
}
}
// console.log(this.state.currentCalculation);
};
handleMathError = () => {
alert("Math Error! Resetting calculator");
this.resetCalculator();
};
handleClearClick = () => {
this.resetCalculator();
};
resetCalculator = () => {
this.setState({
this.INITIAL_STATE
});
}
render() {
return (
<div>
<CalculatorScreen ans={this.state.ans} currentCalculation={this.state.currentCalculation} currentNumber={this.state.currentNumber}/>
<ClearButton handleClick={this.handleClearClick} />
<div className="four ui buttons">
<NumberButton handleClick={this.handleNumberClick} number={7}/>
<NumberButton handleClick={this.handleNumberClick} number={8}/>
<NumberButton handleClick={this.handleNumberClick} number={9}/>
<OperatorButton handleClick={this.handleOperatorClick} operator="divide" />
</div>
<div className="four ui buttons">
<NumberButton handleClick={this.handleNumberClick} number={4}/>
<NumberButton handleClick={this.handleNumberClick} number={5}/>
<NumberButton handleClick={this.handleNumberClick} number={6}/>
<OperatorButton handleClick={this.handleOperatorClick} operator="multiply" />
</div>
<div className="four ui buttons">
<NumberButton handleClick={this.handleNumberClick} number={1}/>
<NumberButton handleClick={this.handleNumberClick} number={2}/>
<NumberButton handleClick={this.handleNumberClick} number={3}/>
<OperatorButton handleClick={this.handleOperatorClick} operator="subtract" />
</div>
<div className="four ui buttons">
<NumberButton handleClick={this.handleNumberClick} number='.' />
<NumberButton handleClick={this.handleNumberClick} number={0}/>
<EqualsButton handleClick={this.handleEqualsClick} />
<OperatorButton handleClick={this.handleOperatorClick} operator="add" />
</div>
</div>
);
}
};
// 20/09 update: display is now derived from other values
class CalculatorScreen extends React.Component {
prettify = calculation => calculation.map(val => Object.keys(operatorDictionary).includes(val) ? operatorDictionary[val] : val).join('');
render() {
const { ans, currentCalculation, currentNumber } = this.props;
const humanFriendlyCalculation = this.prettify(currentCalculation);
let display = currentNumber !== '' || currentCalculation.length > 0 ? `${humanFriendlyCalculation}${currentNumber}` : `${ans||'0'}`;
return (
<div className="ui message"><p id="display" className="ui right aligned header">{display}</p></div>
);
}
}
class NumberButton extends React.Component {
handleClick = () => {
this.props.handleClick(this.props.number);
};
render() {
var textNumbers = ["zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
return (
<button id={textNumbers[this.props.number]||'decimal'} className="ui large fluid black button" onClick={this.handleClick}>{this.props.number}</button>
);
}
}
class OperatorButton extends React.Component {
handleClick = () => {
this.props.handleClick(this.props.operator);
};
render() {
return (
<button id={this.props.operator} className="ui large fluid grey button" onClick={this.handleClick}>{operatorDictionary[this.props.operator]}</button>
);
}
}
class EqualsButton extends React.Component {
render() {
return (
<button id="equals" className="ui large fluid blue button" onClick={this.props.handleClick}>=</button>
);
}
}
class ClearButton extends React.Component {
render() {
return (
<button id='clear' className="ui fluid red button" onClick={this.props.handleClick}>Clear</button>
);
}
}
ReactDOM.render(
<CalculatorBody />,
document.getElementById('content')
);
/* Calculator IIFE */
// calculator.calculate is the only exposed method, and it takes in an array of numbers and operators (e.g. `[3, 'plus', 7, 'divide', 10]`) and returns the result.
// It doesn't accept brackets.
window.calculator = (function(){
var operators = {
'^': {
precedence: 4,
leftAssociative: false,
calculate(x, y) {
return Math.pow(x, y);
},
},
'multiply': {
precedence: 3,
leftAssociative: true,
calculate(x, y) {
return x * y;
},
},
'divide': {
precedence: 3,
leftAssociative: true,
calculate(x, y) {
return x / y;
},
},
'add': {
precedence: 2,
leftAssociative: true,
calculate(x, y) {
return x + y;
},
},
'subtract': {
precedence: 2,
leftAssociative: true,
calculate(x, y) {
return x - y;
},
},
};
var calculator = Object.assign(Object.create(operators),{
postfixEvaluator(calculationArray) {
var resultStack = [];
for (let i = 0; i < calculationArray.length; i++) {
let token = calculationArray[i];
if (this[token]) {
let operand_2 = resultStack.pop();
let operand_1 = resultStack.pop();
resultStack.push(this[token].calculate(operand_1, operand_2));
}
else {
resultStack.push(token);
}
}
return resultStack.pop();
},
shuntingYard(calculationArray) {
var output = [];
var oper = [];
for (let i = 0; i<calculationArray.length; i++) {
let token = calculationArray[i];
if (typeof token === 'number') {
output.push(token);
} else {
while (
oper.length > 0 &&
(this[oper[oper.length - 1]].precedence >
this[token].precedence ||
(this[oper[oper.length - 1]].precedence ===
this[token].precedence &&
this[oper[oper.length - 1]].leftAssociative))
) {
output.push(oper.pop());
}
oper.push(token);
}
}
while (oper.length > 0) {
output.push(oper.pop());
}
return output;
},
calculate(inputArray) {
var postfixArray = this.shuntingYard(inputArray);
// console.log(postfixArray);
return this.postfixEvaluator(postfixArray);
}
});
console.log(calculator.calculate([3, '-', 2])); // 1
return {
calculate: input => calculator.calculate(input),
};
})();
View Compiled
This Pen doesn't use any external CSS resources.