inital Jack
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
.git/
|
||||||
|
*.sh
|
||||||
|
*.bat
|
||||||
|
README.md
|
||||||
|
compose.yaml
|
||||||
|
Dockerfile
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
# Use a lightweight Nginx image
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy the static files to the Nginx html directory
|
||||||
|
COPY . /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Expose port 80
|
||||||
|
EXPOSE 80
|
||||||
|
|
||||||
|
# Start Nginx
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Calculator
|
||||||
|
|
||||||
|
A simple, scientific calculator that runs in your browser.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
This project can be run in a few different ways, and dependencies vary based on the method you choose.
|
||||||
|
|
||||||
|
- **Browser Only**: No dependencies, but the calculator will not be served from a webserver.
|
||||||
|
- **Python**: To serve the `index.html` file, you'll need Python installed.
|
||||||
|
- **Docker**: To run the project in a container, you'll need Docker installed.
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
- **With Python**: Run `start-server.bat`. This will start a Python web server and open the calculator in your default browser.
|
||||||
|
- **With Docker**: Run `run-docker.bat`. This will use Docker Compose to build and run the container.
|
||||||
|
|
||||||
|
### Linux / macOS
|
||||||
|
|
||||||
|
- **With Python**: Run `./start-server.sh`. You may need to make it executable first with `chmod +x start-server.sh`. This will start a Python web server and open the calculator in your default browser.
|
||||||
|
- **With Docker**: Run `./run-docker.sh`. You may need to make it executable first with `chmod +x run-docker.sh`. This will use Docker Compose to build and run the container.
|
||||||
|
|
||||||
|
## Server Setup
|
||||||
|
|
||||||
|
To deploy the calculator on a server, you only need Docker and Docker Compose installed. You do not need to clone this repository.
|
||||||
|
|
||||||
|
1. **Install Docker and Docker Compose**: Follow the official installation instructions for your server's operating system.
|
||||||
|
|
||||||
|
2. **Create a `compose.yaml` file**: On your server, create a new file named `compose.yaml` and add the following content:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: ghcr.io/jones013/jack_calculator:latest
|
||||||
|
ports:
|
||||||
|
- "8888:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Start the container**: In the same directory where you created the `compose.yaml` file, run the following command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-d` flag runs the container in detached mode. The `restart: unless-stopped` policy ensures that the container will automatically restart if the server is rebooted.
|
||||||
|
|
||||||
|
The calculator will then be available at `http://<your-server-ip>:8888`.
|
||||||
|
|
||||||
|
**Note on Firewalls**: Ensure that your server's firewall is configured to allow incoming traffic on port 8888.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Branches
|
||||||
|
|
||||||
|
Use `**main`** or `**user**` for this layout. Maintainer tooling and full docs live on the `**developer**` branch.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build: .
|
||||||
|
image: calculator:latest
|
||||||
|
ports:
|
||||||
|
- "8888:80"
|
||||||
|
restart: unless-stopped
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
+671
@@ -0,0 +1,671 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Scientific Calculator</title>
|
||||||
|
<link rel="icon" type="image/png" href="images/gigachad.png" />
|
||||||
|
<link rel="apple-touch-icon" href="images/gigachad.png" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0f1419;
|
||||||
|
--surface: #1a2332;
|
||||||
|
--key: #243044;
|
||||||
|
--key-hover: #2d3d54;
|
||||||
|
--accent: #3d8bfd;
|
||||||
|
--accent-hover: #5a9dff;
|
||||||
|
--text: #e8eef7;
|
||||||
|
--muted: #8a9bb0;
|
||||||
|
--danger: #e85d5d;
|
||||||
|
--sci: #1e2a3d;
|
||||||
|
--sci-hover: #263548;
|
||||||
|
--radius: 10px;
|
||||||
|
font-family: "Segoe UI", system-ui, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: radial-gradient(ellipse 120% 80% at 50% -20%, #1e3a5f 0%, var(--bg) 55%);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc {
|
||||||
|
width: min(100% - 2rem, 680px);
|
||||||
|
padding: 1.25rem;
|
||||||
|
background: var(--surface);
|
||||||
|
border-radius: calc(var(--radius) + 4px);
|
||||||
|
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45), 0 0 0 1px rgba(255, 255, 255, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 320px;
|
||||||
|
margin: 0 auto 0.75rem;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 3px solid var(--accent);
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
max-height: 250px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calc-panel {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0 0 0.65rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-wrap {
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 0.65rem 0.85rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
min-height: 3.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.expression {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--muted);
|
||||||
|
min-height: 1.15em;
|
||||||
|
word-break: break-all;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
font-weight: 500;
|
||||||
|
word-break: break-all;
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn {
|
||||||
|
font: inherit;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding: 0.35rem 0.65rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
border-radius: 999px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--sci);
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mode-btn:hover {
|
||||||
|
background: var(--sci-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys.sci {
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
margin-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.keys.main {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
font: inherit;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 0.72rem 0.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--key);
|
||||||
|
color: var(--text);
|
||||||
|
transition: background 0.12s ease, transform 0.08s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.sci {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
padding: 0.55rem 0.15rem;
|
||||||
|
background: var(--sci);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.sci:hover {
|
||||||
|
background: var(--sci-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background: var(--key-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.op {
|
||||||
|
background: #2a3548;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.op:hover {
|
||||||
|
background: #33425a;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.equals {
|
||||||
|
background: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
grid-row: span 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.equals:hover {
|
||||||
|
background: var(--accent-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.danger {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
button.zero {
|
||||||
|
grid-column: span 2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="calc" role="application" aria-label="Scientific calculator">
|
||||||
|
<div class="hero-wrap">
|
||||||
|
<img
|
||||||
|
class="hero-img"
|
||||||
|
src="images/gigachad.png"
|
||||||
|
alt="GigaChad meme portrait"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="calc-panel">
|
||||||
|
<h1>Scientific calculator</h1>
|
||||||
|
<div class="mode-row">
|
||||||
|
<button type="button" class="mode-btn" id="degBtn" title="Toggle angle units for sin/cos/tan and inverse trig">DEG</button>
|
||||||
|
</div>
|
||||||
|
<div class="display-wrap">
|
||||||
|
<div class="expression" id="expr" aria-live="polite"></div>
|
||||||
|
<div class="result" id="out">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="keys sci">
|
||||||
|
<button type="button" class="sci danger" data-action="clear">C</button>
|
||||||
|
<button type="button" class="sci" data-action="back">⌫</button>
|
||||||
|
<button type="button" class="sci op" data-append="(">(</button>
|
||||||
|
<button type="button" class="sci op" data-append=")">)</button>
|
||||||
|
<button type="button" class="sci op" data-action="ans">ANS</button>
|
||||||
|
<button type="button" class="sci" data-append="sin(">sin</button>
|
||||||
|
<button type="button" class="sci" data-append="cos(">cos</button>
|
||||||
|
<button type="button" class="sci" data-append="tan(">tan</button>
|
||||||
|
<button type="button" class="sci" data-append="asin(">asin</button>
|
||||||
|
<button type="button" class="sci" data-append="acos(">acos</button>
|
||||||
|
<button type="button" class="sci" data-append="atan(">atan</button>
|
||||||
|
<button type="button" class="sci" data-append="log(">log</button>
|
||||||
|
<button type="button" class="sci" data-append="ln(">ln</button>
|
||||||
|
<button type="button" class="sci" data-append="sqrt(">√</button>
|
||||||
|
<button type="button" class="sci op" data-append="^">x^y</button>
|
||||||
|
<button type="button" class="sci op" data-append="^2">x²</button>
|
||||||
|
<button type="button" class="sci op" data-append="^(-1)">1/x</button>
|
||||||
|
<button type="button" class="sci op" data-append="pi">π</button>
|
||||||
|
<button type="button" class="sci op" data-append="e">e</button>
|
||||||
|
<button type="button" class="sci op" data-append="!">!</button>
|
||||||
|
</div>
|
||||||
|
<div class="keys main">
|
||||||
|
<button type="button" class="op" data-append="/">÷</button>
|
||||||
|
<button type="button" class="op" data-append="*">×</button>
|
||||||
|
<button type="button" class="op" data-append="-">−</button>
|
||||||
|
<button type="button" class="op" data-append="+">+</button>
|
||||||
|
<button type="button" data-digit="7">7</button>
|
||||||
|
<button type="button" data-digit="8">8</button>
|
||||||
|
<button type="button" data-digit="9">9</button>
|
||||||
|
<button type="button" class="op equals" data-action="equals">=</button>
|
||||||
|
<button type="button" data-digit="4">4</button>
|
||||||
|
<button type="button" data-digit="5">5</button>
|
||||||
|
<button type="button" data-digit="6">6</button>
|
||||||
|
<button type="button" data-digit="1">1</button>
|
||||||
|
<button type="button" data-digit="2">2</button>
|
||||||
|
<button type="button" data-digit="3">3</button>
|
||||||
|
<button type="button" class="zero" data-digit="0">0</button>
|
||||||
|
<button type="button" data-action="dot">.</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
const exprEl = document.getElementById("expr");
|
||||||
|
const outEl = document.getElementById("out");
|
||||||
|
const degBtn = document.getElementById("degBtn");
|
||||||
|
|
||||||
|
let exprStr = "";
|
||||||
|
let lastResult = 0;
|
||||||
|
let degMode = true;
|
||||||
|
|
||||||
|
function formatDisplay(n) {
|
||||||
|
if (!Number.isFinite(n)) return "Error";
|
||||||
|
const s = String(n);
|
||||||
|
if (s.length > 16) return n.toPrecision(12).replace(/\.?0+e/, "e");
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
function prettyExpr(s) {
|
||||||
|
if (!s) return "";
|
||||||
|
return s
|
||||||
|
.replace(/\*/g, "×")
|
||||||
|
.replace(/\//g, "÷")
|
||||||
|
.replace(/pi/g, "π");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateView() {
|
||||||
|
exprEl.textContent = prettyExpr(exprStr);
|
||||||
|
outEl.textContent = formatDisplay(lastResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
function factorial(n) {
|
||||||
|
if (!Number.isFinite(n)) throw new Error("domain");
|
||||||
|
if (n < 0 || Math.abs(n - Math.round(n)) > 1e-12) throw new Error("domain");
|
||||||
|
const k = Math.round(n);
|
||||||
|
if (k > 170) throw new Error("overflow");
|
||||||
|
let r = 1;
|
||||||
|
for (let i = 2; i <= k; i++) r *= i;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFunc(name, x) {
|
||||||
|
const rad = (d) => (d * Math.PI) / 180;
|
||||||
|
const deg = (r) => (r * 180) / Math.PI;
|
||||||
|
switch (name) {
|
||||||
|
case "sin":
|
||||||
|
return degMode ? Math.sin(rad(x)) : Math.sin(x);
|
||||||
|
case "cos":
|
||||||
|
return degMode ? Math.cos(rad(x)) : Math.cos(x);
|
||||||
|
case "tan":
|
||||||
|
return degMode ? Math.tan(rad(x)) : Math.tan(x);
|
||||||
|
case "asin":
|
||||||
|
if (degMode) return deg(Math.asin(x));
|
||||||
|
return Math.asin(x);
|
||||||
|
case "acos":
|
||||||
|
if (degMode) return deg(Math.acos(x));
|
||||||
|
return Math.acos(x);
|
||||||
|
case "atan":
|
||||||
|
if (degMode) return deg(Math.atan(x));
|
||||||
|
return Math.atan(x);
|
||||||
|
case "log":
|
||||||
|
return Math.log10(x);
|
||||||
|
case "ln":
|
||||||
|
return Math.log(x);
|
||||||
|
case "sqrt":
|
||||||
|
return Math.sqrt(x);
|
||||||
|
case "abs":
|
||||||
|
return Math.abs(x);
|
||||||
|
default:
|
||||||
|
throw new Error("fn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tokenize(str) {
|
||||||
|
const tokens = [];
|
||||||
|
let i = 0;
|
||||||
|
const peek = () => str[i];
|
||||||
|
const readNum = () => {
|
||||||
|
let start = i;
|
||||||
|
if (peek() === ".") {
|
||||||
|
i++;
|
||||||
|
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||||||
|
} else {
|
||||||
|
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||||||
|
if (peek() === ".") {
|
||||||
|
i++;
|
||||||
|
while (i < str.length && /[0-9]/.test(str[i])) i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const raw = str.slice(start, i);
|
||||||
|
if (raw === "." || raw === "") throw new Error("num");
|
||||||
|
const v = parseFloat(raw);
|
||||||
|
if (!Number.isFinite(v)) throw new Error("num");
|
||||||
|
return { type: "number", value: v };
|
||||||
|
};
|
||||||
|
|
||||||
|
while (i < str.length) {
|
||||||
|
const c = peek();
|
||||||
|
if (/\s/.test(c)) {
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/[0-9.]/.test(c)) {
|
||||||
|
tokens.push(readNum());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (/[a-zA-Z]/.test(c)) {
|
||||||
|
let j = i;
|
||||||
|
while (j < str.length && /[a-zA-Z]/.test(str[j])) j++;
|
||||||
|
const id = str.slice(i, j);
|
||||||
|
i = j;
|
||||||
|
if (id === "pi") tokens.push({ type: "const", name: "pi" });
|
||||||
|
else if (id === "e") tokens.push({ type: "const", name: "e" });
|
||||||
|
else if (
|
||||||
|
["sin", "cos", "tan", "asin", "acos", "atan", "log", "ln", "sqrt", "abs"].includes(id)
|
||||||
|
) {
|
||||||
|
tokens.push({ type: "func", name: id });
|
||||||
|
} else throw new Error("id");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "(") {
|
||||||
|
tokens.push({ type: "lparen" });
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === ")") {
|
||||||
|
tokens.push({ type: "rparen" });
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (c === "!") {
|
||||||
|
tokens.push({ type: "fact" });
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ("+-*/^".includes(c)) {
|
||||||
|
tokens.push({ type: "op", v: c });
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new Error("tok");
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertImplicitMul(tokens) {
|
||||||
|
const out = [];
|
||||||
|
const needs = (a, b) => {
|
||||||
|
if (!a || !b) return false;
|
||||||
|
const end = ["number", "rparen", "fact"];
|
||||||
|
const start = ["number", "lparen", "func", "const"];
|
||||||
|
return end.includes(a.type) && start.includes(b.type);
|
||||||
|
};
|
||||||
|
for (let k = 0; k < tokens.length; k++) {
|
||||||
|
const t = tokens[k];
|
||||||
|
if (k > 0 && needs(out[out.length - 1], t)) {
|
||||||
|
out.push({ type: "op", v: "*" });
|
||||||
|
}
|
||||||
|
out.push(t);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAndEval(tokens) {
|
||||||
|
let p = 0;
|
||||||
|
const peek = () => tokens[p];
|
||||||
|
const take = () => tokens[p++];
|
||||||
|
|
||||||
|
function parseAdditive() {
|
||||||
|
let left = parseMultiplicative();
|
||||||
|
while (peek()?.type === "op" && (peek().v === "+" || peek().v === "-")) {
|
||||||
|
const op = take().v;
|
||||||
|
const right = parseMultiplicative();
|
||||||
|
left = op === "+" ? left + right : left - right;
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMultiplicative() {
|
||||||
|
let left = parsePower();
|
||||||
|
while (peek()?.type === "op" && (peek().v === "*" || peek().v === "/")) {
|
||||||
|
const op = take().v;
|
||||||
|
const right = parsePower();
|
||||||
|
left = op === "*" ? left * right : left / right;
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePower() {
|
||||||
|
let left = parseUnary();
|
||||||
|
if (peek()?.type === "op" && peek().v === "^") {
|
||||||
|
take();
|
||||||
|
const right = parsePower();
|
||||||
|
left = Math.pow(left, right);
|
||||||
|
}
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseUnary() {
|
||||||
|
if (peek()?.type === "op" && peek().v === "-") {
|
||||||
|
take();
|
||||||
|
return -parseUnary();
|
||||||
|
}
|
||||||
|
if (peek()?.type === "op" && peek().v === "+") {
|
||||||
|
take();
|
||||||
|
return parseUnary();
|
||||||
|
}
|
||||||
|
return parsePostfix();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePostfix() {
|
||||||
|
let val = parsePrimary();
|
||||||
|
while (peek()?.type === "fact") {
|
||||||
|
take();
|
||||||
|
val = factorial(val);
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePrimary() {
|
||||||
|
const t = peek();
|
||||||
|
if (!t) throw new Error("exp");
|
||||||
|
if (t.type === "number") {
|
||||||
|
take();
|
||||||
|
return t.value;
|
||||||
|
}
|
||||||
|
if (t.type === "const") {
|
||||||
|
take();
|
||||||
|
return t.name === "pi" ? Math.PI : Math.E;
|
||||||
|
}
|
||||||
|
if (t.type === "func") {
|
||||||
|
const name = take().name;
|
||||||
|
if (peek()?.type !== "lparen") throw new Error("(");
|
||||||
|
take();
|
||||||
|
const inner = parseAdditive();
|
||||||
|
if (peek()?.type !== "rparen") throw new Error(")");
|
||||||
|
take();
|
||||||
|
return applyFunc(name, inner);
|
||||||
|
}
|
||||||
|
if (t.type === "lparen") {
|
||||||
|
take();
|
||||||
|
const inner = parseAdditive();
|
||||||
|
if (peek()?.type !== "rparen") throw new Error(")");
|
||||||
|
take();
|
||||||
|
return inner;
|
||||||
|
}
|
||||||
|
throw new Error("prim");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = parseAdditive();
|
||||||
|
if (peek()) throw new Error("trail");
|
||||||
|
if (!Number.isFinite(result)) throw new Error("nan");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluate(raw) {
|
||||||
|
const sub = String(raw).trim();
|
||||||
|
if (!sub) throw new Error("empty");
|
||||||
|
let s = sub.replace(/\bANS\b/g, "(" + String(lastResult) + ")");
|
||||||
|
const tokens = insertImplicitMul(tokenize(s));
|
||||||
|
return parseAndEval(tokens);
|
||||||
|
}
|
||||||
|
|
||||||
|
function lastChar() {
|
||||||
|
return exprStr.slice(-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldInsertMul(next) {
|
||||||
|
if (!exprStr) return false;
|
||||||
|
const a = lastChar();
|
||||||
|
const b = next[0];
|
||||||
|
if (/[0-9]/.test(a) && /[0-9]/.test(b)) return false;
|
||||||
|
if (a === "^") return false;
|
||||||
|
const aEnd = /[0-9\)\!]/.test(a);
|
||||||
|
const bStart = /[\(0-9a-zA-Z]/.test(b) && b !== "!";
|
||||||
|
return aEnd && bStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
function append(s) {
|
||||||
|
if (shouldInsertMul(s)) exprStr += "*";
|
||||||
|
exprStr += s;
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputDigit(d) {
|
||||||
|
append(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
function inputDot() {
|
||||||
|
append(".");
|
||||||
|
}
|
||||||
|
|
||||||
|
function doEval() {
|
||||||
|
try {
|
||||||
|
const v = evaluate(exprStr);
|
||||||
|
lastResult = v;
|
||||||
|
exprStr = "";
|
||||||
|
updateView();
|
||||||
|
} catch {
|
||||||
|
lastResult = NaN;
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
exprStr = "";
|
||||||
|
lastResult = 0;
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
function back() {
|
||||||
|
exprStr = exprStr.slice(0, -1);
|
||||||
|
updateView();
|
||||||
|
}
|
||||||
|
|
||||||
|
function insertAns() {
|
||||||
|
append("ANS");
|
||||||
|
}
|
||||||
|
|
||||||
|
degBtn.addEventListener("click", function () {
|
||||||
|
degMode = !degMode;
|
||||||
|
degBtn.textContent = degMode ? "DEG" : "RAD";
|
||||||
|
degBtn.title = degMode
|
||||||
|
? "Angles in degrees (click for radians)"
|
||||||
|
: "Angles in radians (click for degrees)";
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector(".calc").addEventListener("click", function (e) {
|
||||||
|
const btn = e.target.closest("button");
|
||||||
|
if (!btn) return;
|
||||||
|
if (btn === degBtn) return;
|
||||||
|
|
||||||
|
const ap = btn.getAttribute("data-append");
|
||||||
|
const d = btn.getAttribute("data-digit");
|
||||||
|
const action = btn.getAttribute("data-action");
|
||||||
|
|
||||||
|
if (action === "equals") {
|
||||||
|
doEval();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (d !== null) {
|
||||||
|
inputDigit(d);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ap !== null) {
|
||||||
|
append(ap);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (action === "clear") clear();
|
||||||
|
else if (action === "back") back();
|
||||||
|
else if (action === "dot") inputDot();
|
||||||
|
else if (action === "ans") insertAns();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key >= "0" && e.key <= "9") {
|
||||||
|
e.preventDefault();
|
||||||
|
inputDigit(e.key);
|
||||||
|
} else if (e.key === ".") {
|
||||||
|
e.preventDefault();
|
||||||
|
inputDot();
|
||||||
|
} else if (e.key === "+") {
|
||||||
|
e.preventDefault();
|
||||||
|
append("+");
|
||||||
|
} else if (e.key === "-") {
|
||||||
|
e.preventDefault();
|
||||||
|
append("-");
|
||||||
|
} else if (e.key === "*") {
|
||||||
|
e.preventDefault();
|
||||||
|
append("*");
|
||||||
|
} else if (e.key === "/") {
|
||||||
|
e.preventDefault();
|
||||||
|
append("/");
|
||||||
|
} else if (e.key === "^") {
|
||||||
|
e.preventDefault();
|
||||||
|
append("^");
|
||||||
|
} else if (e.key === "(") {
|
||||||
|
e.preventDefault();
|
||||||
|
append("(");
|
||||||
|
} else if (e.key === ")") {
|
||||||
|
e.preventDefault();
|
||||||
|
append(")");
|
||||||
|
} else if (e.key === "Enter" || (e.key === "=" && !e.shiftKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
doEval();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
clear();
|
||||||
|
} else if (e.key === "Backspace") {
|
||||||
|
e.preventDefault();
|
||||||
|
back();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updateView();
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
where docker >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Docker not found. Install Docker Desktop.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Calculator: http://localhost:8888
|
||||||
|
start "" "http://localhost:8888"
|
||||||
|
docker compose up
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "Docker not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Calculator: http://localhost:8888"
|
||||||
|
(
|
||||||
|
sleep 2
|
||||||
|
command -v xdg-open >/dev/null 2>&1 && xdg-open "http://localhost:8888" 2>/dev/null || true
|
||||||
|
command -v open >/dev/null 2>&1 && open "http://localhost:8888" 2>/dev/null || true
|
||||||
|
) &
|
||||||
|
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
exec docker compose up
|
||||||
|
fi
|
||||||
|
if command -v docker-compose >/dev/null 2>&1; then
|
||||||
|
exec docker-compose up
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Docker Compose not found."
|
||||||
|
exit 1
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
where python >nul 2>&1
|
||||||
|
if %errorlevel% equ 0 (
|
||||||
|
set "PY=python"
|
||||||
|
) else (
|
||||||
|
where py >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Python not found. Install from https://www.python.org/
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
set "PY=py"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Calculator: http://localhost:8888
|
||||||
|
start "" "http://localhost:8888"
|
||||||
|
"%PY%" -m http.server 8888 --bind 127.0.0.1
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
|
||||||
|
if command -v python3 >/dev/null 2>&1; then
|
||||||
|
PY=python3
|
||||||
|
elif command -v python >/dev/null 2>&1; then
|
||||||
|
PY=python
|
||||||
|
else
|
||||||
|
echo "Python not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Calculator: http://localhost:8888"
|
||||||
|
(
|
||||||
|
sleep 1
|
||||||
|
command -v xdg-open >/dev/null 2>&1 && xdg-open "http://localhost:8888" 2>/dev/null || true
|
||||||
|
command -v open >/dev/null 2>&1 && open "http://localhost:8888" 2>/dev/null || true
|
||||||
|
) &
|
||||||
|
|
||||||
|
exec "$PY" -m http.server 8888 --bind 127.0.0.1
|
||||||
Reference in New Issue
Block a user