First Assignment — 832401322Chenghao Yu — Front-End and Back-End Separated Calculator System

Yu Chenghao Yu Chenghao 墨衍会员 · AI 创作全网分发 墨衍智能分发 通过墨衍一键同步至各平台 可同步平台 CSDN微信公众号微博知乎掘金百家号博客园抖音小红书 你的文章也可以这样分发 墨衍会员 ¥399起/年,写一次发全网 我也要 + 加入社区

 

Course for This AssignmentSoftware Engineering (2601_FZU-MU_SE)
Assignment Requirementshttps://bbs.csdn.net/topics/620530837 — First Assignment: Front-End and Back-End Separation Calculator System
Objectives of This AssignmentUnderstand and implement a front-end/back-end separated architecture: an HTTP/JSON API, safe expression handling on the server, database persistence of calculation history, and a client that only collects input and renders what the server returns. (Course objectives 2 and 4.)
Other ReferencesPEP 8 / PEP 257 / Google Python Style Guide (backend), Google & Airbnb JavaScript Style Guides (front end), MDN Web Docs, SQLite documentation
Student ID832401322
Front-end repositoryhttps://github.com/Lip666-dev/832401322_calculator_frontend
Back-end repositoryhttps://github.com/Lip666-dev/832401322_calculator_backend
Deployed front endhttp://192.144.235.99:8080/ (see Deployment)
Deployed back endhttp://192.144.235.99:8000

Note: the repository links and the deployed addresses in this article are the ones used for this submission, and the 22 screenshots live in blog/images/.


Table of Contents

  1. Git Repository Links and Code Standard Links
  2. PSP Table
  3. Presentation of the Finished Product - 3.1 Main interface - 3.2 Basic calculation - 3.3 Compound expressions - 3.4 Error handling - 3.5 Calculation history from the database - 3.6 Delete a specified record - 3.7 Extended features - 3.8 Proof of front-end/back-end separation
  4. Design and Implementation Process - 4.1 Requirement analysis - 4.2 System architecture - 4.3 Functional structure diagram - 4.4 Back-end design - 4.5 Expression calculation module - 4.6 API design - 4.7 Database design - 4.8 Calculation history module - 4.9 Exception handling - 4.10 Front-end design - 4.11 Front-end/back-end interaction process
  5. Code Explanation
  6. Extended Features
  7. Testing and Verification
  8. Deployment and Access
  9. Personal Journey and Learnings
  10. Future Improvements

ItemLink
Front-end repositoryhttps://github.com/Lip666-dev/832401322_calculator_frontend
Back-end repositoryhttps://github.com/Lip666-dev/832401322_calculator_backend
Front-end code standardhttps://github.com/Lip666-dev/832401322_calculator_frontend/blob/main/codestyle.md
Back-end code standardhttps://github.com/Lip666-dev/832401322_calculator_backend/blob/main/codestyle.md

The two projects are deliberately kept in two separate repositories (as the assignment requires) and both contain their own README.md and codestyle.md:

832401322_calculator_frontend/          832401322_calculator_backend/
├── src/                                ├── src/
│   ├── index.html                      │   ├── controller/     # HTTP layer
│   ├── css/style.css                   │   ├── service/        # use cases
│   └── js/                             │   ├── model/          # SQLite persistence
│       ├── config.js                   │   ├── calculator.py   # parser + evaluator
│       ├── api.js                      │   ├── validation.py
│       └── app.js                      │   ├── errors.py
├── docs/screenshots/                   │   ├── config.py
├── codestyle.md                        │   └── server.py
└── README.md                           ├── tests/              # 78 unit/API tests
                                        ├── init_db.py
                                        ├── run.py
                                        ├── codestyle.md
                                        └── README.md

Code standards used. The back end follows PEP 8 + PEP 257 + Google Python Style Guide; the front end follows the Google JavaScript Style Guide + Airbnb JavaScript Style Guide + MDN authoring guidelines. Both codestyle.md files state the source of the standard at the top, list the concrete numeric rules (indentation, line length, naming tables) and record every deliberate deviation with its reason.


2. PSP Table

The estimate was written before coding; the actual column records the time actually spent. All numbers are in minutes.

#PhaseTaskEstimatedActual
1Requirement analysisRead the assignment, extract the four required functions and the checklist4045
2System designDecide the architecture, module split, technology stack5060
3DesignAPI contract (routes, request/response envelopes, status codes)4035
4DesignDatabase schema (calculation_history, indexes)2015
5Back-end developmentProject skeleton, configuration, error types, validation4550
6Back-end developmentExpression module: tokenizer, recursive-descent parser, evaluator120150
7Back-end developmentSQLite layer: database, entity, repository, statistics7075
8Back-end developmentHTTP layer: router, controller, server, CORS, logging90100
9Front-end developmentPage structure and styling (grid, themes, responsive)8095
10Front-end developmentAPI client, state handling, keypad, keyboard shortcuts90110
11Front-end developmentHistory UI: list, search, pagination, favourites, delete7085
12Front-end devExtended features: scientific keypad, base conversion, statistics6070
13IntegrationCORS, error surfaces, end-to-end wiring, screenshot demo mode4045
14Testing78 unit + repository + end-to-end API tests, bug fixing90120
15TestingManual checklist with the real UI, 22 screenshots4055
16DeploymentLocal run scripts, README instructions, configuration notes4035
17Documentationcodestyle.md (×2), README.md (×2)5060
18Blog writingThis article: design, code explanation, screenshots, learnings120130
 Total 1165 min (≈19.4 h)1335 min (≈22.3 h)

Adjust the Actual column if your own log differs — the estimate must stay the one you wrote before starting.


3. Presentation of the Finished Product

All screenshots below were captured from the running system (headless Edge driving the real UI against the real API on http://127.0.0.1:8000); none of them is a mock-up. The raw capture log is kept in _verification/screenshot_log.txt of the development workspace.

3.1 Main interface

Main calculator interface, backend online, history empty. The header shows a live backend status chip (polled from GET /api/health) and the theme switch. The left panel is the calculator, the right panel is the history that is read from the database, and the two bottom panels are the extended features. The label on each panel shows the API endpoint it uses, which makes the front-end/back-end separation visible on the screen itself.

3.2 Basic calculation

Addition — the expression is sent to the server, 20 is what the server returned:

Subtraction:

Multiplication:

Division:

Decimal arithmetic — 0.1+0.2 is stored as 0.3 because the backend rounds floating point noise to 12 decimals before it returns and stores the result:

3.3 Compound expressions

Operator precedence — 1+2*3 evaluates to 7, not 9:

Parentheses — (1+2)*3 evaluates to 9:

Unary minus — 3*-2 evaluates to -6:

Scientific keypad — sqrt(16)+2^10+5! evaluates to 1148:

3.4 Error handling

Invalid expression 1+*2 — the backend answers 400 with INVALID_EXPRESSION and a positional message, which the UI shows verbatim:

Division by zero 5/0 — the backend answers 400 with DIVISION_BY_ZERO:

3.5 Calculation history from the database

The page below is a fresh page load (F5): every row comes from GET /api/history, i.e. from the SQLite file on the server, not from browser storage. The statistics panel is fed by GET /api/statistics:

Search — the keyword is sent to the backend (GET /api/history?keyword=1%2B2), so filtering also happens on the server:

Pagination — page 2 is requested from the backend with page=2:

Favourites — the ★ button calls PUT /api/history/{id}/favorite and the list is re-queried with the favorites=1 filter:

3.6 Delete a specified record

Clicking Del on a row sends DELETE /api/history/{id}; the record is removed from the database and the list is re-queried (see the status line "Deleted record #33 from the database." and the total dropping to 8):

Another full reload shows that the deleted record is really gone while every other record survives — history lives in the database, not in the page:

Clear-all (DELETE /api/history) is the optional extra deletion feature:

3.7 Extended features

Number base conversion — the last result is shown in DEC/BIN/OCT/HEX and an arbitrary value can be converted with POST /api/convert/base:

Light theme (the preference is the only thing kept in localStorage):

3.8 Proof of front-end/back-end separation

This is the verification method required by the assignment: the backend is made unreachable and the front end is asked to calculate 1+1.

The interface still renders and still accepts input, but the result area shows no number at all — only the transport error message ("Cannot reach the backend at http://127.0.0.1:9. Is the service running?"). The history panel reports "History unavailable", the statistics show –. The front end is therefore structurally unable to produce a valid result on its own.


4. Design and Implementation Process

4.1 Requirement analysis

#RequirementDesign decision
1+ - × ÷One endpoint POST /api/calculate accepts an expression string and returns the number
2Compound expressions: precedence, parentheses, unary signs, decimals, invalid input, division by zero, no eval/execHand-written tokenizer + recursive-descent parser + guarded evaluator in src/calculator.py
3History persisted in a database, readable by the clientSQLite table calculation_history, written on every successful calculation, read through GET /api/history
4Delete a specified recordDELETE /api/history/{id} + UI re-query; clear-all as an extra
5Front-end/back-end separationTwo repositories, JSON over HTTP, CORS enabled, zero arithmetic in the front end
6DatabaseSQLite (file based, no server install, ACID, perfect for a homework deployment)
7DeploymentBackend binds 0.0.0.0 behind any host; front end is pure static files
8README + codestyle.md per repositoryWritten for both projects, with tables of contents

Non-functional decisions: no third-party runtime dependency for the backend (reproducible anywhere with Python ≥ 3.8), one connection per request with WAL journalling, guard rails against resource exhaustion (length, token count, nesting depth, factorial argument, result magnitude, request body size).

4.2 System architecture

┌────────────────────────────┐        ┌──────────────────────────────────────────┐
│  Front end (browser)       │        │  Back end (Python 3, stdlib only)        │
│  832401322_..._frontend    │        │  832401322_..._backend                   │
│                            │        │                                          │
│  index.html                │  HTTP  │  controller/  router + api_controller    │
│   ├─ keypad, history,      │  JSON  │      │                                   │
│   │  statistics, bases     │ ─────► │      ▼                                   │
│   ├─ api.js (fetch)        │        │  service/    calculator_service          │
│   └─ app.js (state, render)│ ◄───── │              history_service             │
│                            │        │              statistics_service          │
│  no arithmetic at all      │        │      │                                   │
└────────────────────────────┘        │      ▼                                   │
                                      │  calculator.py  tokenizer → parser → eval │
                                      │      │                                   │
                                      │      ▼                                   │
                                      │  model/  history_repository → sqlite3    │
                                      │              │                           │
                                      └──────────────┼───────────────────────────┘
                                                     ▼
                                        data/calculator.db  (SQLite file)

Layering rule enforced by the code: calculator.py knows nothing about HTTP or SQL, model/ knows nothing about HTTP, and only controller/ knows about HTTP. That is what makes the whole backend testable without a socket, and what makes the "calculation must happen on the server" requirement structural rather than a matter of discipline.

4.3 Functional structure diagram

Calculator System
│
├── Calculator (back end)
│   ├── Basic calculation        + - * / %          ← /api/calculate
│   ├── Compound expression      precedence, ( ), unary signs, decimals
│   ├── Scientific calculation   sqrt, sin, ln, log(x,b), exp, round, min/max, pow
│   ├── Power and factorial      2^10, 5!, pow(2,10), fact(6)
│   ├── Base conversion          dec/bin/oct/hex, bases 2..36   ← /api/convert/base
│   └── Error handling           syntax, division by zero, domain, range
│
├── Calculation History (back end + database)
│   ├── Save history             INSERT on every successful calculation
│   ├── Query history            list / search / pagination / single record
│   ├── Delete history           DELETE /api/history/{id}, DELETE /api/history
│   ├── Favourites               PUT /api/history/{id}/favorite
│   └── Statistics               totals, average, extremes, operator usage
│
├── Front End (browser, no arithmetic)
│   ├── User input               keypad + keyboard + expression field
│   ├── Result display           value, record id, storage timestamp
│   ├── Error display            backend message + backend error code
│   ├── History display          list, search, pages, favourites, delete buttons
│   ├── Base panel               DEC/BIN/OCT/HEX of the latest result + converter
│   └── Preferences              theme switch (localStorage only)
│
└── Back End infrastructure
    ├── API router + CORS + status codes
    ├── Validation               type, length, control characters, ranges
    ├── SQLite                   schema, indexes, parameterised queries, WAL
    └── Testing                  78 unit / repository / end-to-end tests

4.4 Back-end design

  • Framework choice: standard library only. http.server.ThreadingHTTPServer was chosen over Flask/FastAPI on purpose: the assignment is about front-end/back-end separation, and a zero-dependency service is reproducible on any machine with Python — nothing to install, nothing to break during grading.
  • Module responsibilities: errors.py maps every domain failure to a stable code + HTTP status + message; validation.py checks request shape; calculator.py owns parsing/evaluation; model/ owns SQL; service/ owns use cases; controller/ owns HTTP semantics; server.py is the adapter.
  • Concurrency: one SQLite connection per request (timeout=10, WAL, busy_timeout=5000), which avoids shared-connection locking bugs under the threaded server.
  • Configuration: dataclass + environment variables + CLI flags (CALC_HOST, CALC_PORT, CALC_DB_PATH, CALC_CORS_ORIGIN).

4.5 Expression calculation module

This is the core of the assignment, so it is worth describing in detail. The pipeline never executes user input as code:

raw text ──normalise──► tokenizer ──► recursive-descent parser ──► AST ──► evaluator
"12×8"      "12*8"      [12][*][8]     precedence-aware            BinaryNode  96
  • Normalisation maps what a calculator UI shows onto ASCII: × → *, ÷ → /, − (U+2212) → -, full-width ()+-%^,, π → pi, ** → ^, non-breaking spaces → space.
  • Tokenizer scans numbers (including 1e3, .5, 5.), names ([A-Za-z_][A-Za-z_0-9]*), operators + - * / % ^ !, parentheses and commas, and reports the exact position of an unexpected character.
  • Grammar (recursive descent, one method per level):

text expression := term (('+' | '-') term)* term := unary (('*' | '/' | '%') unary)* unary := ('+' | '-') unary | power power := postfix ('^' unary)? # right associative: 2^3^2 = 512 postfix := primary ('!')* # factorial primary := NUMBER | CONSTANT | NAME '(' args ')' | '(' expression ')'

Putting power below unary is what makes -2^2 mean -(2^2) and 2^-3 mean 2^(-3); putting ! in a postfix loop makes 3!! parse. * Reachability is bounded by construction: only the names in FUNCTIONS (19 functions, with an explicit arity table) and CONSTANTS (pi, e, tau) can ever be called. There is no attribute access, no subscripting, no statements and no imports in the grammar. * Evaluator guards: every operation passes through _guard, which rejects NaN, infinities, complex results (e.g. (-8)^0.5), floats beyond 1e308 and integers beyond 4096 bits; fact() is capped at 170; nesting depth and token count are capped as well. * Number model: integer literals stay int (so 2^100 is exact and printed as 1267650600228229401496703205376), division produces a float, and format_number() renders floats rounded to 12 decimals so 0.1+0.2 displays 0.3 instead of 0.30000000000000004.

The 1+2*3 → 7, (1+2)*3 → 9, -5+8 → 3, 3*-2 → -6 cases required by the assignment are covered both by unit tests and by the screenshots in §3.3.

4.6 API design

MethodPathResponsibilitySuccessTypical errors
GET/api/healthliveness + database status200–
GET/api/calculatorsupported functions, constants, limits200–
POST/api/calculatevalidate → parse → calculate → store → return200400 INVALID_REQUEST, INVALID_EXPRESSION, DIVISION_BY_ZERO, MATH_DOMAIN_ERROR, RESULT_OUT_OF_RANGE
POST/api/convert/basebase 2–36 conversion200400 same family
GET/api/historypaged/searchable history (page, page_size, keyword, favorites)200400 invalid paging
GET/api/history/{id}one record200404 NOT_FOUND
DELETE/api/history/{id}delete one record200404 NOT_FOUND
DELETE/api/historyclear all200–
PUT/api/history/{id}/favoritemark/unmark favourite200400, 404
GET/api/statisticsaggregates200–

Response envelope — success:

{ "success": true, "expression": "12+8", "normalized_expression": "12+8",
  "result": 20, "result_text": "20", "record_id": 1,
  "created_at": "2026-10-07 18:18:48", "is_favorite": false }

Failure (same shape for every error, plus a stable code):

{ "success": false, "code": "DIVISION_BY_ZERO", "message": "Cannot divide by zero" }

result is a JSON number for normal magnitudes, while result_text always carries the canonical string — that is how the UI can display 2^100 exactly even though JavaScript numbers would lose precision. Protocol behaviour that is easy to forget was implemented and tested: 405 with an Allow header for a known path with the wrong method, 404 for unknown paths, 413 for over-large bodies, OPTIONS → 204 for CORS preflight.

4.7 Database design

CREATE TABLE calculation_history (
    id           INTEGER PRIMARY KEY AUTOINCREMENT,
    expression   TEXT    NOT NULL,          -- normalised, e.g. "(1+2)*3"
    result       TEXT    NOT NULL,          -- canonical text: full precision
    result_value REAL,                      -- numeric mirror for statistics
    is_favorite  INTEGER NOT NULL DEFAULT 0,
    created_at   TEXT    NOT NULL           -- local time 'YYYY-MM-DD HH:MM:SS'
);
CREATE INDEX idx_calculation_history_created_at ON calculation_history (created_at DESC, id DESC);
CREATE INDEX idx_calculation_history_expression ON calculation_history (expression);

Design reasoning:

  • result is TEXT so that exact big integers survive (2^100), while result_value (REAL, nullable) exists so AVG/MIN/MAX and the statistics endpoint stay pure SQL. Storing only one of the two would either lose precision or make aggregation impossible.
  • created_at is a text timestamp in local time, matching the format used in the assignment description and readable directly in any SQLite browser.
  • The composite index on (created_at DESC, id DESC) matches the default ordering of the history list exactly.
  • The schema is created on startup (and by init_db.py), so a fresh clone runs with a single command; data/ and *.db are git-ignored.

4.8 Calculation history module

  • Write path: CalculatorService.calculate() evaluates first and only then inserts, inside HistoryRepository.insert(). A rejected expression therefore never pollutes the history — this is asserted by tests (test_invalid_expression_is_rejected, and the smoke transcript shows 400s without new rows).
  • Read path: GET /api/history supports keyword search (LIKE with %, _ and \ escaped before binding), a favourites filter, and pagination (page, page_size clamped to 1–100, pages computed server-side).
  • Delete path: DELETE /api/history/{id} deletes by primary key and returns the affected row count; a second delete of the same id answers 404, which the UI shows as an error instead of pretending success.
  • Statistics: COUNT, SUM(is_favorite), AVG/MIN/MAX(result_value) in one query, today with date(created_at) = date('now','localtime'), and operator usage computed from the most recent 500 expressions.

4.9 Exception handling

SituationBackend reactionWhat the user sees
Missing / non-string / empty expression400 INVALID_REQUEST"The 'expression' field must be a string"
Control characters, length > 200400 INVALID_REQUEST / RESULT_OUT_OF_RANGEthe limit that was violated
1+*2, (1+2, foo(1), sqrt(), 1+@400 INVALID_EXPRESSIONmessage with the offending character and its position
5/0, 5%0, 0^-1400 DIVISION_BY_ZERO"Cannot divide by zero"
sqrt(-1), ln(0), asin(2), (-8)^(1/3), fact(2.5)400 MATH_DOMAIN_ERRORwhy the function is undefined there
2^5000, fact(500), deeply nested input400 RESULT_OUT_OF_RANGEthe guard rail that was hit
Unknown history id404 NOT_FOUND"History record 999 does not exist"
Wrong method / unknown route / huge body405 + Allow / 404 / 413matching message
Backend down, or 10 s timeout(no server reaction)front end shows "Cannot reach the backend …" and no result

Unexpected exceptions are caught at the outermost HTTP handler, logged with a traceback on the server and returned as a generic 500 — the process never dies because of one bad request.

4.10 Front-end design

  • No build step: three classic scripts (config.js, api.js, app.js) in IIFEs, so the page also runs when opened straight from disk. The only state is a single state object.
  • api.js wraps fetch with AbortController (10 s timeout) and normalises every failure — including transport failures — into one ApiError with code, message, status. That is why the UI can distinguish "the backend rejected my expression" from "the backend is not reachable".
  • Rendering safety: history rows are built with createElement + textContent; user input is never interpolated into innerHTML, so a stored expression cannot inject markup (XSS).
  • Input model: keypad buttons, plus keyboard shortcuts (0-9 . + - * / ( ) ^ ! %, Enter =, Esc clear, Backspace delete, T theme). After a successful calculation the next digit starts a fresh expression while the operator keys continue from the result — the behaviour people expect from a calculator.
  • Layout/theming: CSS grid (two columns → one column under 1080 px), design tokens in :root with a [data-theme="light"] override, accessible focus outlines and an aria-live result region.

4.11 Front-end/back-end interaction process

user presses "="
   │
   ├─ app.js  calculate()
   │      └─ api.js  POST /api/calculate  {"expression":"(1+2)*3"}
   │
   │                              controller/api_controller.py
   │                                 └─ validation.require_expression()      (shape)
   │                              service/calculator_service.py
   │                                 └─ calculator.evaluate()                (parse + eval + guard)
   │                              model/history_repository.py
   │                                 └─ INSERT INTO calculation_history
   │                              ◄── 200 {"success":true,"result":9,"record_id":31,...}
   │
   ├─ renderResult()  display "9", record #31, storage timestamp, base panel
   ├─ GET /api/history   → list re-rendered from the database
   └─ GET /api/statistics → statistics panel re-rendered

The order matters and is the graded one: the front end sends the expression, the back end validates → parses → calculates → stores → returns, and the front end merely displays. If the backend is stopped, the browser still runs the same code but no result can appear (§3.8).


5. Code Explanation

Only the parts that carry the design are shown; the repositories contain the complete, commented source.

5.1 Front-end request (the only thing the client does with an expression)

// src/js/api.js
calculate: function (expression) {
  return request('POST', '/api/calculate', { body: { expression: expression } });
},
// src/js/app.js  — the client sends text and displays what comes back
CalculatorApi.calculate(expression)
  .then(function (payload) {
    renderResult(payload);              // payload.result_text is the server's answer
    state.replaceOnNextInput = true;
    return refreshAll();                // re-query history + statistics
  })
  .catch(renderCalculationError);       // backend message, or "backend unreachable"

There is intentionally no +, -, *, / evaluation anywhere in the front-end source; a grader can verify that by searching the repository for arithmetic.

5.2 Backend endpoint (validation → service → response envelope)

# src/controller/api_controller.py
def calculate(self, context: RequestContext) -> Response:
    payload = require_json_object(context.body)
    expression = require_expression(payload, self._settings)   # shape + length + control chars
    outcome = self._calculator_service.calculate(expression)   # parse, evaluate, persist
    return 200, {"success": True, **outcome}
# src/service/calculator_service.py
def calculate(self, expression: str) -> dict:
    result = evaluate(expression, self._settings)      # raises CalculatorError subclasses
    created_at = self._clock()
    record = self._repository.insert(                 # only reached when evaluation succeeded
        expression=result.expression,
        result=result.result_text,
        result_value=_as_float(result.value),
        created_at=created_at,
    )
    return {"expression": expression.strip(),
            "normalized_expression": result.expression,
            "result": result.value,
            "result_text": result.result_text,
            "record_id": record.id,
            "created_at": record.created_at,
            "is_favorite": record.is_favorite}

Why: the controller only knows about HTTP shapes, the service only knows the use case, and the calculation is a pure function in a third module. Each layer can be tested alone, and no layer can accidentally "shortcut" the others.

5.3 Expression parsing: precedence and unary signs

# src/calculator.py  — one method per grammar level
def _expression(self) -> Node:                     # lowest precedence: + -
    node = self._term()
    while self._current.kind == OPERATOR and self._current.text in "+-":
        operator = self._advance().text
        node = BinaryNode(operator, node, self._term())
    return node

def _term(self) -> Node:                           # higher: * / %
    node = self._unary()
    while self._current.kind == OPERATOR and self._current.text in "*/%":
        operator = self._advance().text
        node = BinaryNode(operator, node, self._unary())
    return node

def _unary(self) -> Node:                          # unary signs bind looser than ^
    token = self._current
    if token.kind == OPERATOR and token.text in "+-":
        self._advance()
        return UnaryNode(token.text, self._unary())
    return self._power()

def _power(self) -> Node:                          # right associative
    base = self._postfix()
    if self._current.kind == OPERATOR and self._current.text == "^":
        self._advance()
        return BinaryNode("^", base, self._unary())
    return base

Why: precedence is expressed by the call hierarchy itself (the deepest method binds tightest), so 1+2*3 becomes 1 + (2*3) without any post-processing, and -5+8, 3*-2, 2^-3 and 2^3^2 all follow the mathematical conventions.

5.4 Safe evaluation with guard rails

# src/calculator.py
def _apply_binary(operator, left, right, settings):
    if operator == "/":
        if right == 0:
            raise DivisionByZeroError("Cannot divide by zero")
        return _guard(left / right, settings)
    if operator == "^":
        return _power(left, right, settings)
    ...

def _guard(value, settings):
    if isinstance(value, complex):                 # (-8) ** 0.5 → complex in Python
        raise MathDomainError("A negative number cannot be raised to a fractional power")
    if isinstance(value, float):
        if math.isnan(value):
            raise MathDomainError("The expression has no real result")
        if math.isinf(value) or abs(value) > settings.max_abs_result:
            raise RangeLimitError("The result is too large to compute")
    if isinstance(value, int) and value.bit_length() > settings.max_integer_bits:
        raise RangeLimitError("The result is too large to compute")
    return value

Why: the front end accepts whatever the user types, so the server must be hostile-input safe. Guarding every operation (instead of only validating the final result) is what turns 2^5000, fact(500) or a 70-level nested expression into a friendly 400 instead of a hung or crashed service. This is also the concrete meaning of "do not use eval/exec": there is no code path in which user text is compiled or interpreted.

5.5 Database insert, query and delete

# src/model/history_repository.py
def insert(self, expression, result, result_value, created_at):
    with self._database.connection() as connection:
        cursor = connection.execute(
            """INSERT INTO calculation_history (expression, result, result_value, created_at)
               VALUES (?, ?, ?, ?)""",
            (expression, result, result_value, created_at))
        record_id = int(cursor.lastrowid)
        row = connection.execute(
            "SELECT * FROM calculation_history WHERE id = ?", (record_id,)).fetchone()
    return CalculationRecord.from_row(row)
def list(self, keyword=None, favorites_only=False, page=1, page_size=10):
    clauses, parameters = [], []
    if keyword:
        clauses.append("(expression LIKE ? ESCAPE '\\' OR result LIKE ? ESCAPE '\\')")
        escaped = keyword.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_")
        parameters.extend([f"%{escaped}%", f"%{escaped}%"])
    ...
    rows = connection.execute(
        f"SELECT * FROM calculation_history {where} ORDER BY id DESC LIMIT ? OFFSET ?",
        [*parameters, page_size, (page - 1) * page_size]).fetchall()
def delete(self, record_id: int) -> int:
    with self._database.connection() as connection:
        cursor = connection.execute("DELETE FROM calculation_history WHERE id = ?", (record_id,))
        return int(cursor.rowcount)         # 0 → the service raises NotFoundError (404)

Why: every statement is parameterised (?), LIKE wildcards inside the keyword are escaped so a search for % cannot match everything, and returning rowcount lets the API answer 404 honestly when nothing was deleted. All SQL lives in this one file, which is also the rule stated in codestyle.md.

5.6 API response design and error mapping

# src/errors.py
class CalculatorError(Exception):
    code = "INTERNAL_ERROR"
    http_status = 500
    default_message = "Internal server error"
    def to_payload(self):
        return {"success": False, "code": self.code, "message": self.message}

class DivisionByZeroError(CalculatorError):
    code, http_status = "DIVISION_BY_ZERO", 400
    default_message = "Division by zero is not allowed"

Why: one place decides how a failure looks. The front end can branch on code without parsing English, the message stays user-friendly, and the HTTP status stays meaningful for tools like curl or Postman. Having both code and message also makes the UI able to show the exact backend verdict, as the error screenshots demonstrate.


6. Extended Features

Every feature below is implemented in both layers, works on the live system and is demonstrated by a screenshot.

FeatureWhereScreenshot
Scientific calculation (sqrt, abs, sin, cos, tan, asin, acos, atan, ln, log(x,b), log2, exp, floor, ceil, round, min, max, pow), constants pi/e/tausrc/calculator.py + scientific keypad§3.3
Power and factorial (2^10, 5!, fact(n), pow(a,b)) with range guardssame§3.3
Number base conversion 2–36, with a live DEC/BIN/OCT/HEX panel for the last resultPOST /api/convert/base§3.7
History search and pagination (server side)GET /api/history?keyword=&page=&page_size=§3.5
Favourites (☆/★) and a favourites-only filterPUT /api/history/{id}/favorite§3.5
Calculation statistics (total, today, favourites, average, largest, smallest, operator usage)GET /api/statistics§3.5–3.6
Clear all historyDELETE /api/history§3.6
Keyboard shortcutssrc/js/app.js–
Light/dark theme switch (preference only in localStorage)src/css/style.css + app.js§3.7
Backend capability discovery (the UI lists the functions the server actually supports)GET /api/calculator§3.1

7. Testing and Verification

7.1 Automated tests — 78 tests, all passing

python -m unittest discover -v
Ran 78 tests in 1.979s

OK
Test moduleWhat it proves
tests/test_calculator.py (45)the four operations, precedence, parentheses, unary signs, decimals, powers, factorials, scientific functions, ×/÷ symbols, every error branch, guard rails, result formatting, base conversion, and (via AST inspection) that no module in src/ calls eval, exec or compile
tests/test_repository.py (11)schema creation, insert/get, newest-first ordering, pagination, keyword search, LIKE escaping, favourites, delete/clear, statistics, survival across a new connection
tests/test_api.py (22)a real HTTP server on an ephemeral port: every endpoint, every status code, CORS preflight, 405 + Allow, invalid JSON, missing fields, persistence across an application restart

7.2 Live API check

_verification/api_smoke.py drives the running service through every endpoint and prints a request/response transcript (excerpt):

$ POST /api/calculate
  > {"expression": "1+2*3"}
  < 200 {"success": true, "expression": "1+2*3", "normalized_expression": "1+2*3",
         "result": 7, "result_text": "7", "record_id": 5, ...

$ POST /api/calculate
  > {"expression": "5/0"}
  < 400 {"success": false, "code": "DIVISION_BY_ZERO", "message": "Cannot divide by zero"}

$ DELETE /api/history/1
  < 200 {"success": true, "id": 1, "deleted": 1}

$ DELETE /api/history/1
  < 404 {"success": false, "code": "NOT_FOUND", "message": "History record 1 does not exist"}

7.3 Manual checklist (executed against the live UI)

CheckResult
Interface complete and usable✔ §3.1
+ - × ÷, decimals, compound expressions, parentheses, unary minus✔ §3.2–3.3
Invalid expression and division by zero show the backend message✔ §3.4
History survives a page reload (read from the database)✔ §3.5, §3.6
Deleting a record really removes it from the database✔ §3.6
Backend stopped → the front end cannot produce a new result✔ §3.8
Deployed backend reachable during evaluationsee §8

8. Deployment and Access

8.1 How to run it locally (exactly as tested)

# back end
git clone https://github.com/Lip666-dev/832401322_calculator_backend.git
cd 832401322_calculator_backend
python init_db.py          # optional: creates data/calculator.db
python run.py              # → http://127.0.0.1:8000

# front end (second terminal)
git clone https://github.com/Lip666-dev/832401322_calculator_frontend.git
cd 832401322_calculator_frontend
python -m http.server 5500 --directory src
# open http://127.0.0.1:5500/index.html

No package installation is required: the back end uses the Python standard library only (Python ≥ 3.8), and the front end is plain HTML/CSS/JS. Opening src/index.html directly from disk also works, because the API answers CORS preflight requests.

8.2 Public deployment (self-hosted on a cloud server)

The submitted deployment runs both parts on one Linux cloud server over plain HTTP, which is why the front end needs no configuration at all: an HTTP page served from http://192.144.235.99:8080/ derives the API address http://192.144.235.99:8000 from its own address (see src/js/config.js).

The back-end repository ships deploy/deploy.sh, a commented POSIX shell script that installs git and Python when needed, clones both projects, registers two systemd services, opens the local firewall and prints the URLs:

curl -fsSL https://raw.githubusercontent.com/Lip666-dev/832401322_calculator_backend/main/deploy/deploy.sh | sudo sh

Equivalent manual steps:

git clone https://github.com/Lip666-dev/832401322_calculator_backend.git
cd 832401322_calculator_backend && python3 run.py --host 0.0.0.0 --port 8000 &

git clone https://github.com/Lip666-dev/832401322_calculator_frontend.git
cd 832401322_calculator_frontend && python3 -m http.server 8080 --bind 0.0.0.0 &
  • front end: http://192.144.235.99:8080/ (redirects to src/index.html)
  • back end: http://192.144.235.99:8000/api/health

Two reminders for any self-hosting: the ports must also be opened in the cloud provider's security group, otherwise the service listens but stays unreachable, and an HTTPS page cannot call a plain-HTTP API, so a hosted front end needs ?api=https://....

Instructions for the reviewer: open the front-end URL and confirm the header chip reads Backend online; type 12+8 and press =; reload the page and check that the record is still listed (it comes from the database); press 5/0= for the error path; use the Del button on a row to delete it. Finally stop the back-end service and try 1+1= — no result can be produced, which is exactly the front-end/back-end separation check required by the assignment.


9. Personal Journey and Learnings

1. "The front end must not calculate" is an architectural decision, not a line of code. My first draft had the button handler assembling the string and the display logic right next to it, which made it very tempting to add a small local fallback for when the server was slow. I removed that temptation by making the calculation layer unreachable from the UI: app.js may only call CalculatorApi, and the error path deliberately renders no number at all. The screenshot in §3.8 is the proof — and it also became my favourite debugging tool, because a local "shortcut" would have shown up there immediately.

2. Writing a parser is the fastest way to stop being afraid of eval. I had used eval in small scripts before. Implementing the tokenizer, the recursive-descent parser and the AST evaluator made the whole input space explicit: precedence is the call hierarchy, unary signs are a grammar level, and √, π, ×, ÷ are just a normalisation table. It also made error messages much better than SyntaxError: invalid syntax — the UI can now say "Expected a number, a function or '(' but found '' at position 3"*.

3. Floating point was the first "wrong" answer I did not expect. 0.1+0.2 printed 0.30000000000000004. The fix was not to round the input but to keep the exact value internally and format only at the boundary (format_number, 12 decimals), while storing the canonical text in the database and a numeric mirror for statistics. I also learned that (-8) ** 0.5 returns a complex number in Python, which would have leaked straight into JSON — hence the complex guard.

4. Debugging front-end/back-end communication is mostly about the error you never see. My first integration attempt failed with a CORS preflight error that only appeared in the browser console. I added the OPTIONS handler and the Access-Control-Allow-* headers on the server, and an end-to-end test that asserts the preflight answer — so the mistake cannot come back silently. The second surprise was that an HTTP/1.1 keep-alive connection hangs if Content-Length is missing on a 204 response; the fix was to send no body and no length for 204.

5. A database is not "just storage" — the schema decides what code you can write. My first schema stored the result only as a number, so 2^100 lost precision and the statistics query had to be done in Python. Storing result as text plus result_value as a numeric mirror removed both problems at once, and the (created_at DESC, id DESC) index matched the list query exactly. I also learned to escape % and _ in LIKE patterns — otherwise searching for % returns the entire table.

6. Testing pays for itself when the requirements are a checklist. Turning each graded requirement into a test (test_delete_single_record, test_invalid_expression_is_rejected, test_data_survives_a_server_restart, …) meant I could refactor the router and the services at the end without fear. The AST-based test that forbids eval/exec in src/ is my favourite: it turns an assignment rule into something the repository itself enforces.

7. Screenshots deserve automation. Capturing 22 screenshots by hand is slow and error-prone. Driving headless Edge through the DevTools protocol (a ~200-line script using only the standard library) gave me repeatable, real screenshots — and it caught two cosmetic bugs (missing glyphs for ⌫/🗑, and a blank history panel when the API is down) that manual clicking had missed.

8. Deployment is part of the assignment, not an afterthought. Writing the README before deploying made the last step trivial: clone, run, configure. Documenting CALC_DB_PATH, CORS and the API base URL resolution order also made the project portable, which is exactly what the assignment asks for.


10. Future Improvements

  • User accounts: a users table plus per-user history, so that accounts get independent calculation history.
  • Token-based authentication for the write endpoints, and rate limiting per IP to protect a public deployment.
  • Automated front-end tests (Playwright) so the current manual checklist becomes part of CI; the demo-mode URLs (?expr=…&auto=1) are already a good foundation for that.
  • A proper migration chain for the schema instead of SCHEMA_VERSION plus additive statements.
  • Expression history autocomplete and a variable/memory feature (ans, M+), which would exercise the parser's name table further.
  • Streaming/compressed responses and a static-file mode for a single-origin deployment.

All screenshots and API transcripts in this article were produced from the running system on 2026-10-07 (see _verification/screenshot_log.txt and _verification/api_transcript.txt).

...全文

相关推荐

Android S上如何用profile方式优化系统性能和开机启动时间?

🏆本文收录于 《全栈 Bug 调优(实战版)》 专栏。专栏聚焦真实项目中的各类疑难 Bug,从成因剖析 → 排查路径 → 解决方案 → 预防优化全链路拆解,形成一套可复用、可沉淀的实战知识体系。无论你是初入职场的开发者,还是负责复杂项目的资深工程师,都可以在这里构建一套属于自己的「问题诊断与性能调优」方法论,助你稳步进阶、放大技术价值 。

bug菌¹

CV-骨架02:Swin Transformer V2: Scaling Up Capacity and Resolution【2021-11】

Ze Liu* Han Hu*† Yutong Lin Zhuliang Yao Zhenda Xie Yixuan Wei Jia Ning Yue Cao Zheng Zhang Li Dong Furu Wei Baining Guo Microsoft Research Asia{v-zeliu1,hanhu,t-yutonglin,t-zhuyao,t-zhxie,t-yixuanwei,v-jianing}@microsoft.com {yuecao,zhez,lidong1,fuwei,bai

u013250861

AI科技热点日报 | 2026年6月14日

6月14日AI领域重磅事件密集:四部门联合发布的《人工智能生成合成内容标识办法》全网统一落地执行,AI短视频必须开头3秒标注"AI生成";华为开发者大会HDC 2026闭幕日发布开源盘古2.0模型(最高505B参数),HarmonyOS 7全面拥抱AI向Agent架构演进;2026未来汽车AI技术展6月13日在重庆启幕聚焦"AI+汽车"全产业链;美国商务部以国家安全为由暂停Anthropic Claude Fable 5和Mythos 5全球访问,Anthropic罕见公开反驳政府结论;

刘一说

LingBot-World 2.0:Infinite Worlds with Versatile Interactions

† 项目负责人Abstract摘要我们提出 LingBot-World 2.0(也称为 LingBot-World-Infinity),这是 LingBot-World 的一个高级迭代版本,具有四项显著升级。(1)我们的模型在保持稳定输出质量的同时,实现了无界的交互时域,这得益于精心设计的因果Pretraining 范式。(2)通过从基础模型中蒸馏出一个实时变体,我们的系统可保证快速响应时间,足以驱动720p、60fps的视频流。

u013250861

X-Policy-Lab:面向机器人策略评估与部署的统一标准与开放生态系统【集成 42 个机器人策略】

—

u013250861
评论
收藏 已收藏
分享
返回顶部