├── Logotype primary.png ├── public ├── favicon.ico ├── manifest.json └── index.html ├── src ├── logic │ ├── isNumber.js │ ├── operate.js │ ├── calculate.js │ └── calculate.test.js ├── component │ ├── App.css │ ├── App.test.js │ ├── Display.css │ ├── ButtonPanel.css │ ├── Display.js │ ├── Button.css │ ├── App.js │ ├── Button.js │ └── ButtonPanel.js ├── index.js └── index.css ├── .gitignore ├── README.md ├── package.json └── LICENSE /Logotype primary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewagain/calculator/HEAD/Logotype primary.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andrewagain/calculator/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /src/logic/isNumber.js: -------------------------------------------------------------------------------- 1 | export default function isNumber(item) { 2 | return /[0-9]+/.test(item); 3 | } 4 | -------------------------------------------------------------------------------- /src/component/App.css: -------------------------------------------------------------------------------- 1 | .component-app { 2 | display: flex; 3 | flex-direction: column; 4 | flex-wrap: wrap; 5 | height: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | coverage 8 | 9 | # production 10 | build 11 | 12 | # misc 13 | .DS_Store 14 | npm-debug.log 15 | -------------------------------------------------------------------------------- /src/component/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | }); 9 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./component/App"; 4 | import "./index.css"; 5 | import "github-fork-ribbon-css/gh-fork-ribbon.css"; 6 | 7 | ReactDOM.render(, document.getElementById("root")); 8 | -------------------------------------------------------------------------------- /src/component/Display.css: -------------------------------------------------------------------------------- 1 | .component-display { 2 | background-color: #858694; 3 | color: white; 4 | text-align: right; 5 | font-weight: 200; 6 | flex: 0 0 auto; 7 | width: 100%; 8 | } 9 | 10 | .component-display > div { 11 | font-size: 2.5rem; 12 | padding: 0.2rem 0.7rem 0.1rem 0.5rem; 13 | } 14 | -------------------------------------------------------------------------------- /src/component/ButtonPanel.css: -------------------------------------------------------------------------------- 1 | .component-button-panel { 2 | background-color: #858694; 3 | display: flex; 4 | flex-direction: row; 5 | flex-wrap: wrap; 6 | flex: 1 0 auto; 7 | } 8 | 9 | .component-button-panel > div { 10 | width: 100%; 11 | margin-bottom: 1px; 12 | flex: 1 0 auto; 13 | display: flex; 14 | } 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Calculator", 3 | "name": "React Calculator Example App", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/component/Display.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import "./Display.css"; 5 | 6 | export default class Display extends React.Component { 7 | static propTypes = { 8 | value: PropTypes.string, 9 | }; 10 | 11 | render() { 12 | return ( 13 |
14 |
{this.props.value}
15 |
16 | ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Calculator 2 | --- 3 | 4 | 5 | Created with *create-react-app*. See the [full create-react-app guide](https://github.com/facebookincubator/create-react-app/blob/master/packages/react-scripts/template/README.md). 6 | 7 | 8 | 9 | Try It 10 | --- 11 | 12 | [ahfarmer.github.io/calculator](https://ahfarmer.github.io/calculator/) 13 | 14 | 15 | 16 | Install 17 | --- 18 | 19 | `npm install` 20 | 21 | 22 | 23 | Usage 24 | --- 25 | 26 | `npm start` 27 | -------------------------------------------------------------------------------- /src/component/Button.css: -------------------------------------------------------------------------------- 1 | .component-button { 2 | display: inline-flex; 3 | width: 25%; 4 | flex: 1 0 auto; 5 | } 6 | 7 | .component-button.wide { 8 | width: 50%; 9 | } 10 | 11 | .component-button button { 12 | background-color: #e0e0e0; 13 | border: 0; 14 | font-size: 1.5rem; 15 | margin: 0 1px 0 0; 16 | flex: 1 0 auto; 17 | padding: 0; 18 | } 19 | 20 | .component-button:last-child button { 21 | margin-right: 0; 22 | } 23 | 24 | .component-button.orange button { 25 | background-color: #f5923e; 26 | color: white; 27 | } 28 | -------------------------------------------------------------------------------- /src/component/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Display from "./Display"; 3 | import ButtonPanel from "./ButtonPanel"; 4 | import calculate from "../logic/calculate"; 5 | import "./App.css"; 6 | 7 | export default class App extends React.Component { 8 | state = { 9 | total: null, 10 | next: null, 11 | operation: null, 12 | }; 13 | 14 | handleClick = buttonName => { 15 | this.setState(calculate(this.state, buttonName)); 16 | }; 17 | 18 | render() { 19 | return ( 20 |
21 | 22 | 23 |
24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/component/Button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import "./Button.css"; 4 | 5 | export default class Button extends React.Component { 6 | static propTypes = { 7 | name: PropTypes.string, 8 | orange: PropTypes.bool, 9 | wide: PropTypes.bool, 10 | clickHandler: PropTypes.func, 11 | }; 12 | 13 | handleClick = () => { 14 | this.props.clickHandler(this.props.name); 15 | }; 16 | 17 | render() { 18 | const className = [ 19 | "component-button", 20 | this.props.orange ? "orange" : "", 21 | this.props.wide ? "wide" : "", 22 | ]; 23 | 24 | return ( 25 |
26 | 27 |
28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/logic/operate.js: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | 3 | export default function operate(numberOne, numberTwo, operation) { 4 | const one = Big(numberOne || "0"); 5 | const two = Big(numberTwo || (operation === "÷" || operation === 'x' ? "1": "0")); //If dividing or multiplying, then 1 maintains current value in cases of null 6 | if (operation === "+") { 7 | return one.plus(two).toString(); 8 | } 9 | if (operation === "-") { 10 | return one.minus(two).toString(); 11 | } 12 | if (operation === "x") { 13 | return one.times(two).toString(); 14 | } 15 | if (operation === "÷") { 16 | if (two === "0") { 17 | alert("Divide by 0 error"); 18 | return "0"; 19 | } else { 20 | return one.div(two).toString(); 21 | } 22 | } 23 | throw Error(`Unknown operation '${operation}'`); 24 | } 25 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | font-size: 10px; 4 | } 5 | 6 | body { 7 | background-color: black; 8 | margin: 0; 9 | padding: 0; 10 | font-family: sans-serif; 11 | height: 100%; 12 | } 13 | 14 | #root { 15 | height: 100%; 16 | } 17 | 18 | body .github-fork-ribbon:before { 19 | background-color: #333; 20 | } 21 | 22 | @media screen and (max-width: 400px) { 23 | .github-fork-ribbon { 24 | display: none; 25 | } 26 | } 27 | 28 | @media (min-width: 400px) and (min-height: 400px) { 29 | html { 30 | font-size: 20px; 31 | } 32 | } 33 | 34 | @media (min-width: 500px) and (min-height: 500px) { 35 | html { 36 | font-size: 30px; 37 | } 38 | } 39 | 40 | @media (min-width: 600px) and (min-height: 600px) { 41 | html { 42 | font-size: 40px; 43 | } 44 | } 45 | 46 | @media (min-width: 800px) and (min-height: 800px) { 47 | html { 48 | font-size: 50px; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calculator", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "homepage": "http://ahfarmer.github.io/calculator", 6 | "devDependencies": { 7 | "chai": "^4.2.0", 8 | "gh-pages": "^2.0.1", 9 | "prettier": "^1.17.1", 10 | "react-scripts": "^3.0.1" 11 | }, 12 | "dependencies": { 13 | "big.js": "^5.2.2", 14 | "github-fork-ribbon-css": "^0.2.1", 15 | "react": "^16.8.6", 16 | "react-dom": "^16.8.6" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test --env=jsdom", 22 | "eject": "react-scripts eject", 23 | "deploy": "gh-pages -d build" 24 | }, 25 | "prettier": { 26 | "trailingComma": "all" 27 | }, 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Andrew H Farmer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | React Calculator 9 | 10 | 11 | 14 |
15 | 19 | Fork me on GitHub 20 | 21 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/component/ButtonPanel.js: -------------------------------------------------------------------------------- 1 | import Button from "./Button"; 2 | import React from "react"; 3 | import PropTypes from "prop-types"; 4 | 5 | import "./ButtonPanel.css"; 6 | 7 | export default class ButtonPanel extends React.Component { 8 | static propTypes = { 9 | clickHandler: PropTypes.func, 10 | }; 11 | 12 | handleClick = buttonName => { 13 | this.props.clickHandler(buttonName); 14 | }; 15 | 16 | render() { 17 | return ( 18 |
19 |
20 |
25 |
26 |
31 |
32 |
37 |
38 |
43 |
44 |
48 |
49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/logic/calculate.js: -------------------------------------------------------------------------------- 1 | import Big from "big.js"; 2 | 3 | import operate from "./operate"; 4 | import isNumber from "./isNumber"; 5 | 6 | /** 7 | * Given a button name and a calculator data object, return an updated 8 | * calculator data object. 9 | * 10 | * Calculator data object contains: 11 | * total:String the running total 12 | * next:String the next number to be operated on with the total 13 | * operation:String +, -, etc. 14 | */ 15 | export default function calculate(obj, buttonName) { 16 | if (buttonName === "AC") { 17 | return { 18 | total: null, 19 | next: null, 20 | operation: null, 21 | }; 22 | } 23 | 24 | if (isNumber(buttonName)) { 25 | if (buttonName === "0" && obj.next === "0") { 26 | return {}; 27 | } 28 | // If there is an operation, update next 29 | if (obj.operation) { 30 | if (obj.next) { 31 | return { next: obj.next + buttonName }; 32 | } 33 | return { next: buttonName }; 34 | } 35 | // If there is no operation, update next and clear the value 36 | if (obj.next) { 37 | const next = obj.next === "0" ? buttonName : obj.next + buttonName; 38 | return { 39 | next, 40 | total: null, 41 | }; 42 | } 43 | return { 44 | next: buttonName, 45 | total: null, 46 | }; 47 | } 48 | 49 | if (buttonName === "%") { 50 | if (obj.operation && obj.next) { 51 | const result = operate(obj.total, obj.next, obj.operation); 52 | return { 53 | total: Big(result) 54 | .div(Big("100")) 55 | .toString(), 56 | next: null, 57 | operation: null, 58 | }; 59 | } 60 | if (obj.next) { 61 | return { 62 | next: Big(obj.next) 63 | .div(Big("100")) 64 | .toString(), 65 | }; 66 | } 67 | return {}; 68 | } 69 | 70 | if (buttonName === ".") { 71 | if (obj.next) { 72 | // ignore a . if the next number already has one 73 | if (obj.next.includes(".")) { 74 | return {}; 75 | } 76 | return { next: obj.next + "." }; 77 | } 78 | return { next: "0." }; 79 | } 80 | 81 | if (buttonName === "=") { 82 | if (obj.next && obj.operation) { 83 | return { 84 | total: operate(obj.total, obj.next, obj.operation), 85 | next: null, 86 | operation: null, 87 | }; 88 | } else { 89 | // '=' with no operation, nothing to do 90 | return {}; 91 | } 92 | } 93 | 94 | if (buttonName === "+/-") { 95 | if (obj.next) { 96 | return { next: (-1 * parseFloat(obj.next)).toString() }; 97 | } 98 | if (obj.total) { 99 | return { total: (-1 * parseFloat(obj.total)).toString() }; 100 | } 101 | return {}; 102 | } 103 | 104 | // Button must be an operation 105 | 106 | // When the user presses an operation button without having entered 107 | // a number first, do nothing. 108 | // if (!obj.next && !obj.total) { 109 | // return {}; 110 | // } 111 | 112 | // User pressed an operation button and there is an existing operation 113 | if (obj.operation) { 114 | return { 115 | total: operate(obj.total, obj.next, obj.operation), 116 | next: null, 117 | operation: buttonName, 118 | }; 119 | } 120 | 121 | // no operation yet, but the user typed one 122 | 123 | // The user hasn't typed a number yet, just save the operation 124 | if (!obj.next) { 125 | return { operation: buttonName }; 126 | } 127 | 128 | // save the operation and shift 'next' into 'total' 129 | return { 130 | total: obj.next, 131 | next: null, 132 | operation: buttonName, 133 | }; 134 | } 135 | -------------------------------------------------------------------------------- /src/logic/calculate.test.js: -------------------------------------------------------------------------------- 1 | import calculate from "./calculate"; 2 | import chai from "chai"; 3 | 4 | // https://github.com/chaijs/chai/issues/469 5 | chai.config.truncateThreshold = 0; 6 | 7 | const expect = chai.expect; 8 | 9 | function pressButtons(buttons) { 10 | const value = {}; 11 | buttons.forEach(button => { 12 | Object.assign(value, calculate(value, button)); 13 | }); 14 | // no need to distinguish between null and undefined values 15 | Object.keys(value).forEach(key => { 16 | if (value[key] === null) { 17 | delete value[key]; 18 | } 19 | }); 20 | return value; 21 | } 22 | 23 | function expectButtons(buttons, expectation) { 24 | expect(pressButtons(buttons)).to.deep.equal(expectation); 25 | } 26 | 27 | function test(buttons, expectation, only = false) { 28 | const func = only ? it.only : it; 29 | func(`buttons ${buttons.join(",")} -> ${JSON.stringify(expectation)}`, () => { 30 | expectButtons(buttons, expectation); 31 | }); 32 | } 33 | 34 | describe("calculate", function() { 35 | test(["6"], { next: "6" }); 36 | 37 | test(["6", "6"], { next: "66" }); 38 | 39 | test(["6", "+", "6"], { 40 | next: "6", 41 | total: "6", 42 | operation: "+", 43 | }); 44 | 45 | test(["6", "+", "6", "="], { 46 | total: "12", 47 | }); 48 | 49 | test(["0", "0", "+", "0", "="], { 50 | total: "0", 51 | }); 52 | 53 | test(["6", "+", "6", "=", "9"], { 54 | next: "9", 55 | }); 56 | 57 | test(["3", "+", "6", "=", "+"], { 58 | total: "9", 59 | operation: "+", 60 | }); 61 | 62 | test(["3", "+", "6", "=", "+", "9"], { 63 | total: "9", 64 | operation: "+", 65 | next: "9", 66 | }); 67 | 68 | test(["3", "+", "6", "=", "+", "9", "="], { 69 | total: "18", 70 | }); 71 | 72 | // When '=' is pressed and there is not enough information to complete 73 | // an operation, the '=' should be disregarded. 74 | test(["3", "+", "=", "3", "="], { 75 | total: "6", 76 | }); 77 | 78 | test(["+"], { 79 | operation: "+", 80 | }); 81 | 82 | test(["+", "2"], { 83 | next: "2", 84 | operation: "+", 85 | }); 86 | 87 | test(["+", "2", "+"], { 88 | total: "2", 89 | operation: "+", 90 | }); 91 | 92 | test(["+", "2", "+", "+"], { 93 | total: "2", 94 | operation: "+", 95 | }); 96 | 97 | test(["+", "2", "+", "5"], { 98 | next: "5", 99 | total: "2", 100 | operation: "+", 101 | }); 102 | 103 | test(["+", "2", "5"], { 104 | next: "25", 105 | operation: "+", 106 | }); 107 | 108 | test(["+", "2", "5"], { 109 | next: "25", 110 | operation: "+", 111 | }); 112 | 113 | test(["+", "6", "+", "5", "="], { 114 | total: "11", 115 | }); 116 | 117 | test(["0", ".", "4"], { 118 | next: "0.4", 119 | }); 120 | 121 | test([".", "4"], { 122 | next: "0.4", 123 | }); 124 | 125 | test([".", "4", "-", ".", "2"], { 126 | total: "0.4", 127 | next: "0.2", 128 | operation: "-", 129 | }); 130 | 131 | test([".", "4", "-", ".", "2", "="], { 132 | total: "0.2", 133 | }); 134 | 135 | // should clear the operator when AC is pressed 136 | test(["1", "+", "2", "AC"], {}); 137 | test(["+", "2", "AC"], {}); 138 | 139 | test(["4", "%"], { 140 | next: "0.04", 141 | }); 142 | 143 | test(["4", "%", "x", "2", "="], { 144 | total: "0.08", 145 | }); 146 | 147 | test(["4", "%", "x", "2"], { 148 | total: "0.04", 149 | operation: "x", 150 | next: "2", 151 | }); 152 | 153 | // the percentage sign should also act as '=' 154 | test(["2", "x", "2", "%"], { 155 | total: "0.04", 156 | }); 157 | 158 | //Test that pressing the multiplication or division sign multiple times should not affect the current computation 159 | test(["2", "x", "x"], { 160 | total: "2", 161 | operation: "x" 162 | }); 163 | 164 | test(["2", "÷", "÷"], { 165 | total: "2", 166 | operation: "÷" 167 | }); 168 | 169 | test(["2", "÷", "x", "+", "-", "x"], { 170 | total: "2", 171 | operation: 'x' 172 | }); 173 | }); 174 | --------------------------------------------------------------------------------