├── 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 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
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 |
--------------------------------------------------------------------------------