Compare commits

...

45 Commits

Author SHA1 Message Date
Austen Adler
c9b5173abd Too many changes to enumerate 2023-10-01 14:57:53 -04:00
Austen Adler
f3cee84c51 Split drawing into functions 2023-09-18 22:48:07 -04:00
Austen Adler
335079ac14 Add enabled field for buttons 2023-09-18 22:21:50 -04:00
Austen Adler
c713e0dbd1 Cleanup 2023-09-18 22:17:30 -04:00
Austen Adler
191b2da96c Remove label 2023-09-18 22:12:27 -04:00
Austen Adler
033d2d973a Cleanup input 2023-09-18 22:11:16 -04:00
Austen Adler
9a12104044 Update gui 2023-09-18 21:29:24 -04:00
Austen Adler
cd1eb22a7d Add ctrl-j as enter 2023-09-17 20:33:39 -04:00
Austen Adler
b8c2f24f6e Add rust_decimal support 2023-04-19 23:27:27 -04:00
Austen Adler
8346412f7a Print stack in gui 2023-04-14 02:21:05 -04:00
Austen Adler
d935cf6101 Update gui to properly send keys to calculator 2023-04-14 02:16:52 -04:00
Austen Adler
78498fbfcd Start working on gui 2023-04-14 01:34:11 -04:00
Austen Adler
0e72baa72c Add lib entry 2023-03-19 12:20:27 -04:00
Austen Adler
0b56093e83 Try pulling the api key from a secret
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
2022-12-26 00:54:00 -05:00
Austen Adler
f2a86fcc4b Only build artifacts on tag
Some checks failed
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2022-12-26 00:49:08 -05:00
Austen Adler
4c65c38e36 Restore README.adoc on earthignore
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-26 00:21:08 -05:00
Austen Adler
b349a1f7f0 Fix packaging
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is failing
2022-12-25 23:59:30 -05:00
Austen Adler
077200cb47 Add .drone.yml
Some checks failed
continuous-integration/drone/push Build is failing
2022-12-25 19:12:16 -05:00
Austen Adler
01941100c9 Update build.earth 2022-12-25 19:05:59 -05:00
Austen Adler
4379cb66a9 Commit old changes 2022-12-25 18:56:01 -05:00
Austen Adler
6bf83540fb Rustfmt 2021-10-12 08:45:44 -04:00
Austen Adler
dc30a93d61 Add unstable rustfmt 2021-10-12 08:45:33 -04:00
Austen Adler
6b24456f87 Use lazy_static for mapping operations 2021-10-12 08:45:04 -04:00
Austen Adler
8a03445936 Move help text to a const 2021-10-12 08:44:30 -04:00
Austen Adler
2f2976bfd7 Split all datatypes 2021-10-12 08:20:24 -04:00
Austen Adler
b5e944e82c Fix more bugs 2021-06-04 22:54:38 -04:00
Austen Adler
456dc6dcd4 Working vector and matrix; fix clippy warnings 2021-06-04 10:38:52 -04:00
Austen Adler
f80011652b Make validate part of the trait 2021-06-03 22:28:51 -04:00
Austen Adler
677444b7ff Fix vector building 2021-06-03 22:26:28 -04:00
Austen Adler
d92a6fbdb0 Add deconstruct operator 2021-06-03 12:24:28 -04:00
Austen Adler
3b10aaf06f Start work on Matrix 2021-06-03 12:16:03 -04:00
Austen Adler
7ee77f4c4c Cleanup code 2021-06-03 09:14:26 -04:00
Austen Adler
c14f809614 Fix clippy warnings 2021-06-03 08:55:31 -04:00
Austen Adler
e3b5457fe6 Write vector multiplication 2021-06-03 08:50:06 -04:00
Austen Adler
0405d25998 Get vectors working 2021-06-02 16:52:51 -04:00
Austen Adler
53891274f1 Get operations working with the calculator interface 2021-06-02 15:01:07 -04:00
Austen Adler
e05b0726f1 Started work on vectors 2021-06-02 14:59:56 -04:00
Austen Adler
bfae3cafce Use references 2021-05-31 15:43:14 -04:00
Austen Adler
752c7513aa Cargo fmt 2021-05-31 15:37:59 -04:00
Austen Adler
d84f2f7076 Add ArithmeticOperation 2021-05-31 15:37:55 -04:00
Austen Adler
39e3c83abc Cleanup numeric entries 2021-05-30 23:10:11 -04:00
Austen Adler
dab0333b31 Continue work on separating complex types 2021-05-30 23:02:32 -04:00
Austen Adler
c47287b4e6 Continue work on separating complex types 2021-05-30 23:01:57 -04:00
Austen Adler
7f5e42b026 Cargo fmt 2021-05-30 16:34:46 -04:00
Austen Adler
445ae3f535 Start work on complex types 2021-05-30 16:31:13 -04:00
43 changed files with 9500 additions and 605 deletions

40
.drone.yml Normal file
View File

@ -0,0 +1,40 @@
---
kind: pipeline
name: rpn_rs
type: docker
steps:
- name: Run earthly
image: earthly/earthly
privileged: true
volumes:
- name: docker_sock
path: /var/run/docker.sock
- name: dist
path: /dist
commands:
- 'file /var/run/docker.sock || :'
- 'pwd'
- 'earthly +all'
- 'cp -v ./dist/* /dist/'
- name: Gitea Artifacts
image: plugins/gitea-release
when:
event:
- tag
settings:
api_key:
from_secret: GITEA_API_KEY
base_url: https://gitea.austen-wares.com
files: dist/*
volumes:
- name: dist
path: /dist
volumes:
- name: docker_sock
host:
path: /var/run/docker.sock
- name: dist
temp: {}

View File

@ -2,3 +2,4 @@
!/src
!/Cargo.*
!/.cargo
!/README.adoc

3440
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[package]
name = "rpn_rs"
version = "0.5.0"
version = "0.6.0"
description = "A TUI RPN calculator, similar to Orpie"
authors = ["Austen Adler <agadler@austenadler.com>"]
edition = "2018"
@ -11,8 +11,32 @@ repository = "https://"
keywords = ["tui", "cli", "rpn"]
categories = ["command-line-utilities"]
[workspace]
members = [
".",
"./rpn_rs_tui/",
"./rpn_rs_gui/",
]
[dependencies]
crossterm = "0.18"
tui = { version = "0.14", default-features = false, features = ['crossterm'] }
serde = {version = "1.0", features = ["derive"]}
confy = "0.4.0"
# confy = "0.4.0"
toml = "0.4.2"
lazy_static = "1.4.0"
rust_decimal = { version = "1.29.1", features=["maths"] }
rust_decimal_macros = "1.29.1"
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-appender = "0.2.2"
tracing = "0.1.37"
egui_extras = "0.22.0"
[dependencies.confy]
# TODO: Update this to v0.5.0 when it finally comes out -- for now, use latest git master
# version = "0.4.0"
git = "https://github.com/rust-cli/confy/"
# TOML will not serialize because of ordering issues
features = ["yaml_conf"]
default-features = false
[package.metadata.deb]
depends = ""

View File

@ -58,7 +58,7 @@ asciidoctor REAMDE.adoc
If you have not done this before, open the calculator and press `<ctrl+s>` to save the basic config.
Edit the config file (in Linux, edit `~/.config/rpn_rs/rpn_rs.toml` and add any constants or macros and press `<ctrl+l>` or reopen the calculator.
Edit the config file (in Linux, edit `~/.config/rpn_rs/rpn_rs.yaml` and add any constants or macros and press `<ctrl+l>` or reopen the calculator.
Sample Macros:
@ -133,3 +133,7 @@ Will I implement these features? I don't know. Lots of these could be done by se
* Bases: Not yet
* Different math operators like `!` or `sum`: If someone asks me to, I guess
* Conditionals: If someone asks me to, I guess
=== Credits
* LCD Solid Font licensed as public domain from: https://www.fontspace.com/lcd-solid-font-f11346

View File

@ -1,48 +1,77 @@
VERSION 0.6
FROM rust:latest
ENV CARGO_HOME=/deps
ARG APP_NAME=rpn_rs
WORKDIR /work
deps:
COPY ./Cargo.toml ./Cargo.lock .
# This is a binary program, expose a main.rs file
RUN mkdir src && touch src/main.rs
RUN cargo fetch
SAVE ARTIFACT /deps
COPY ./Cargo.toml ./Cargo.lock .
# This is a binary program, expose a main.rs file
RUN mkdir src && touch src/main.rs
RUN cargo fetch
SAVE ARTIFACT /deps
rust-builder:
RUN rustup target add x86_64-unknown-linux-musl
RUN rustup target add x86_64-pc-windows-gnu
RUN rustup target add i686-unknown-linux-musl
RUN rustup target add aarch64-unknown-linux-musl
RUN rustup target add arm-unknown-linux-musleabi
RUN rustup target add armv7-unknown-linux-musleabi
# Linux
RUN rustup target add x86_64-unknown-linux-musl
RUN rustup target add i686-unknown-linux-musl
RUN rustup target add i686-unknown-linux-musl
RUN rustup target add aarch64-unknown-linux-musl
RUN rustup target add arm-unknown-linux-musleabi
RUN rustup target add armv7-unknown-linux-musleabi
# Windows
RUN rustup target add x86_64-pc-windows-gnu
RUN rustup target add i686-pc-windows-gnu
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu gdb-mingw-w64 gcc-mingw-w64-x86-64
RUN apt-get update
RUN DEBIAN_FRONTEND=noninteractive apt-get install -y gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu gdb-mingw-w64 gcc-mingw-w64-x86-64 gcc-mingw-w64-i686
SAVE IMAGE docker-wg:5000/rust-builder
RUN cargo install cargo-deb
SAVE IMAGE docker-wg:5000/rust-builder
build:
FROM +rust-builder
COPY . .
COPY +deps/deps /deps
FROM +rust-builder
ARG TOOLCHAIN
COPY +deps/deps /deps
RUN cargo build --release --target "$TOOLCHAIN"
ARG EXT
ARG STRIP_CMD
RUN if [ "$STRIP_CMD" ]; then "$STRIP_CMD" "target/$TOOLCHAIN/release/$APP_NAME$EXT"; fi
# --force for windows since multiple hardlinks
RUN xz --force --keep "target/$TOOLCHAIN/release/$APP_NAME$EXT"
SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT AS LOCAL target/$TOOLCHAIN/release/$APP_NAME-$TOOLCHAIN$EXT
SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT.xz AS LOCAL target/release/$APP_NAME-$TOOLCHAIN$EXT.xz
ARG TOOLCHAIN
# Cache whatever dependency builds you can
COPY ./Cargo.toml ./Cargo.lock .
RUN \
mkdir src && touch src/lib.rs && \
cargo build --offline --release --target "$TOOLCHAIN" `#--jobs 1` && \
rm src/lib.rs && rmdir src && \
:
# Actually build the program
COPY . .
RUN cargo build --offline --release --target "$TOOLCHAIN" `#--jobs 1`
ARG EXT
ARG STRIP_CMD
ARG APP_NAME=rpn_rs
RUN if [ "$STRIP_CMD" ]; then "$STRIP_CMD" "target/$TOOLCHAIN/release/$APP_NAME$EXT"; fi
# --force for windows since multiple hardlinks
RUN xz --force --keep "target/$TOOLCHAIN/release/$APP_NAME$EXT"
# Also build a deb file
IF [ -z "${TOOLCHAIN##*-linux-*}" ]
RUN echo "Building deb for ${TOOLCHAIN}"
RUN cargo deb --target "${TOOLCHAIN}"
SAVE ARTIFACT target/$TOOLCHAIN/debian/$APP_NAME_*.deb AS LOCAL dist/
END
# SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT AS LOCAL dist/
SAVE ARTIFACT target/$TOOLCHAIN/release/$APP_NAME$EXT.xz AS LOCAL dist/$APP_NAME-$TOOLCHAIN$EXT.xz
all:
BUILD --build-arg TOOLCHAIN=x86_64-unknown-linux-musl --build-arg STRIP_CMD=x86_64-linux-gnu-strip +build
BUILD --build-arg TOOLCHAIN=i686-unknown-linux-musl --build-arg STRIP_CMD=x86_64-linux-gnu-strip +build
BUILD --build-arg TOOLCHAIN=aarch64-unknown-linux-musl --build-arg STRIP_CMD=aarch64-linux-gnu-strip +build
BUILD --build-arg TOOLCHAIN=arm-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
BUILD --build-arg TOOLCHAIN=armv7-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
BUILD --build-arg TOOLCHAIN=x86_64-pc-windows-gnu --build-arg STRIP_CMD= --build-arg EXT=.exe +build
BUILD --build-arg TOOLCHAIN=x86_64-unknown-linux-musl --build-arg STRIP_CMD=x86_64-linux-gnu-strip +build
BUILD --build-arg TOOLCHAIN=i686-unknown-linux-musl --build-arg STRIP_CMD=x86_64-linux-gnu-strip +build
BUILD --build-arg TOOLCHAIN=aarch64-unknown-linux-musl --build-arg STRIP_CMD=aarch64-linux-gnu-strip +build
BUILD --build-arg TOOLCHAIN=arm-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
BUILD --build-arg TOOLCHAIN=armv7-unknown-linux-musleabi --build-arg STRIP_CMD=arm-linux-gnueabi-strip +build
BUILD --build-arg TOOLCHAIN=x86_64-pc-windows-gnu --build-arg STRIP_CMD= --build-arg EXT=.exe +build
BUILD --build-arg TOOLCHAIN=i686-pc-windows-gnu --build-arg STRIP_CMD= --build-arg EXT=.exe +build

22
pipeline.yml Normal file
View File

@ -0,0 +1,22 @@
---
resources:
- name: source
type: git
source:
uri: https://gitea.austen-wares.com/stonewareslord/rpn_rs
branch: develop
jobs:
- name: build
serial: true
plan:
- get: source
- task: build
config:
platform: linux
image_resource:
type: docker-image
source: {repository: }
inputs:
- name: source
outputs:
- name: out

View File

@ -0,0 +1,6 @@
# clipboard api is still unstable, so web-sys requires the below flag to be passed for copy (ctrl + c) to work
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
# check status at https://developer.mozilla.org/en-US/docs/Web/API/Clipboard#browser_compatibility
# we don't use `[build]` because of rust analyzer's build cache invalidation https://github.com/emilk/eframe_template/issues/93
[target.wasm32-unknown-unknown]
rustflags = ["--cfg=web_sys_unstable_apis"]

45
rpn_rs_gui/.github/workflows/pages.yml vendored Normal file
View File

@ -0,0 +1,45 @@
name: Github Pages
# By default, runs if you push to master. keeps your deployed app in sync with master branch.
on:
push:
branches:
- master
# to only run when you do a new github release, comment out above part and uncomment the below trigger.
# on:
# release:
# types:
# - published
permissions:
contents: write # for committing to gh-pages branch.
jobs:
build-github-pages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2 # repo checkout
- uses: actions-rs/toolchain@v1 # get rust toolchain for wasm
with:
profile: minimal
toolchain: stable
target: wasm32-unknown-unknown
override: true
- name: Rust Cache # cache the rust build artefacts
uses: Swatinem/rust-cache@v1
- name: Download and install Trunk binary
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
- name: Build # build
# "${GITHUB_REPOSITORY#*/}" evaluates into the name of the repository
# using --public-url something will allow trunk to modify all the href paths like from favicon.ico to repo_name/favicon.ico .
# this is necessary for github pages where the site is deployed to username.github.io/repo_name and all files must be requested
# relatively as eframe_template/favicon.ico. if we skip public-url option, the href paths will instead request username.github.io/favicon.ico which
# will obviously return error 404 not found.
run: ./trunk build --release --public-url "${GITHUB_REPOSITORY#*/}"
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: dist
# this option will not maintain any history of your previous pages deployment
# set to false if you want all page build to be committed to your gh-pages branch history
single-commit: true

105
rpn_rs_gui/.github/workflows/rust.yml vendored Normal file
View File

@ -0,0 +1,105 @@
on: [push, pull_request]
name: CI
env:
# This is required to enable the web_sys clipboard API which egui_web uses
# https://rustwasm.github.io/wasm-bindgen/api/web_sys/struct.Clipboard.html
# https://rustwasm.github.io/docs/wasm-bindgen/web-sys/unstable-apis.html
RUSTFLAGS: --cfg=web_sys_unstable_apis
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features
check_wasm:
name: Check wasm32
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
target: wasm32-unknown-unknown
override: true
- uses: actions-rs/cargo@v1
with:
command: check
args: --all-features --lib --target wasm32-unknown-unknown
test:
name: Test Suite
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
- uses: actions-rs/cargo@v1
with:
command: test
args: --lib
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt
- uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: clippy
- uses: actions-rs/cargo@v1
with:
command: clippy
args: -- -D warnings
trunk:
name: trunk
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: 1.65.0
target: wasm32-unknown-unknown
override: true
- name: Download and install Trunk binary
run: wget -qO- https://github.com/thedodd/trunk/releases/latest/download/trunk-x86_64-unknown-linux-gnu.tar.gz | tar -xzf-
- name: Build
run: ./trunk build

2
rpn_rs_gui/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/dist

2626
rpn_rs_gui/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

48
rpn_rs_gui/Cargo.toml Normal file
View File

@ -0,0 +1,48 @@
[package]
name = "rpn_rs_gui"
version = "0.1.0"
authors = ["Austen Adler <agadler@austenadler.com>"]
edition = "2021"
rust-version = "1.65"
[dependencies]
rpn_rs = {path=".."}
egui = "0.22.0"
eframe = { version = "0.22.0", default-features = false, features = ["accesskit", "default_fonts", "glow", "persistence"] }
# You only need serde if you want app persistence:
serde = { version = "1", features = ["derive"] }
tracing = "0.1.37"
egui_extras = "0.22.0"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.6"
tracing-wasm = "0.2"
wasm-bindgen = { version = "0.2.87" }
wasm-bindgen-futures = "0.4"
# TODO: This block was commented
# [profile.release]
# opt-level = 2 # fast and small wasm
# # Optimize all dependencies even in debug builds:
# [profile.dev.package."*"]
# opt-level = 2
# TODO: This block was commented
# [patch.crates-io]
# If you want to use the bleeding edge version of egui and eframe:
# egui = { git = "https://github.com/emilk/egui", branch = "master" }
# eframe = { git = "https://github.com/emilk/egui", branch = "master" }
# If you fork https://github.com/emilk/egui you can test with:
# egui = { path = "../egui/crates/egui" }
# eframe = { path = "../egui/crates/eframe" }

Binary file not shown.

75
rpn_rs_gui/README.md Normal file
View File

@ -0,0 +1,75 @@
# eframe template
[![dependency status](https://deps.rs/repo/github/emilk/eframe_template/status.svg)](https://deps.rs/repo/github/emilk/eframe_template)
[![Build Status](https://github.com/emilk/eframe_template/workflows/CI/badge.svg)](https://github.com/emilk/eframe_template/actions?workflow=CI)
This is a template repo for [eframe](https://github.com/emilk/egui/tree/master/crates/eframe), a framework for writing apps using [egui](https://github.com/emilk/egui/).
The goal is for this to be the simplest way to get started writing a GUI app in Rust.
You can compile your app natively or for the web, and share it using Github Pages.
## Getting started
Start by clicking "Use this template" at https://github.com/emilk/eframe_template/ or follow [these instructions](https://docs.github.com/en/free-pro-team@latest/github/creating-cloning-and-archiving-repositories/creating-a-repository-from-a-template).
Change the name of the crate: Chose a good name for your project, and change the name to it in:
* `Cargo.toml`
* Change the `package.name` from `eframe_template` to `your_crate`.
* Change the `package.authors`
* `main.rs`
* Change `eframe_template::TemplateApp` to `your_crate::TemplateApp`
* `index.html`
* Change the `<title>eframe template</title>` to `<title>your_crate</title>`. optional.
* `assets/sw.js`
* Change the `'./eframe_template.js'` to `./your_crate.js` (in `filesToCache` array)
* Change the `'./eframe_template_bg.wasm'` to `./your_crate_bg.wasm` (in `filesToCache` array)
### Learning about egui
`src/app.rs` contains a simple example app. This is just to give some inspiration - most of it can be removed if you like.
The official egui docs are at <https://docs.rs/egui>. If you prefer watching a video introduction, check out <https://www.youtube.com/watch?v=NtUkr_z7l84>. For inspiration, check out the [the egui web demo](https://emilk.github.io/egui/index.html) and follow the links in it to its source code.
### Testing locally
Make sure you are using the latest version of stable rust by running `rustup update`.
`cargo run --release`
On Linux you need to first run:
`sudo apt-get install libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev`
On Fedora Rawhide you need to run:
`dnf install clang clang-devel clang-tools-extra libxkbcommon-devel pkg-config openssl-devel libxcb-devel fontconfig-devel`
### Web Locally
You can compile your app to [WASM](https://en.wikipedia.org/wiki/WebAssembly) and publish it as a web page.
We use [Trunk](https://trunkrs.dev/) to build for web target.
1. Install Trunk with `cargo install --locked trunk`.
2. Run `trunk serve` to build and serve on `http://127.0.0.1:8080`. Trunk will rebuild automatically if you edit the project.
3. Open `http://127.0.0.1:8080/index.html#dev` in a browser. See the warning below.
> `assets/sw.js` script will try to cache our app, and loads the cached version when it cannot connect to server allowing your app to work offline (like PWA).
> appending `#dev` to `index.html` will skip this caching, allowing us to load the latest builds during development.
### Web Deploy
1. Just run `trunk build --release`.
2. It will generate a `dist` directory as a "static html" website
3. Upload the `dist` directory to any of the numerous free hosting websites including [GitHub Pages](https://docs.github.com/en/free-pro-team@latest/github/working-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site).
4. we already provide a workflow that auto-deploys our app to GitHub pages if you enable it.
> To enable Github Pages, you need to go to Repository -> Settings -> Pages -> Source -> set to `gh-pages` branch and `/` (root).
>
> If `gh-pages` is not available in `Source`, just create and push a branch called `gh-pages` and it should be available.
You can test the template app at <https://emilk.github.io/eframe_template/>.
## Updating egui
As of 2022, egui is in active development with frequent releases with breaking changes. [eframe_template](https://github.com/emilk/eframe_template/) will be updated in lock-step to always use the latest version of egui.
When updating `egui` and `eframe` it is recommended you do so one version at the time, and read about the changes in [the egui changelog](https://github.com/emilk/egui/blob/master/CHANGELOG.md) and [eframe changelog](https://github.com/emilk/egui/blob/master/crates/eframe/CHANGELOG.md).

2
rpn_rs_gui/Trunk.toml Normal file
View File

@ -0,0 +1,2 @@
[build]
filehash = false

BIN
rpn_rs_gui/assets/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 314 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@ -0,0 +1,28 @@
{
"name": "egui Template PWA",
"short_name": "egui-template-pwa",
"icons": [
{
"src": "./icon-256.png",
"sizes": "256x256",
"type": "image/png"
},
{
"src": "./maskable_icon_x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "./icon-1024.png",
"sizes": "1024x1024",
"type": "image/png"
}
],
"lang": "en-US",
"id": "/index.html",
"start_url": "./index.html",
"display": "standalone",
"background_color": "white",
"theme_color": "white"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

30
rpn_rs_gui/assets/sw.js Normal file
View File

@ -0,0 +1,30 @@
var cacheName = 'egui-template-pwa';
var filesToCache = [
'./',
'./index.html',
'./eframe_template.js',
'./eframe_template_bg.wasm',
];
// self.addEventListener('keydown', function (e) {
// console.log("Got keydown", e);
// e.preventDefault();
// })
/* Start the service worker and cache all of the app's content */
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open(cacheName).then(function (cache) {
return cache.addAll(filesToCache);
})
);
});
/* Serve cached content when offline */
self.addEventListener('fetch', function (e) {
e.respondWith(
caches.match(e.request).then(function (response) {
return response || fetch(e.request);
})
);
});

11
rpn_rs_gui/check.sh Executable file
View File

@ -0,0 +1,11 @@
#!/usr/bin/env bash
# This scripts runs various CI-like checks in a convenient way.
set -eux
cargo check --workspace --all-targets
cargo check --workspace --all-features --lib --target wasm32-unknown-unknown
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings -W clippy::all
cargo test --workspace --all-targets --all-features
cargo test --workspace --doc
trunk build

140
rpn_rs_gui/index.html Normal file
View File

@ -0,0 +1,140 @@
<!DOCTYPE html>
<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<!-- Disable zooming: -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<head>
<!-- change this to your project name -->
<title>RPN RS2</title>
<!-- config for our rust wasm binary. go to https://trunkrs.dev/assets/#rust for more customization -->
<link data-trunk rel="rust" data-wasm-opt="2" />
<!-- this is the base url relative to which other urls will be constructed. trunk will insert this from the public-url option -->
<base data-trunk-public-url />
<link data-trunk rel="icon" href="assets/favicon.ico">
<link data-trunk rel="copy-file" href="assets/sw.js" />
<link data-trunk rel="copy-file" href="assets/manifest.json" />
<link data-trunk rel="copy-file" href="assets/icon-1024.png" />
<link data-trunk rel="copy-file" href="assets/icon-256.png" />
<link data-trunk rel="copy-file" href="assets/icon_ios_touch_192.png" />
<link data-trunk rel="copy-file" href="assets/maskable_icon_x512.png" />
<link rel="manifest" href="manifest.json">
<link rel="apple-touch-icon" href="icon_ios_touch_192.png">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="white">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#404040">
<style>
html {
/* Remove touch delay: */
touch-action: manipulation;
}
body {
/* Light mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #909090;
}
@media (prefers-color-scheme: dark) {
body {
/* Dark mode background color for what is not covered by the egui canvas,
or where the egui canvas is translucent. */
background: #404040;
}
}
/* Allow canvas to fill entire web page: */
html,
body {
overflow: hidden;
margin: 0 !important;
padding: 0 !important;
height: 100%;
width: 100%;
}
/* Position canvas in center-top: */
canvas {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 0%;
left: 50%;
transform: translate(-50%, 0%);
}
.centered {
margin-right: auto;
margin-left: auto;
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #f0f0f0;
font-size: 24px;
font-family: Ubuntu-Light, Helvetica, sans-serif;
text-align: center;
}
/* ---------------------------------------------- */
/* Loading animation from https://loading.io/css/ */
.lds-dual-ring {
display: inline-block;
width: 24px;
height: 24px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 24px;
height: 24px;
margin: 0px;
border-radius: 50%;
border: 3px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<!-- The WASM code will resize the canvas dynamically -->
<!-- the id is hardcoded in main.rs . so, make sure both match. -->
<canvas id="the_canvas_id"></canvas>
<!--Register Service Worker. this will cache the wasm / js scripts for offline use (for PWA functionality). -->
<!-- Force refresh (Ctrl + F5) to load the latest files instead of cached files -->
<script>
// We disable caching during development so that we always view the latest version.
if ('serviceWorker' in navigator && window.location.hash !== "#dev") {
window.addEventListener('load', function () {
navigator.serviceWorker.register('sw.js');
});
}
</script>
</body>
</html>
<!-- Powered by egui: https://github.com/emilk/egui/ -->

539
rpn_rs_gui/src/app.rs Normal file
View File

@ -0,0 +1,539 @@
use std::collections::HashSet;
use egui::{
Align, Button, Color32, Direction, FontData, FontDefinitions, FontFamily, FontId, Frame, Grid,
Id, Key, Label, Layout, Margin, PointerButton, Pos2, Rect, RichText, Rounding, ScrollArea,
Sense, Stroke, Style, TouchId, TouchPhase, Vec2,
};
use egui_extras::{Column, Size, StripBuilder, TableBuilder};
use rpn_rs::calc::{
entries::CalculatorEntry,
errors::CalculatorError,
types::{CalculatorDisplayMode, CalculatorState},
Calculator,
};
use tracing::{error, info};
mod buttons;
use buttons::CalculatorButton;
use self::buttons::{BUTTON_LAYOUT, BUTTON_LAYOUT_SETTINGS};
const DEFAULT_FONT_SIZE: f32 = 45.0;
const STACK_FONT_SIZE: f32 = 25.0;
const DEFAULT_FONT: FontId = FontId::monospace(DEFAULT_FONT_SIZE);
// const BUTTON_SIZE_WIDTH: Size = Size::remainder();
// const BUTTON_SIZE_HEIGHT: Size = Size::remainder();
// const BUTTON_SIZE: Vec2 = Vec2 { x: 67.0, y: 60.0 };
const BUTTON_SPACING: Vec2 = Vec2 { x: 0.0, y: 0.0 };
// const BUTTON_PADDING: Vec2 = Vec2 { x: 24.0, y: 5.0 };
const BUTTON_PADDING: Vec2 = Vec2 { x: 0.0, y: 0.0 };
/// We derive Deserialize/Serialize so we can persist app state on shutdown.
#[derive(serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct TemplateApp {
calculator: CalculatorInner,
#[serde(skip)]
latest_error: Option<CalculatorError>,
#[serde(skip)]
touches_down: HashSet<ClickTapId>,
#[serde(skip)]
new_touches: Vec<Pos2>,
}
#[derive(serde::Deserialize, serde::Serialize, Default)]
#[serde(default)]
pub struct CalculatorInner {
calculator: Calculator,
#[serde(skip)]
error_state: ErrorState,
}
impl CalculatorInner {
fn calculator_input(&mut self, c: char) {
let action = if c == '$' {
self.calculator.backspace()
} else {
self.calculator.take_input(c)
};
if let Err(e) = action {
self.error_state.errored(e);
} else {
self.error_state.success();
}
}
}
/// Phase of a click or tap
#[derive(Debug, Clone, Copy)]
enum ClickPhase {
Start,
End,
Ignored,
}
/// ID of a click or tap
///
/// Required because taps have IDs, but clicks don't
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
enum ClickTapId {
Tap(u64),
Click(u8),
}
impl From<&TouchId> for ClickTapId {
fn from(value: &TouchId) -> Self {
Self::Tap(value.0)
}
}
impl From<&PointerButton> for ClickTapId {
fn from(value: &PointerButton) -> Self {
Self::Click(*value as u8)
}
}
impl From<&TouchPhase> for ClickPhase {
fn from(value: &TouchPhase) -> Self {
match value {
TouchPhase::Start => Self::Start,
TouchPhase::End => Self::End,
TouchPhase::Move | TouchPhase::Cancel => Self::Ignored,
}
}
}
impl Default for TemplateApp {
fn default() -> Self {
Self {
calculator: CalculatorInner::default(),
latest_error: None,
touches_down: HashSet::new(),
new_touches: Vec::new(),
}
}
}
impl TemplateApp {
/// Called once before the first frame.
pub fn new(cc: &eframe::CreationContext<'_>) -> Self {
// This is also where you can customize the look and feel of egui using
// `cc.egui_ctx.set_visuals` and `cc.egui_ctx.set_fonts`.
// Load previous app state (if any).
// Note that you must enable the `persistence` feature for this to work.
let ret;
if let Some(storage) = cc.storage {
ret = eframe::get_value(storage, eframe::APP_KEY).unwrap_or_default();
} else {
ret = Default::default()
}
TemplateApp::initialize_fonts(&cc.egui_ctx);
ret
}
fn draw_stack(&mut self, ui: &mut egui::Ui) {
TableBuilder::new(ui)
.stick_to_bottom(true)
// .column(Column::auto() )
.column(Column::remainder())
// .header(20.0, |mut header| {
// header.col(|ui| {
// ui.heading("Entry");
// });
// header.col(|ui| {
// ui.heading("Value");
// });
// })
.body(|mut body| {
for (_idx, entry) in self.calculator.calculator.stack.iter().enumerate().rev() {
body.row(30.0, |mut row| {
// row.col(|ui| {
// ui.add(Label::new(
// RichText::new(format!("{idx}"))
// .background_color(Color32::RED)
// .font(DEFAULT_FONT)
// .size(STACK_FONT_SIZE)
// .color(Color32::WHITE)
// ));
// });
row.col(|ui| {
ui.add(Label::new(
RichText::new(
entry.format_entry(&self.calculator.calculator.display_mode),
)
.background_color(Color32::DARK_GRAY)
.font(DEFAULT_FONT)
.size(STACK_FONT_SIZE)
.color(Color32::WHITE),
));
});
});
}
});
}
fn draw_error(&mut self, ui: &mut egui::Ui) {
if let Some(ref e) = self.latest_error {
ui.label(
RichText::new(e.to_string())
.font(DEFAULT_FONT)
.size(STACK_FONT_SIZE)
.background_color(Color32::RED)
.color(Color32::WHITE),
);
}
}
fn draw_input(&mut self, ui: &mut egui::Ui) {
ui.painter()
.rect_filled(ui.available_rect_before_wrap(), 0.0, Color32::LIGHT_GREEN);
ui.label(
RichText::new(self.calculator.calculator.get_l())
.color(Color32::BLACK)
.font(FontId::monospace(30.0)),
);
}
fn draw_buttons(&mut self, ui: &mut egui::Ui) {
let button_layout = match self.calculator.calculator.state {
CalculatorState::Normal => BUTTON_LAYOUT,
CalculatorState::WaitingForConstant => return,
CalculatorState::WaitingForMacro => return,
CalculatorState::WaitingForRegister(_) => return,
CalculatorState::WaitingForSetting => BUTTON_LAYOUT_SETTINGS,
};
StripBuilder::new(ui)
.sizes(Size::exact(60.0), button_layout.len())
// .sizes(Size::remainder(), button_layout.len())
.vertical(|mut strip| {
for row in button_layout.iter() {
strip.strip(|builder| {
builder
.sizes(Size::remainder(), row.len())
.horizontal(|mut strip| {
for button_definition in row.iter() {
strip.cell(|ui| {
let sense = Sense::click();
let label = RichText::new(button_definition.value)
.font(DEFAULT_FONT)
.color(Color32::WHITE)
.background_color(Color32::BLACK);
ui.painter().rect_filled(
ui.available_rect_before_wrap(),
0.0,
Color32::GOLD,
);
ui.style_mut().spacing.window_margin = Margin {
left: 0.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
};
let max_size = {
let ret = ui.available_rect_before_wrap();
Vec2 {
x: ret.width(),
y: ret.height(),
}
};
ui.vertical_centered(|ui| {
ui.add_enabled(
button_definition.enabled,
Button::new(label)
.stroke(Stroke::NONE)
.rounding(Rounding::none())
.fill(Color32::from_gray(0x12))
.min_size(max_size)
.sense(sense),
)
});
// let button = {
// let ret = ui.add_enabled(
// button_definition.enabled,
// Button::new(label)
// .stroke(Stroke::NONE)
// .rounding(Rounding::none())
// .fill(Color32::from_gray(0x12))
// .min_size(max_size)
// .sense(sense),
// );
// ret
// };
// Check if any new touches intersect with this button
let max_rect = ui.max_rect();
for touch in self.new_touches.iter() {
if max_rect.contains(*touch) {
self.calculator
.calculator_input(button_definition.value);
}
}
});
}
});
});
}
});
}
fn layout() -> Layout {
Layout::from_main_dir_and_cross_align(Direction::TopDown, Align::Center)
.with_cross_justify(true)
}
fn handle_input(&mut self, i: &egui::InputState) {
if i.events.is_empty() {
return;
}
for e in i.events.iter() {
match e {
egui::Event::Text(t) => {
self.calculator.calculator_input(t.chars().next().unwrap());
}
// egui::Event::Key {
// key: Key::ArrowLeft,
// pressed: true,
// ..
// } => {
// self.calculator.calculator_input('<');
// }
egui::Event::Key {
key: Key::ArrowRight,
pressed: true,
..
} => {
self.calculator.calculator_input('>');
}
egui::Event::Key {
key: Key::Enter,
pressed: true,
..
} => {
self.calculator.calculator_input(' ');
}
egui::Event::Touch {
device_id: _,
id,
phase,
pos,
force: _,
} => {
self.handle_touch_event(phase.into(), pos.clone(), id);
}
egui::Event::PointerButton {
pos,
button,
pressed,
modifiers: _,
} => self.handle_touch_event(
if *pressed {
ClickPhase::Start
} else {
ClickPhase::End
},
*pos,
button,
),
egui::Event::Copy
| egui::Event::Cut
| egui::Event::Paste(_)
| egui::Event::Key {
key: _,
pressed: _,
repeat: _,
modifiers: _,
}
| egui::Event::PointerMoved(_)
| egui::Event::PointerGone
| egui::Event::Scroll(_)
| egui::Event::Zoom(_)
| egui::Event::CompositionStart
| egui::Event::CompositionUpdate(_)
| egui::Event::CompositionEnd(_)
| egui::Event::AccessKitActionRequest(_)
| egui::Event::MouseWheel {
unit: _,
delta: _,
modifiers: _,
}
| egui::Event::WindowFocused(_) => continue,
}
}
}
fn handle_touch_event(&mut self, phase: ClickPhase, pos: Pos2, id: impl Into<ClickTapId>) {
let id = id.into();
match phase {
ClickPhase::Start => {
// TODO: This can be way better
// If this is a brand new touch
if !self.touches_down.contains(&id)
// And it isn't a duplicate
// This can occur on phones where touches can be pointer events
&& !self.new_touches.contains(&pos)
{
self.new_touches.push(pos);
}
self.touches_down.insert(id);
}
ClickPhase::End => {
self.touches_down.remove(&id);
}
ClickPhase::Ignored => {}
}
}
fn initialize_fonts(egui_ctx: &egui::Context) {
info!("Initializing fonts");
let mut fonts = FontDefinitions::default();
fonts.font_data.insert(
"LCD_Solid".to_owned(),
FontData::from_static(include_bytes!("../LcdSolid-VPzB.ttf")),
);
fonts
.families
.get_mut(&FontFamily::Monospace)
.unwrap()
.insert(0, "LCD_Solid".to_owned());
fonts
.families
.get_mut(&FontFamily::Monospace)
.unwrap()
.push("LCD_Solid".to_owned());
egui_ctx.set_fonts(fonts);
}
}
impl eframe::App for TemplateApp {
/// Called by the frame work to save state before shutdown.
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, eframe::APP_KEY, self);
}
/// Called each time the UI needs repainting, which may be many times per second.
/// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`.
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
let Self { .. } = self;
let mut style: Style = (*ctx.style()).clone();
style.spacing.button_padding = BUTTON_PADDING;
ctx.set_style(style);
self.calculator.error_state = ErrorState::default();
egui::CentralPanel::default().show(ctx, |ui| {
// ui.with_layout(self.layout, add_contents)
ui.input(|i: &egui::InputState| self.handle_input(i));
StripBuilder::new(ui)
// header
.size(Size::exact(65.0))
// Stack
// .size(Size::remainder().at_most(35.0))
.size(Size::remainder().at_least(40.0))
// Input
.size(Size::exact(40.0))
// Error
.size(Size::exact(20.0))
// Buttons
.size(Size::exact(450.0))
.vertical(|mut strip| {
strip.cell(|ui| {
ui.heading("rpn_rs_gui");
ui.hyperlink("https://gitea.austen-wares.com/stonewareslord/rpn_rs");
});
strip.cell(|ui| {
// Stack
ui.painter().rect_filled(
ui.available_rect_before_wrap(),
0.0,
Color32::LIGHT_GRAY,
);
self.draw_stack(ui);
});
strip.cell(|ui| {
self.draw_input(ui);
// Reset the error state and update `self.latest_error` if required
match std::mem::take(&mut self.calculator.error_state) {
ErrorState::NoModify => {}
ErrorState::Errored(e) => self.latest_error = Some(e),
ErrorState::Clear => self.latest_error = None,
}
});
strip.cell(|ui| {
// Error bar
self.draw_error(ui);
});
strip.cell(|ui| {
// Buttons
self.draw_buttons(ui);
});
})
});
self.new_touches.drain(..);
}
}
enum ErrorState {
// Do not touch the state of the error
NoModify,
// There was an error; this was the latest value
Errored(CalculatorError),
// We should clear the error at the end
Clear,
}
impl ErrorState {
fn errored(&mut self, e: CalculatorError) {
error!("Calculator input error: {e:?}");
match self {
Self::NoModify | ErrorState::Clear => *self = Self::Errored(e),
Self::Errored(_) => {
// We already errored, so do not change anything
}
}
}
fn success(&mut self) {
match self {
Self::NoModify => {
// There was a success and there was no previous failure, so clear any error value
*self = Self::Clear
}
Self::Errored(_) => {
// There was a previous error. We can't clear the error
}
Self::Clear => {
// The calculator error was already removed
}
}
}
}
impl Default for ErrorState {
fn default() -> ErrorState {
ErrorState::NoModify
}
}

View File

@ -0,0 +1,108 @@
use rpn_rs::calc::Calculator;
pub struct CalculatorButton {
pub value: char,
pub help: Option<&'static str>,
pub enabled: bool,
}
impl CalculatorButton {
const fn new(value: char, help: Option<&'static str>, enabled: bool) -> Self {
Self {
value,
help,
enabled,
}
}
}
pub(crate) const BUTTON_LAYOUT_SETTINGS: &[&[CalculatorButton]] = &[
&[CalculatorButton::new('q', Some("Exit"), true)],
&[
CalculatorButton::new('d', Some("Degrees"), true),
CalculatorButton::new('r', Some("Radians"), true),
CalculatorButton::new('g', Some("Grads"), true),
],
&[
CalculatorButton::new('_', Some("Default"), true),
CalculatorButton::new(',', Some("Comma separated"), true),
CalculatorButton::new(' ', Some("Space separated"), true),
],
&[
CalculatorButton::new('s', Some("Scientific"), true),
CalculatorButton::new('S', Some("Scientific (stack precision)"), true),
],
&[
CalculatorButton::new('e', Some("Engineering"), true),
CalculatorButton::new('E', Some("Engineering (stack precision)"), true),
],
&[
CalculatorButton::new('f', Some("Fixed"), true),
CalculatorButton::new('F', Some("Fixed (stack precision)"), true),
],
// CalculatorButton::new('w', Some("Do not write settings and stack on quit (default)"), true),
// CalculatorButton::new('W', Some("Write stack and settings on quit"), true),
// CalculatorButton::new('L', Some("Left align"), true),
// CalculatorButton::new('R', Some("Right align"), true),
];
pub(crate) const BUTTON_LAYOUT: &[&[CalculatorButton]] = &[
&[
// CalculatorButton::new('s', "Sin", true),
CalculatorButton::new('\\', Some("Drop"), true),
// TODO: Settings buttons
CalculatorButton::new('@', Some("Settings"), true),
// CalculatorButton::new(' ', Some("Enter"), true),
CalculatorButton::new('>', Some("Swap"), true),
CalculatorButton::new('$', Some("Backspace"), true),
],
&[
// CalculatorButton::new('|', "AbsoluteValue", true),
CalculatorButton::new('^', Some("Pow"), true),
CalculatorButton::new('U', Some("Redo"), true),
CalculatorButton::new('u', Some("Undo"), true),
CalculatorButton::new('L', Some("Ln"), true),
// CalculatorButton::new('l', Some("Log"), true),
],
&[
// CalculatorButton::new('c', "Cos", true),
CalculatorButton::new('v', Some("Sqrt"), true),
// CalculatorButton::new('%', Some("Modulo"), true),
CalculatorButton::new('n', Some("Negate"), true),
CalculatorButton::new('i', Some("Inverse"), true),
CalculatorButton::new('/', Some("Divide"), true),
],
&[
// CalculatorButton::new('t', "Tan", true),
CalculatorButton::new('7', None, true),
CalculatorButton::new('8', None, true),
CalculatorButton::new('9', None, true),
CalculatorButton::new('*', Some("Multiply"), true),
],
&[
// CalculatorButton::new('S', "ASin", true),
CalculatorButton::new('4', None, true),
CalculatorButton::new('5', None, true),
CalculatorButton::new('6', None, true),
CalculatorButton::new('-', Some("Subtract"), true),
],
&[
// CalculatorButton::new('C', "ACos", true),
CalculatorButton::new('1', None, true),
CalculatorButton::new('2', None, true),
CalculatorButton::new('3', None, true),
CalculatorButton::new('+', Some("Add"), true),
],
&[
// CalculatorButton::new('T', "ATan", true),
CalculatorButton::new('0', None, true),
CalculatorButton::new('.', Some("Decimal"), true),
CalculatorButton::new(' ', Some("Return"), true),
CalculatorButton::new(' ', Some("Return"), true),
],
// CalculatorButton::new ( '?', "IntegerDivide", true),
// CalculatorButton::new ( 'V', "BuildVector", true),
// CalculatorButton::new ( 'M', "BuildMatrix", true),
// CalculatorButton::new ( '_', "Deconstruct", true),
// CalculatorButton::new ( ', true)', "Transpose"),
];

4
rpn_rs_gui/src/lib.rs Normal file
View File

@ -0,0 +1,4 @@
#![warn(clippy::all, rust_2018_idioms)]
mod app;
pub use app::TemplateApp;

39
rpn_rs_gui/src/main.rs Normal file
View File

@ -0,0 +1,39 @@
#![warn(clippy::all, rust_2018_idioms)]
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
// When compiling natively:
#[cfg(not(target_arch = "wasm32"))]
fn main() -> eframe::Result<()> {
// Log to stdout (if you run with `RUST_LOG=debug`).
tracing_subscriber::fmt::init();
let native_options = eframe::NativeOptions::default();
eframe::run_native(
"RPN RS2",
native_options,
Box::new(|cc| Box::new(rpn_rs_gui::TemplateApp::new(cc))),
)
}
// when compiling to web using trunk.
#[cfg(target_arch = "wasm32")]
fn main() {
// Make sure panics are logged using `console.error`.
console_error_panic_hook::set_once();
// Redirect tracing to console.log and friends:
tracing_wasm::set_as_global_default();
let web_options = eframe::WebOptions::default();
wasm_bindgen_futures::spawn_local(async {
eframe::WebRunner::new()
.start(
"the_canvas_id", // hardcode it
web_options,
Box::new(|cc| Box::new(rpn_rs_gui::TemplateApp::new(cc))),
)
.await
.expect("failed to start eframe");
});
}

14
rpn_rs_tui/Cargo.toml Normal file
View File

@ -0,0 +1,14 @@
[package]
name = "rpn_rs_tui"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rpn_rs = {path=".."}
crossterm = { version = "0.18" }
tui = { version = "0.14", default-features = false, features = ["crossterm"] }
tracing-appender = "0.2.2"
tracing-subscriber = "0.3.17"
tracing = "0.1.37"

View File

@ -1,8 +1,9 @@
use std::iter;
use std::sync::mpsc;
use std::{iter, sync::mpsc};
use std::thread;
use std::time::{Duration, Instant};
use std::{
thread,
time::{Duration, Instant},
};
use crossterm::event::{self, Event as CEvent, KeyEvent};

View File

@ -5,12 +5,51 @@
// Cannot fix this, so don't warn me about it
#![allow(clippy::multiple_crate_versions)]
mod calc;
use rpn_rs::{calc, constants};
mod event;
use tracing::info;
const BORDER_SIZE: u16 = 2;
const HELP_TEXT: &str = "\
+ => Add s => Sin\n\
- => Subtract c => Cos\n\
/ => Divide S => ASin\n\
n => Negate C => ACos\n\
| => Abs T => ATan\n\
i => Inverse v => Sqrt\n\
% => Modulo u => Undo\n\
\\ => Drop U => Redo\n\
? => IntegerDivide ^ => Pow\n\
<ret> => Dup l => Log\n\
L => Ln e => *10^\n\
_ => Explode vector or matrix\n\
<right> => Swap <down> => Edit\n\
uU => Undo/Redo ` => Constants\n\
r => Load Register R => Save Register\n\
m => Macros @ => Settings\n\
^s => Save Config ^l => Load Config\
";
const SETTINGS_HELP_TEXT: &str = "\
d => Degrees\n\
r => Radians\n\
g => Grads\n\
_ => Default\n\
, => Comma separated\n\
<space> => Space separated\n\
s => Scientific\n\
S => Scientific (stack precision)\n\
e => Engineering\n\
E => Engineering (stack precision)\n\
f => Fixed\n\
F => Fixed (stack precision)\n\
w => Do not write settings and stack on quit (default)\n\
W => Write stack and settings on quit\n\
L => Left align\n\
R => Right align\
";
use calc::{
entries::CalculatorEntry,
errors::CalculatorResult,
types::{CalculatorAlignment, CalculatorState, RegisterState},
Calculator,
@ -23,7 +62,7 @@ use crossterm::{
use event::{Event, Events};
// use io::stdout;
use std::{cmp, convert::TryFrom, error::Error, io, io::Write};
use std::{cmp, convert::TryFrom, error::Error, fs::OpenOptions, io, io::Write};
use tui::{
backend::CrosstermBackend,
layout::{Constraint, Direction, Layout, Rect},
@ -70,32 +109,13 @@ impl Default for App {
}
impl App {
// This function is long because it contains help text
#[allow(clippy::too_many_lines)]
fn draw_clippy_dialogs<T: Write>(&mut self, f: &mut Frame<CrosstermBackend<T>>) {
match (&self.state, &self.calculator.state) {
(AppState::Help, _) => {
draw_clippy_rect(
&ClippyRectangle {
title: "Help",
msg: "\
+ => Add s => Sin\n\
- => Subtract c => Cos\n\
* => Multiply t => Tan\n\
/ => Divide S => ASin\n\
n => Negate C => ACos\n\
| => Abs T => ATan\n\
i => Inverse v => Sqrt\n\
% => Modulo u => Undo\n\
\\ => Drop U => Redo\n\
? => IntegerDivide ^ => Pow\n\
<ret> => Dup l => Log\n\
L => Ln e => *10^\n\
<right> => Swap <down> => Edit\n\
uU => Undo/Redo ` => Constants\n\
r => Load Register R => Save Register\n\
m => Macros @ => Settings\n\
^s => Save Config ^l => Load Config\
",
msg: HELP_TEXT,
},
f,
);
@ -113,7 +133,7 @@ impl App {
"{}: {} ({})",
key,
constant.help,
self.calculator.display_mode.format_number(constant.value)
constant.value.format_entry(&self.calculator.display_mode)
)
})
.fold(String::new(), |acc, s| acc + &s + "\n")
@ -134,7 +154,13 @@ impl App {
.calculator
.registers
.iter()
.map(|(key, value)| format!("{}: {}", key, value))
.map(|(key, value)| {
format!(
"{}: {}",
key,
value.format_entry(&self.calculator.display_mode)
)
})
.fold(String::new(), |acc, s| acc + &s + "\n")
.trim_end(),
},
@ -160,24 +186,7 @@ impl App {
draw_clippy_rect(
&ClippyRectangle {
title: "Settings",
msg: "\
d => Degrees\n\
r => Radians\n\
g => Grads\n\
_ => Default\n\
, => Comma separated\n\
<space> => Space separated\n\
s => Scientific\n\
S => Scientific (stack precision)\n\
e => Engineering\n\
E => Engineering (stack precision)\n\
f => Fixed\n\
F => Fixed (stack precision)\n\
w => Do not write settings and stack on quit (default)\n\
W => Write stack and settings on quit\n\
L => Left align\n\
R => Right align\
",
msg: SETTINGS_HELP_TEXT,
},
f,
);
@ -233,9 +242,11 @@ impl App {
.enumerate()
.rev()
.map(|(i, m)| {
let number = self.calculator.display_mode.format_number(*m);
let number = m.format_entry(&self.calculator.display_mode);
let content = match self.calculator.calculator_alignment {
CalculatorAlignment::Left => format!("{:>2}: {}", i, number),
CalculatorAlignment::Left => {
format!("{:>2}: {}", i, number)
}
CalculatorAlignment::Right => {
let ret = format!("{} :{:>2}", number, i);
if ret.len() < chunk.width.saturating_sub(BORDER_SIZE) as usize {
@ -298,7 +309,36 @@ impl App {
}
}
fn init_logging() {
use tracing_subscriber::{filter::LevelFilter, fmt, prelude::*, EnvFilter};
let file = OpenOptions::new()
.create(true)
.append(true)
.open("/tmp/rpn_rs.log")
.unwrap();
let (non_blocking_file, _guard) = tracing_appender::non_blocking(file);
tracing_subscriber::registry()
.with(fmt::layer().with_writer(non_blocking_file))
// .with(fmt::layer().with_writer(io::stderr))
.with(
EnvFilter::builder()
.with_default_directive(LevelFilter::ERROR.into())
.with_env_var("RPN_RS_LOG")
.from_env()
.unwrap(),
)
.init();
info!("Logging initialized");
}
fn main() -> Result<(), Box<dyn Error>> {
if std::env::var("ENABLE_LOGGING").as_ref().map(String::as_str) == Ok("1") {
init_logging();
}
enable_raw_mode()?;
let mut stdout = io::stdout();
@ -370,6 +410,10 @@ fn handle_key(app: &mut App, key: KeyEvent) -> CalculatorResult<CalculatorRespon
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::NONE,
@ -408,7 +452,7 @@ fn handle_key(app: &mut App, key: KeyEvent) -> CalculatorResult<CalculatorRespon
app.calculator.take_input(c)?;
}
}
_ => {}
_key_event => {}
},
(AppState::Help, _) => match key {
KeyEvent {
@ -461,7 +505,7 @@ struct ClippyRectangle<'a> {
}
impl ClippyRectangle<'_> {
// TODO: Make this static somehow
// Cannot be static since the clippy rectangle's text can change
fn size(&self) -> Dimensions {
let (width, height) = self.msg.lines().fold((0, 0), |(width, height), l| {
(cmp::max(width, l.len()), height + 1)

2
rustfmt.toml Normal file
View File

@ -0,0 +1,2 @@
unstable_features = true
imports_granularity = "Crate"

View File

@ -1,14 +1,21 @@
use rust_decimal::prelude::ToPrimitive;
pub mod entries;
use crate::constants;
use lazy_static::lazy_static;
pub mod errors;
pub mod operations;
pub mod types;
use crate::calc::entries::CalculatorEntry;
use rust_decimal::Decimal;
use confy::{load, store};
use entries::{Entry, Matrix, Number, Vector};
use errors::{CalculatorError, CalculatorResult};
use operations::{CalculatorOperation, CalculatorStateChange, MacroState, OpArgs};
use serde::ser::Serializer;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
use std::collections::{HashSet, VecDeque};
use operations::{
ArithmeticOperation, CalculatorOperation, CalculatorStateChange, MacroState, OpArgs,
};
use serde::{ser::Serializer, Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use types::{
CalculatorAlignment, CalculatorAngleMode, CalculatorConstant, CalculatorConstants,
CalculatorDisplayMode, CalculatorMacro, CalculatorMacros, CalculatorRegisters, CalculatorState,
@ -22,6 +29,79 @@ const APP_NAME: &str = "rpn_rs";
/// The default precision to sue
const DEFAULT_PRECISION: usize = 3;
lazy_static! {
static ref OPERATION_MAP: HashMap<char, CalculatorOperation> = [
('+', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Add),
),
('-', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Subtract),
),
('*', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Multiply),
),
('/', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Divide),
),
('n', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Negate),
),
('|', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::AbsoluteValue),
),
('i', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Inverse),
),
('%', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Modulo),
),
('?', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::IntegerDivide),
),
('s', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Sin),
),
('c', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Cos),
),
('t', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Tan),
),
('S', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::ASin),
),
('C', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::ACos),
),
('T', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::ATan),
),
('v', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Sqrt),
),
('^', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Pow),
),
('l', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Log),
),
('L', CalculatorOperation::ArithmeticOperation(
ArithmeticOperation::Ln),
),
// Temporary
('V', CalculatorOperation::BuildVector),
('M', CalculatorOperation::BuildMatrix),
('_', CalculatorOperation::Deconstruct),
(')', CalculatorOperation::Transpose),
// Special
('\\', CalculatorOperation::Drop),
(' ', CalculatorOperation::Dup),
('>', CalculatorOperation::Swap),
('u', CalculatorOperation::Undo),
('U', CalculatorOperation::Redo),
].iter().copied().collect();
}
/// The history mode of the entry - either a single change or a macro bound
#[derive(PartialEq, Debug, Serialize, Deserialize)]
enum HistoryMode {
@ -36,8 +116,6 @@ pub struct Calculator {
/// The entry buffer
#[serde(skip)]
l: String,
/// The stack
pub stack: VecDeque<f64>,
/// True if the user would like to save on quit
save_on_close: bool,
/// Left or right aligned display
@ -48,6 +126,8 @@ pub struct Calculator {
/// The display format such as separated or scientific
#[serde(flatten)]
pub display_mode: CalculatorDisplayMode,
/// The stack
pub stack: VecDeque<Entry>,
/// A set of the currently running macros, used for ensuring there are no recursive macro calls
#[serde(skip)]
active_macros: HashSet<char>,
@ -93,7 +173,7 @@ impl Default for Calculator {
active_macros: HashSet::new(),
registers: CalculatorRegisters::new(),
state: CalculatorState::Normal,
stack: vec![1.0, 2.0].into_iter().collect(),
stack: vec![].into_iter().collect(),
save_on_close: false,
macros: [
(
@ -119,21 +199,27 @@ impl Default for Calculator {
't',
CalculatorConstant {
help: String::from("Tau (2pi)"),
value: std::f64::consts::TAU,
value: Entry::Number(Number {
value: constants::TAU,
}),
},
),
(
'e',
CalculatorConstant {
help: String::from("Euler's Number e"),
value: std::f64::consts::E,
value: Entry::Number(Number {
value: constants::E,
}),
},
),
(
'p',
CalculatorConstant {
help: String::from("Pi"),
value: std::f64::consts::PI,
value: Entry::Number(Number {
value: constants::PI,
}),
},
),
]
@ -157,10 +243,10 @@ impl Calculator {
}
pub fn load_config() -> CalculatorResult<Self> {
load(APP_NAME).map_err(|e| CalculatorError::LoadError(Some(e)))
load(APP_NAME, None).map_err(|e| CalculatorError::LoadError(Some(e)))
}
pub fn save_config(&self) -> CalculatorResult<()> {
store(APP_NAME, self).map_err(|e| CalculatorError::SaveError(Some(e)))
store(APP_NAME, None, self).map_err(|e| CalculatorError::SaveError(Some(e)))
}
pub fn take_input(&mut self, c: char) -> CalculatorResult<()> {
@ -175,6 +261,8 @@ impl Calculator {
CalculatorState::WaitingForSetting => self.setting_input(c),
}
}
// This function is very long, but it is just a match, so it should be fine
#[allow(clippy::too_many_lines)]
fn normal_input(&mut self, c: char) -> CalculatorResult<()> {
match c {
c @ '0'..='9' | c @ '.' | c @ 'e' => match c {
@ -183,11 +271,7 @@ impl Calculator {
Ok(())
}
'e' => {
if self.l.is_empty() {
let f = self.pop().or(Err(CalculatorError::NotEnoughStackEntries))?;
self.l = f.to_string();
}
self.edit()?;
if !self.l.contains('e') {
self.l.push('e');
@ -203,31 +287,6 @@ impl Calculator {
}
_ => Err(CalculatorError::ParseError),
},
'+' => self.op(CalculatorOperation::Add),
'-' => self.op(CalculatorOperation::Subtract),
'*' => self.op(CalculatorOperation::Multiply),
'/' => self.op(CalculatorOperation::Divide),
'n' => self.op(CalculatorOperation::Negate),
'|' => self.op(CalculatorOperation::AbsoluteValue),
'i' => self.op(CalculatorOperation::Inverse),
'%' => self.op(CalculatorOperation::Modulo),
'\\' => self.op(CalculatorOperation::Drop),
'?' => self.op(CalculatorOperation::IntegerDivide),
' ' => self.op(CalculatorOperation::Dup),
'>' => self.op(CalculatorOperation::Swap),
's' => self.op(CalculatorOperation::Sin),
'c' => self.op(CalculatorOperation::Cos),
't' => self.op(CalculatorOperation::Tan),
'S' => self.op(CalculatorOperation::ASin),
'C' => self.op(CalculatorOperation::ACos),
'T' => self.op(CalculatorOperation::ATan),
'v' => self.op(CalculatorOperation::Sqrt),
'^' => self.op(CalculatorOperation::Pow),
'l' => self.op(CalculatorOperation::Log),
'L' => self.op(CalculatorOperation::Ln),
// Special
'u' => self.op(CalculatorOperation::Undo),
'U' => self.op(CalculatorOperation::Redo),
// State modifiers
'm' => {
self.state = CalculatorState::WaitingForMacro;
@ -249,7 +308,9 @@ impl Calculator {
self.state = CalculatorState::WaitingForSetting;
Ok(())
}
_ => Err(CalculatorError::NoSuchOperator(c)),
c => self.op(OPERATION_MAP
.get(&c)
.ok_or(CalculatorError::NoSuchOperator(c))?),
}
}
fn constant_input(&mut self, c: char) -> CalculatorResult<()> {
@ -257,7 +318,8 @@ impl Calculator {
.constants
.get(&c)
.ok_or(CalculatorError::NoSuchConstant(c))?
.value;
.value
.clone();
self.push(f)?;
self.state = CalculatorState::Normal;
@ -273,7 +335,7 @@ impl Calculator {
}
// Record the macro started, if this is the outer macro
self.op(CalculatorOperation::Macro(MacroState::Start))?;
self.op(&CalculatorOperation::Macro(MacroState::Start))?;
// Record that we are running macro c
self.active_macros.insert(c);
@ -292,7 +354,7 @@ impl Calculator {
self.active_macros.remove(&c);
// Record the macro is over, if this is the outer macro
self.op(CalculatorOperation::Macro(MacroState::End))?;
self.op(&CalculatorOperation::Macro(MacroState::End))?;
Ok(())
}
@ -306,8 +368,8 @@ impl Calculator {
let f = self
.registers
.get(&c)
.ok_or(CalculatorError::NoSuchRegister(c))?;
let f = *f;
.ok_or(CalculatorError::NoSuchRegister(c))?
.clone();
self.push(f)?;
}
}
@ -372,7 +434,7 @@ impl Calculator {
if !self.active_macros.is_empty() {
self.active_macros.clear();
// Should always be successful, but report the error if there is one
self.op(CalculatorOperation::Macro(MacroState::End))?;
self.op(&CalculatorOperation::Macro(MacroState::End))?;
}
Ok(())
}
@ -387,10 +449,15 @@ impl Calculator {
return Ok(());
}
self.l = self
.pop()
.or(Err(CalculatorError::NotEnoughStackEntries))?
.to_string();
// Temporary check to see if we can get an editable string
let str = self.peek(0)?.to_editable_string()?;
// If we got here, then there was no error. Pop
self.pop()?;
// Set l after popping
self.l = str;
Ok(())
}
/// Get the value of l
@ -415,8 +482,11 @@ impl Calculator {
return Ok(false);
}
let f = self.l.parse::<f64>().or(Err(CalculatorError::ParseError))?;
self.push(f)?;
let f = self
.l
.parse::<Decimal>()
.or(Err(CalculatorError::ParseError))?;
self.push(Entry::Number(Number { value: f }))?;
self.l.clear();
Ok(true)
}
@ -426,51 +496,117 @@ impl Calculator {
}
/// Pushes a value onto the stack and makes a state change
fn push(&mut self, f: f64) -> CalculatorResult<()> {
fn push(&mut self, f: Entry) -> CalculatorResult<()> {
self.direct_state_change(CalculatorStateChange {
pop: OpArgs::None,
push: OpArgs::Unary(f),
})
}
/// Returns the value of the bottom of the stack by popping it using a state change
pub fn pop(&mut self) -> CalculatorResult<f64> {
pub fn pop(&mut self) -> CalculatorResult<Entry> {
let f = self.peek(0)?;
self.direct_state_change(CalculatorStateChange {
pop: OpArgs::Unary(f),
pop: OpArgs::Unary(f.clone()),
push: OpArgs::None,
})?;
Ok(f)
}
/// Returns a calculator value
fn peek(&mut self, idx: usize) -> CalculatorResult<f64> {
fn peek(&mut self, idx: usize) -> CalculatorResult<Entry> {
self.flush_l()?;
match self.stack.get(idx) {
None => Err(CalculatorError::NotEnoughStackEntries),
Some(r) => Ok(*r),
Some(r) => Ok(r.clone()),
}
}
/// Pops a precision instead of an f64. Precisions are of type usize
pub fn pop_precision(&mut self) -> CalculatorResult<usize> {
let f = self.peek(0)?;
// Ensure this can be cast to a usize
if !f.is_finite() || f.is_sign_negative() {
return Err(CalculatorError::ArithmeticError);
/// Pops a usize
pub fn peek_usize(&mut self) -> CalculatorResult<(usize, Entry)> {
let entry = self.peek(0)?;
let f = match entry {
Entry::Matrix(_) | Entry::Vector(_) => return Err(CalculatorError::TypeMismatch),
Entry::Number(Number { value }) => value,
};
match f.to_usize() {
Some(u) => Ok((u, entry)),
None => Err(CalculatorError::ArithmeticError),
}
#[allow(clippy::cast_sign_loss)]
let u = f as usize;
}
/// Pops a precision instead of an Entry. Precisions are of type usize
pub fn pop_precision(&mut self) -> CalculatorResult<usize> {
let (u, entry) = self.peek_usize()?;
if u > MAX_PRECISION {
return Err(CalculatorError::PrecisionTooHigh);
}
self.direct_state_change(CalculatorStateChange {
pop: OpArgs::Unary(f),
pop: OpArgs::Unary(entry),
push: OpArgs::None,
})?;
Ok(u)
}
pub fn build_vector(&mut self) -> CalculatorResult<CalculatorStateChange> {
let (count, count_entry) = self.peek_usize()?;
// The arguments need to be reversed since 1 2 3 3V should produce a vector [1;2;3], not [3;2;1]
let mut entries: Vec<Entry> = (1..=count)
.rev()
.map(|i| self.peek(i))
.collect::<CalculatorResult<Vec<Entry>>>()?;
let new_entry = Vector::from(&entries)?;
entries.push(count_entry);
Ok(CalculatorStateChange {
// Since we reversed the arguments once, we need to reverse again
pop: OpArgs::Variable(entries.into_iter().rev().collect::<Vec<Entry>>()),
push: OpArgs::Unary(new_entry),
})
}
pub fn build_matrix(&mut self) -> CalculatorResult<CalculatorStateChange> {
let (count, count_entry) = self.peek_usize()?;
// The arguments need to be reversed, see build_vector
let mut entries: Vec<Entry> = (1..=count)
.rev()
.map(|i| self.peek(i))
.collect::<CalculatorResult<Vec<Entry>>>()?;
let new_entry = Matrix::from(&entries[..])?;
entries.push(count_entry);
Ok(CalculatorStateChange {
// Since we reversed the arguments once, we need to reverse again
pop: OpArgs::Variable(entries.into_iter().rev().collect::<Vec<Entry>>()),
push: OpArgs::Unary(new_entry),
})
}
pub fn deconstruct(&mut self) -> CalculatorResult<CalculatorStateChange> {
let entry = self.peek(0)?;
let entries: Vec<Entry> = match &entry {
Entry::Matrix(matrix) => Ok(matrix
.vectors
.iter()
.rev()
.map(|v| Entry::Vector(v.clone()))
.collect()),
Entry::Vector(vector) => Ok(vector
.values
.iter()
.rev()
.map(|n| Entry::Number(*n))
.collect()),
Entry::Number(_number) => Err(CalculatorError::TypeMismatch),
}?;
Ok(CalculatorStateChange {
pop: OpArgs::Unary(entry),
push: OpArgs::Variable(entries),
})
}
/// Performs a calculator operation such as undo, redo, operator, or dup
pub fn op(&mut self, op: CalculatorOperation) -> CalculatorResult<()> {
pub fn op(&mut self, op: &CalculatorOperation) -> CalculatorResult<()> {
// Dup is special -- don't actually run it if l needs to be flushed
if self.flush_l()? {
if let CalculatorOperation::Dup = op {
@ -478,68 +614,76 @@ impl Calculator {
}
}
let state_change = match op {
CalculatorOperation::Add => self.binary_op(|[a, b]| OpArgs::Unary(b + a)),
CalculatorOperation::Subtract => self.binary_op(|[a, b]| OpArgs::Unary(b - a)),
CalculatorOperation::Multiply => self.binary_op(|[a, b]| OpArgs::Unary(b * a)),
CalculatorOperation::Divide => self.binary_op(|[a, b]| OpArgs::Unary(b / a)),
CalculatorOperation::IntegerDivide => {
self.binary_op(|[a, b]| OpArgs::Unary(b.div_euclid(a)))
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Add) => {
self.binary_op(|[a, b]| Ok(OpArgs::Unary(b.add(&a)?)))
}
CalculatorOperation::Negate => self.unary_op(|a| OpArgs::Unary(-a)),
CalculatorOperation::AbsoluteValue => self.unary_op(|a| OpArgs::Unary(a.abs())),
CalculatorOperation::Inverse => self.unary_op(|a| OpArgs::Unary(a.recip())),
CalculatorOperation::Modulo => self.binary_op(|[a, b]| OpArgs::Unary(b % a)),
//CalculatorOperation::Remainder => self.binary_op(|[a, b]| OpArgs::Unary(b.rem_euclid(a))),
CalculatorOperation::Dup => self.unary_op(|a| OpArgs::Binary([a, a])),
CalculatorOperation::Drop => self.unary_op(|_| OpArgs::None),
CalculatorOperation::Swap => self.binary_op(|[a, b]| OpArgs::Binary([b, a])),
CalculatorOperation::Sin => self.unary_op(match self.angle_mode {
CalculatorAngleMode::Degrees => |a: f64| OpArgs::Unary(a.to_radians().sin()),
CalculatorAngleMode::Radians => |a: f64| OpArgs::Unary(a.sin()),
CalculatorAngleMode::Grads => {
|a: f64| OpArgs::Unary((a * std::f64::consts::PI / 200.0).sin())
}
}),
CalculatorOperation::Cos => self.unary_op(match self.angle_mode {
CalculatorAngleMode::Degrees => |a: f64| OpArgs::Unary(a.to_radians().cos()),
CalculatorAngleMode::Radians => |a: f64| OpArgs::Unary(a.cos()),
CalculatorAngleMode::Grads => {
|a: f64| OpArgs::Unary((a * std::f64::consts::PI / 200.0).cos())
}
}),
CalculatorOperation::Tan => self.unary_op(match self.angle_mode {
CalculatorAngleMode::Degrees => |a: f64| OpArgs::Unary(a.to_radians().tan()),
CalculatorAngleMode::Radians => |a: f64| OpArgs::Unary(a.tan()),
CalculatorAngleMode::Grads => {
|a: f64| OpArgs::Unary((a * std::f64::consts::PI / 200.0).tan())
}
}),
CalculatorOperation::ASin => self.unary_op(match self.angle_mode {
CalculatorAngleMode::Degrees => |a: f64| OpArgs::Unary(a.asin().to_degrees()),
CalculatorAngleMode::Radians => |a: f64| OpArgs::Unary(a.asin()),
CalculatorAngleMode::Grads => {
|a: f64| OpArgs::Unary(a.asin() * std::f64::consts::PI / 200.0)
}
}),
CalculatorOperation::ACos => self.unary_op(match self.angle_mode {
CalculatorAngleMode::Degrees => |a: f64| OpArgs::Unary(a.acos().to_degrees()),
CalculatorAngleMode::Radians => |a: f64| OpArgs::Unary(a.acos()),
CalculatorAngleMode::Grads => {
|a: f64| OpArgs::Unary(a.acos() * std::f64::consts::PI / 200.0)
}
}),
CalculatorOperation::ATan => self.unary_op(match self.angle_mode {
CalculatorAngleMode::Degrees => |a: f64| OpArgs::Unary(a.atan().to_degrees()),
CalculatorAngleMode::Radians => |a: f64| OpArgs::Unary(a.atan()),
CalculatorAngleMode::Grads => {
|a: f64| OpArgs::Unary(a.atan() * std::f64::consts::PI / 200.0)
}
}),
CalculatorOperation::Sqrt => self.unary_op(|a| OpArgs::Unary(a.sqrt())),
CalculatorOperation::Log => self.unary_op(|a| OpArgs::Unary(a.log10())),
CalculatorOperation::Ln => self.unary_op(|a| OpArgs::Unary(a.ln())),
CalculatorOperation::Pow => self.binary_op(|[a, b]| OpArgs::Unary(b.powf(a))),
CalculatorOperation::E => self.binary_op(|[a, b]| OpArgs::Unary(b * 10.0_f64.powf(a))),
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Subtract) => {
self.binary_op(|[a, b]| Ok(OpArgs::Unary(b.sub(&a)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Multiply) => {
self.binary_op(|[a, b]| Ok(OpArgs::Unary(b.mul(&a)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Divide) => {
self.binary_op(|[a, b]| Ok(OpArgs::Unary(b.div(&a)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::IntegerDivide) => {
self.binary_op(|[a, b]| Ok(OpArgs::Unary(b.int_divide(&a)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Negate) => {
self.unary_op(|a| Ok(OpArgs::Unary(a.negate()?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::AbsoluteValue) => {
self.unary_op(|a| Ok(OpArgs::Unary(a.abs()?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Inverse) => {
self.unary_op(|a| Ok(OpArgs::Unary(a.inverse()?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Modulo) => {
self.binary_op(|[a, b]| Ok(OpArgs::Unary(b.modulo(&a)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Sin) => {
let angle_mode = self.angle_mode;
self.unary_op(|a| Ok(OpArgs::Unary(a.sin(angle_mode)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Cos) => {
let angle_mode = self.angle_mode;
self.unary_op(|a| Ok(OpArgs::Unary(a.cos(angle_mode)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Tan) => {
let angle_mode = self.angle_mode;
self.unary_op(|a| Ok(OpArgs::Unary(a.tan(angle_mode)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::ASin) => {
let angle_mode = self.angle_mode;
self.unary_op(|a| Ok(OpArgs::Unary(a.asin(angle_mode)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::ACos) => {
let angle_mode = self.angle_mode;
self.unary_op(|a| Ok(OpArgs::Unary(a.acos(angle_mode)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::ATan) => {
let angle_mode = self.angle_mode;
self.unary_op(|a| Ok(OpArgs::Unary(a.atan(angle_mode)?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Sqrt) => {
self.unary_op(|a| Ok(OpArgs::Unary(a.sqrt()?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Log) => {
self.unary_op(|a| Ok(OpArgs::Unary(a.log()?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Ln) => {
self.unary_op(|a| Ok(OpArgs::Unary(a.ln()?)))
}
CalculatorOperation::ArithmeticOperation(ArithmeticOperation::Pow) => {
self.binary_op(|[a, b]| Ok(OpArgs::Unary(b.pow(&a)?)))
}
CalculatorOperation::BuildVector => self.build_vector(),
CalculatorOperation::BuildMatrix => self.build_matrix(),
CalculatorOperation::Deconstruct => self.deconstruct(),
CalculatorOperation::Transpose => self.unary_op(|a| Ok(OpArgs::Unary(a.transpose()?))),
CalculatorOperation::Dup => self.unary_op(|a| Ok(OpArgs::Binary([a.clone(), a]))),
CalculatorOperation::Drop => self.unary_op(|_| Ok(OpArgs::None)),
CalculatorOperation::Swap => self.binary_op(|[a, b]| Ok(OpArgs::Binary([b, a]))),
CalculatorOperation::Undo => return self.history_op(false),
CalculatorOperation::Redo => return self.history_op(true),
// Macros are a no-op operator; need to insert for undo/redo
@ -551,7 +695,7 @@ impl Calculator {
Ok(CalculatorStateChange {
pop: OpArgs::None,
push: OpArgs::Macro(state),
push: OpArgs::Macro(*state),
})
}
};
@ -614,40 +758,6 @@ impl Calculator {
}
}
}
/// Performs a state change on a unary operation
fn unary_op(
&mut self,
op: impl FnOnce(f64) -> OpArgs,
) -> CalculatorResult<CalculatorStateChange> {
let arg = self
.stack
.get(0)
.ok_or(CalculatorError::NotEnoughStackEntries)?;
Ok(CalculatorStateChange {
pop: OpArgs::Unary(*arg),
push: op(*arg),
})
}
/// Performs a state change on a binary operation
fn binary_op(
&mut self,
op: impl FnOnce([f64; 2]) -> OpArgs,
) -> CalculatorResult<CalculatorStateChange> {
let args: [f64; 2] = [
*self
.stack
.get(0)
.ok_or(CalculatorError::NotEnoughStackEntries)?,
*self
.stack
.get(1)
.ok_or(CalculatorError::NotEnoughStackEntries)?,
];
Ok(CalculatorStateChange {
pop: OpArgs::Binary(args),
push: op(args),
})
}
/// Performs a state change and clears the redo buf. This is used when *not* undoing/redoing.
fn direct_state_change(&mut self, c: CalculatorStateChange) -> CalculatorResult<()> {
@ -673,39 +783,59 @@ impl Calculator {
match to_push {
OpArgs::Unary(a) => {
if a.is_nan() || a.is_infinite() {
if !a.is_valid() {
return Err(CalculatorError::ArithmeticError);
}
}
OpArgs::Binary([a, b]) => {
if a.is_nan() || b.is_nan() || a.is_infinite() || b.is_infinite() {
if !a.is_valid() || !b.is_valid() {
return Err(CalculatorError::ArithmeticError);
}
}
OpArgs::Macro(_) | OpArgs::None => {}
OpArgs::Variable(entries) => {
if entries.iter().any(|e| !e.is_valid()) {
return Err(CalculatorError::ArithmeticError);
}
}
};
match to_pop {
OpArgs::Unary(a) => {
self.stack_eq(0, *a)?;
self.stack_eq(0, a)?;
self.stack.pop_front();
}
OpArgs::Binary([a, b]) => {
self.stack_eq(0, *a)?;
self.stack_eq(1, *b)?;
self.stack_eq(0, a)?;
self.stack_eq(1, b)?;
self.stack.pop_front();
self.stack.pop_front();
}
OpArgs::Variable(entries) => {
entries
.iter()
.enumerate()
.try_for_each(|(i, e)| self.stack_eq(i, e))?;
for _ in 0..entries.len() {
self.stack.pop_front();
}
}
OpArgs::Macro(_) | OpArgs::None => {}
};
match to_push {
OpArgs::Unary(a) => {
self.stack.push_front(*a);
self.stack.push_front(a.clone());
}
OpArgs::Binary([a, b]) => {
self.stack.push_front(*b);
self.stack.push_front(*a);
self.stack.push_front(b.clone());
self.stack.push_front(a.clone());
}
OpArgs::Variable(entries) => {
for e in entries.iter().rev() {
self.stack.push_front(e.clone());
}
}
OpArgs::Macro(_) | OpArgs::None => {}
};
@ -720,68 +850,95 @@ impl Calculator {
}
/// Checks if a value on the stack is equal to a given value
fn stack_eq(&mut self, idx: usize, value: f64) -> CalculatorResult<()> {
if (self.peek(idx)? - value).abs() > f64::EPSILON {
fn stack_eq(&mut self, idx: usize, value: &Entry) -> CalculatorResult<()> {
if self.peek(idx)? == *value {
Ok(())
} else {
Err(CalculatorError::CorruptStateChange(format!(
"Stack index {} should be {}, but is {}",
idx,
value,
self.peek(idx)?,
)))
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn gen_sample_calculator() -> Calculator {
let mut calc = Calculator::default();
// Empty the stack and push a few numbers
input_str(&mut calc, "\\\\123 456 789");
calc
}
fn input_str(calc: &mut Calculator, input: &str) {
for c in input.chars() {
assert!(calc.take_input(c).is_ok());
}
}
fn assert_float_eq(a: f64, b: f64) {
assert!(a - b < f64::EPSILON, "Value '{}' did not match '{}'", a, b);
/// Performs a state change on a unary operation
fn unary_op(
&mut self,
op: impl FnOnce(Entry) -> CalculatorResult<OpArgs>,
) -> CalculatorResult<CalculatorStateChange> {
let arg = self.peek(0)?;
Ok(CalculatorStateChange {
pop: OpArgs::Unary(arg.clone()),
push: op(arg)?,
})
}
#[test]
fn basic_ops() {
let mut calc = gen_sample_calculator();
assert_float_eq(calc.peek(0).unwrap(), 789_f64);
input_str(&mut calc, "+");
assert_float_eq(calc.peek(0).unwrap(), 1_245_f64);
input_str(&mut calc, "+");
assert_float_eq(calc.peek(0).unwrap(), 1_368_f64);
// The stack now only has one element
assert!(!calc.take_input('+').is_ok());
input_str(&mut calc, "n");
assert_float_eq(calc.pop().unwrap(), -1_368_f64);
input_str(&mut calc, "64v100v");
assert_float_eq(calc.pop().unwrap(), 10_f64);
assert_float_eq(calc.pop().unwrap(), 8_f64);
}
#[test]
fn peek() {
let mut calc = gen_sample_calculator();
// There should be three digits
assert_float_eq(calc.peek(0).unwrap(), 789_f64);
assert_float_eq(calc.peek(1).unwrap(), 456_f64);
assert_float_eq(calc.peek(2).unwrap(), 123_f64);
assert!(!calc.peek(3).is_ok());
assert!(true);
/// Performs a state change on a binary operation
fn binary_op(
&mut self,
op: impl FnOnce([Entry; 2]) -> CalculatorResult<OpArgs>,
) -> CalculatorResult<CalculatorStateChange> {
let args: [Entry; 2] = [self.peek(0)?, self.peek(1)?];
Ok(CalculatorStateChange {
pop: OpArgs::Binary(args.clone()),
push: op(args)?,
})
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// fn gen_sample_calculator() -> Calculator {
// let mut calc = Calculator::default();
// // Empty the stack and push a few numbers
// input_str(&mut calc, "\\\\123 456 789");
// calc
// }
// fn input_str(calc: &mut Calculator, input: &str) {
// for c in input.chars() {
// assert!(calc.take_input(c).is_ok());
// }
// }
// fn assert_float_eq(a: f64, b: f64) {
// assert!(
// (a - b).abs() < f64::EPSILON,
// "Value '{}' did not match '{}'",
// a,
// b
// );
// }
// #[test]
// fn basic_ops() {
// let mut calc = gen_sample_calculator();
// assert_float_eq(calc.peek(0).unwrap(), 789_f64);
// input_str(&mut calc, "+");
// assert_float_eq(calc.peek(0).unwrap(), 1_245_f64);
// input_str(&mut calc, "+");
// assert_float_eq(calc.peek(0).unwrap(), 1_368_f64);
// // The stack now only has one element
// assert!(!calc.take_input('+').is_ok());
// input_str(&mut calc, "n");
// assert_float_eq(calc.pop().unwrap(), -1_368_f64);
// input_str(&mut calc, "64v100v");
// assert_float_eq(calc.pop().unwrap(), 10_f64);
// assert_float_eq(calc.pop().unwrap(), 8_f64);
// }
// #[test]
// fn peek() {
// let mut calc = gen_sample_calculator();
// // There should be three digits
// assert_float_eq(calc.peek(0).unwrap(), 789_f64);
// assert_float_eq(calc.peek(1).unwrap(), 456_f64);
// assert_float_eq(calc.peek(2).unwrap(), 123_f64);
// assert!(!calc.peek(3).is_ok());
// }
// }

496
src/calc/entries.rs Normal file
View File

@ -0,0 +1,496 @@
// TODO: Clippy is recommending pass by value instead of by ref, but I plan to add imaginary numbers, which will change this
#![allow(clippy::trivially_copy_pass_by_ref)]
mod matrix;
mod number;
mod vector;
use super::errors::CalculatorResult;
use crate::calc::{types::CalculatorAngleMode, CalculatorDisplayMode};
pub use matrix::{Matrix, MatrixDimensions};
pub use number::Number;
use serde::{Deserialize, Serialize};
use std::{cmp::PartialEq, fmt};
pub use vector::{Vector, VectorDirection};
pub trait CalculatorEntry
where
Self: Sized,
{
// Misc
fn is_valid(&self) -> bool;
/// Turns self into a valid entry
fn validate(self) -> CalculatorResult<Entry>;
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String;
fn to_editable_string(&self) -> CalculatorResult<String>;
// Mathematical operations
// Unary
fn negate(&self) -> CalculatorResult<Entry>;
fn abs(&self) -> CalculatorResult<Entry>;
fn inverse(&self) -> CalculatorResult<Entry>;
fn transpose(&self) -> CalculatorResult<Entry>;
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry>;
fn sqrt(&self) -> CalculatorResult<Entry>;
fn log(&self) -> CalculatorResult<Entry>;
fn ln(&self) -> CalculatorResult<Entry>;
// Binary
fn add(&self, arg: &Entry) -> CalculatorResult<Entry>;
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry>;
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry>;
fn div(&self, arg: &Entry) -> CalculatorResult<Entry>;
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry>;
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry>;
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry>;
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Entry {
Number(Number),
Vector(Vector),
Matrix(Matrix),
}
// macro_rules! op_child_call {
// ($a:ident, $op:ident) => {
// match $a {
// Self::Number(number) => number.$op(),
// Self::Vector(vector) => vector.$op(),
// }
// };
// ($a:ident, $op:ident, $arg:expr) => {
// match $a {
// Self::Number(number) => number.$op($arg),
// Self::Vector(vector) => vector.$op($arg),
// }
// };
// }
impl CalculatorEntry for Entry {
fn to_editable_string(&self) -> CalculatorResult<String> {
match self {
Self::Number(number) => number.to_editable_string(),
Self::Vector(vector) => vector.to_editable_string(),
Self::Matrix(matrix) => matrix.to_editable_string(),
}
}
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
match self {
Self::Number(number) => number.format_entry(display_mode),
Self::Vector(vector) => vector.format_entry(display_mode),
Self::Matrix(matrix) => matrix.format_entry(display_mode),
}
}
fn is_valid(&self) -> bool {
match self {
Self::Number(number) => number.is_valid(),
Self::Vector(vector) => vector.is_valid(),
Self::Matrix(matrix) => matrix.is_valid(),
}
}
fn validate(self) -> CalculatorResult<Entry> {
match self {
Self::Number(number) => number.validate(),
Self::Vector(vector) => vector.validate(),
Self::Matrix(matrix) => matrix.validate(),
}
}
fn negate(&self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.negate(),
Self::Vector(vector) => vector.negate(),
Self::Matrix(matrix) => matrix.negate(),
}
}
fn abs(&self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.abs(),
Self::Vector(vector) => vector.abs(),
Self::Matrix(matrix) => matrix.abs(),
}
}
fn inverse(&self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.inverse(),
Self::Vector(vector) => vector.inverse(),
Self::Matrix(matrix) => matrix.inverse(),
}
}
fn transpose(&self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.transpose(),
Self::Vector(vector) => vector.transpose(),
Self::Matrix(matrix) => matrix.transpose(),
}
}
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.sin(angle_mode),
Self::Vector(vector) => vector.sin(angle_mode),
Self::Matrix(matrix) => matrix.sin(angle_mode),
}
}
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.cos(angle_mode),
Self::Vector(vector) => vector.cos(angle_mode),
Self::Matrix(matrix) => matrix.cos(angle_mode),
}
}
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.tan(angle_mode),
Self::Vector(vector) => vector.tan(angle_mode),
Self::Matrix(matrix) => matrix.tan(angle_mode),
}
}
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.asin(angle_mode),
Self::Vector(vector) => vector.asin(angle_mode),
Self::Matrix(matrix) => matrix.asin(angle_mode),
}
}
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.acos(angle_mode),
Self::Vector(vector) => vector.acos(angle_mode),
Self::Matrix(matrix) => matrix.acos(angle_mode),
}
}
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.atan(angle_mode),
Self::Vector(vector) => vector.atan(angle_mode),
Self::Matrix(matrix) => matrix.atan(angle_mode),
}
}
fn sqrt(&self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.sqrt(),
Self::Vector(vector) => vector.sqrt(),
Self::Matrix(matrix) => matrix.sqrt(),
}
}
fn log(&self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.log(),
Self::Vector(vector) => vector.log(),
Self::Matrix(matrix) => matrix.log(),
}
}
fn ln(&self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.ln(),
Self::Vector(vector) => vector.ln(),
Self::Matrix(matrix) => matrix.ln(),
}
}
fn add(&self, arg: &Self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.add(arg),
Self::Vector(vector) => vector.add(arg),
Self::Matrix(matrix) => matrix.add(arg),
}
}
fn sub(&self, arg: &Self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.sub(arg),
Self::Vector(vector) => vector.sub(arg),
Self::Matrix(matrix) => matrix.sub(arg),
}
}
fn mul(&self, arg: &Self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.mul(arg),
Self::Vector(vector) => vector.mul(arg),
Self::Matrix(matrix) => matrix.mul(arg),
}
}
fn div(&self, arg: &Self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.div(arg),
Self::Vector(vector) => vector.div(arg),
Self::Matrix(matrix) => matrix.div(arg),
}
}
fn int_divide(&self, arg: &Self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.int_divide(arg),
Self::Vector(vector) => vector.int_divide(arg),
Self::Matrix(matrix) => matrix.int_divide(arg),
}
}
fn modulo(&self, arg: &Self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.modulo(arg),
Self::Vector(vector) => vector.modulo(arg),
Self::Matrix(matrix) => matrix.modulo(arg),
}
}
fn pow(&self, arg: &Self) -> CalculatorResult<Self> {
match self {
Self::Number(number) => number.pow(arg),
Self::Vector(vector) => vector.pow(arg),
Self::Matrix(matrix) => matrix.pow(arg),
}
}
}
impl fmt::Display for Entry {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Matrix(matrix) => write!(f, "{}", matrix),
Self::Vector(vector) => write!(f, "{}", vector),
Self::Number(number) => write!(f, "{}", number),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
fn valid_square_matrix() -> Entry {
Entry::Matrix(Matrix {
dimensions: MatrixDimensions {
columns: 3,
rows: 3,
},
vectors: vec![
Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(1.0) },
Number { value: dec!(2.0) },
Number { value: dec!(-3.0) },
],
},
Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(4.0) },
Number { value: dec!(-5.0) },
Number { value: dec!(0.0) },
],
},
Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(-7.0) },
Number { value: dec!(8.0) },
Number { value: dec!(9.0) },
],
},
],
})
}
#[test]
fn valid_cascading_operations() {
for (op, entry, goal) in &[
(
"negate",
valid_square_matrix().negate(),
Matrix::from(&[
Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(-1.0) },
Number { value: dec!(-2.0) },
Number { value: dec!(3.0) },
],
}),
Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(-4.0) },
Number { value: dec!(5.0) },
Number { value: dec!(-0.0) },
],
}),
Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(7.0) },
Number { value: dec!(-8.0) },
Number { value: dec!(-9.0) },
],
}),
]),
),
// (
// "abs",
// valid_square_matrix().abs(),
// Matrix::from(&[
// Entry::Vector(Vector {
// direction: VectorDirection::Column,
// values: vec![
// Number { value: dec!(1.0) },
// Number { value: dec!(2.0) },
// Number { value: dec!(3.0) },
// ],
// }),
// Entry::Vector(Vector {
// direction: VectorDirection::Column,
// values: vec![
// Number { value: dec!(4.0) },
// Number { value: dec!(5.0) },
// Number { value: dec!(0.0) },
// ],
// }),
// Entry::Vector(Vector {
// direction: VectorDirection::Column,
// values: vec![
// Number { value: dec!(7.0) },
// Number { value: dec!(8.0) },
// Number { value: dec!(9.0) },
// ],
// }),
// ]),
// ),
// (valid_square_matrix().inverse(), Matrix::from(
// Entry::Vector(Vector {
// direction: VectorDirection::Column,
// values: vec![
// Number{value: dec!(1.0},)
// Number{value: dec!(2.0},)
// Number{value: dec!(-3.0},)
// ]}),
// Entry::Vector (Vector{
// direction: VectorDirection::Column,
// values: vec![
// Number{value: dec!(4.0},)
// Number{value: dec!(-5.0},)
// Number{value: dec!(0.0},)
// ]}),
// Entry::Vector (Vector{
// direction: VectorDirection::Column,
// values: vec![
// Number{value: dec!(-7.0},)
// Number{value: dec!(8.0},)
// Number{value: dec!(9.0},)
// ])),
(
"transpose",
valid_square_matrix().transpose(),
Matrix::from(&[
Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(1.0) },
Number { value: dec!(4.0) },
Number { value: dec!(-7.0) },
],
}),
Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(2.0) },
Number { value: dec!(-5.0) },
Number { value: dec!(8.0) },
],
}),
Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(-3.0) },
Number { value: dec!(0.0) },
Number { value: dec!(9.0) },
],
}),
]),
),
(
"sqrt",
(Matrix {
dimensions: MatrixDimensions {
columns: 1,
rows: 3,
},
vectors: vec![Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(1.0) },
Number { value: dec!(100.0) },
Number { value: dec!(64.0) },
],
}],
})
.sqrt(),
Matrix::from(&[Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(1.0) },
Number { value: dec!(10.0) },
Number { value: dec!(8.0) },
],
})]),
),
// NEWS
(
"log",
(Matrix {
dimensions: MatrixDimensions {
columns: 1,
rows: 3,
},
vectors: vec![Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(1.0) },
Number { value: dec!(100.0) },
Number {
value: dec!(100_000.0),
},
],
}],
})
.log(),
Matrix::from(&[Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(0.0) },
Number { value: dec!(2.0) },
Number { value: dec!(5.0) },
],
})]),
),
(
"ln",
(Matrix {
dimensions: MatrixDimensions {
columns: 1,
rows: 3,
},
vectors: vec![Vector {
direction: VectorDirection::Column,
values: vec![
Number { value: dec!(1.0) },
Number {
value: constants::E,
},
],
}],
})
.ln(),
Matrix::from(&[Entry::Vector(Vector {
direction: VectorDirection::Column,
values: vec![Number { value: dec!(0.0) }, Number { value: dec!(1.0) }],
})]),
),
] {
println!("Testing: {}", op);
let left = entry.as_ref().expect("Operation failed");
let right = goal.as_ref().expect("Invalid test goal construction");
assert_eq!(left, right);
}
}
}

352
src/calc/entries/matrix.rs Normal file
View File

@ -0,0 +1,352 @@
use super::{Entry, Number, Vector, VectorDirection};
use crate::calc::{
entries::CalculatorEntry,
errors::{CalculatorError, CalculatorResult},
types::{CalculatorAngleMode, CalculatorDisplayMode},
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct MatrixDimensions {
pub rows: usize,
pub columns: usize,
}
impl MatrixDimensions {
pub const fn transpose(&self) -> Self {
Self {
rows: self.columns,
columns: self.rows,
}
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Matrix {
pub vectors: Vec<Vector>,
pub dimensions: MatrixDimensions,
}
impl Matrix {
pub fn from(entries: &[Entry]) -> CalculatorResult<Entry> {
if entries.is_empty() {
return Err(CalculatorError::NotEnoughStackEntries);
}
let vectors = entries
.iter()
.map(|e| match e {
Entry::Matrix(_) | Entry::Number(_) => Err(CalculatorError::TypeMismatch),
Entry::Vector(vector) => Ok(vector.clone()),
})
.collect::<CalculatorResult<Vec<Vector>>>()?;
// Get the num_rows and dimension of the matrix
let first_vector = vectors
.get(0)
.ok_or(CalculatorError::NotEnoughStackEntries)?;
// The number of rows in this column-based matrix
let num_rows = first_vector.values.len();
// The direction all vectors must face
let vector_direction = first_vector.direction;
// Either the dimension lengths mismatch, or the vectors are facing different directions (and are longer than 1, since a 1-length vector orientation does not matter
if vectors.iter().any(|v| v.values.len() != num_rows)
|| (num_rows > 1 && vectors.iter().any(|v| v.direction != vector_direction))
{
return Err(CalculatorError::DimensionMismatch);
}
let dimensions = MatrixDimensions {
rows: num_rows,
columns: vectors.len(),
};
let ret = Self {
vectors,
dimensions,
};
// If the user tried making a matrix out of row vectors, we need to transpose it, which forces column vectors
if vector_direction == VectorDirection::Row && num_rows > 1 {
ret.transpose()
} else {
ret.validate()
}
}
fn iterated_unary(
&self,
op: impl Fn(&Vector) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
Self {
vectors: self
.vectors
.iter()
.map(|v| op(v))
.map(|e| match e {
Ok(Entry::Vector(vector)) => Ok(vector),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Vector>>>()?,
dimensions: self.dimensions.clone(),
}
.validate()
}
fn iterated_binary_num(
&self,
number: &Number,
op: impl Fn(&Vector, &Entry) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
Self {
vectors: self
.vectors
.iter()
.map(|v| op(v, &Entry::Number(*number)))
.map(|e| match e {
Ok(Entry::Vector(vector)) => Ok(vector),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Vector>>>()?,
dimensions: self.dimensions.clone(),
}
.validate()
}
fn iterated_binary_mat(
&self,
m2: &Self,
op: impl Fn(&Vector, &Entry) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
if self.dimensions != m2.dimensions {
return Err(CalculatorError::DimensionMismatch);
}
Self {
vectors: self
.vectors
.iter()
.zip(m2.vectors.iter())
.map(|(v1, v2)| op(v1, &Entry::Vector(v2.clone())))
.map(|e| match e {
Ok(Entry::Vector(vector)) => Ok(vector),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Vector>>>()?,
dimensions: self.dimensions.clone(),
}
.validate()
}
}
impl CalculatorEntry for Matrix {
fn to_editable_string(&self) -> CalculatorResult<String> {
Err(CalculatorError::TypeMismatch)
}
fn is_valid(&self) -> bool {
// The the number of vectors is equal to the 0th dimension
self.vectors.len() == self.dimensions.columns
// The number of elements in all vectors are equal to the 1st dimension, and each is valid
&& self
.vectors
.iter()
.all(|v| v.values.len() == self.dimensions.rows && v.is_valid())
// The dimensions are not zero
&& self.dimensions.rows > 0 && self.dimensions.columns > 0
}
fn validate(self) -> CalculatorResult<Entry> {
if self.is_valid() {
Ok(Entry::Matrix(self))
} else {
Err(CalculatorError::ArithmeticError)
}
}
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
format!(
"[ {} ]",
self.vectors
.iter()
.map(|vector| vector.format_entry(display_mode))
.collect::<Vec<String>>()
.join(" ")
)
}
// Mathematical operations
fn negate(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Vector::negate)
}
fn abs(&self) -> CalculatorResult<Entry> {
// TODO: Compute determinant
Err(CalculatorError::NotYetImplemented)
}
fn inverse(&self) -> CalculatorResult<Entry> {
// TODO: Inverse
Err(CalculatorError::NotYetImplemented)
}
fn transpose(&self) -> CalculatorResult<Entry> {
// Iterate over all rows
let mut vectors: Vec<Vector> = vec![];
for r in 0..self.dimensions.rows {
vectors.push(Vector {
values: self
.vectors
.iter()
.map(|v| {
// For each row, get the r'th element to build a new vector
v.values
.get(r)
.map_or_else(|| Err(CalculatorError::DimensionMismatch), |n| Ok(*n))
})
.collect::<CalculatorResult<Vec<Number>>>()?,
direction: VectorDirection::Column,
});
}
Self {
vectors,
dimensions: self.dimensions.transpose(),
}
.validate()
}
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|v| v.sin(angle_mode))
}
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|v| v.cos(angle_mode))
}
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|v| v.tan(angle_mode))
}
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|v| v.asin(angle_mode))
}
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|v| v.acos(angle_mode))
}
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|v| v.atan(angle_mode))
}
fn sqrt(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Vector::sqrt)
}
fn log(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Vector::log)
}
fn ln(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Vector::ln)
}
// Binary
fn add(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::add),
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
Entry::Number(number) => self.iterated_binary_num(number, Vector::add),
}
}
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::sub),
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
Entry::Number(number) => self.iterated_binary_num(number, Vector::sub),
}
}
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(m2) => {
if self.dimensions.columns != m2.dimensions.rows {
return Err(CalculatorError::DimensionMismatch);
}
let dimensions = MatrixDimensions {
rows: self.dimensions.rows,
columns: m2.dimensions.columns,
};
// A matrix is a list of column vectors, so transpose self and zip the columns
let transposed_self: Self = match self.transpose()? {
Entry::Matrix(t) => t,
_ => {
return Err(CalculatorError::InternalError(String::from(
"Matrix transpose produced wrong type",
)))
}
};
let mut vectors: Vec<Vector> = vec![];
for c in &m2.vectors {
let mut vector: Vector = Vector {
values: vec![],
direction: VectorDirection::Column,
};
for r in &transposed_self.vectors {
if let Entry::Number(number) =
c.transpose()?.mul(&Entry::Vector(r.clone()))?
{
vector.values.push(number);
} else {
return Err(CalculatorError::InternalError(String::from(
"Vector multiplication did not produce a number",
)));
}
}
vectors.push(vector);
}
Self {
vectors,
dimensions,
}
.validate()
}
Entry::Vector(vector) => self.mul(&Self::from(
&[Entry::Vector(vector.clone())], // Treat a vector as a 1D matrix
)?),
Entry::Number(number) => self.iterated_binary_num(number, Vector::mul),
}
}
fn div(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::div),
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
Entry::Number(number) => self.iterated_binary_num(number, Vector::div),
}
}
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::int_divide),
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
Entry::Number(number) => self.iterated_binary_num(number, Vector::int_divide),
}
}
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(m2) => self.iterated_binary_mat(m2, Vector::modulo),
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
Entry::Number(number) => self.iterated_binary_num(number, Vector::modulo),
}
}
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_m2) => Err(CalculatorError::TypeMismatch),
Entry::Vector(_vector) => Err(CalculatorError::TypeMismatch),
Entry::Number(number) => self.iterated_binary_num(number, Vector::pow),
}
}
}
impl fmt::Display for Matrix {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"[ {} ]",
self.vectors
.iter()
.map(|vector| format!("{}", vector))
.collect::<Vec<String>>()
.join("; ")
)
}
}

384
src/calc/entries/number.rs Normal file
View File

@ -0,0 +1,384 @@
use super::{Entry, Matrix, Vector};
use crate::{
calc::{
entries::CalculatorEntry,
errors::{CalculatorError, CalculatorResult},
types::{CalculatorAngleMode, CalculatorDisplayMode},
},
constants,
};
use rust_decimal::{Decimal, MathematicalOps};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct Number {
pub value: Decimal,
}
// impl PartialEq for Number {
// fn eq(&self, other: &Self) -> bool {
// (self.value - other.value).abs() < f64::EPSILON
// }
// }
// }
impl CalculatorEntry for Number {
fn to_editable_string(&self) -> CalculatorResult<String> {
Ok(format!("{}", self.value))
}
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
match display_mode {
CalculatorDisplayMode::Default => format!("{}", self.value),
CalculatorDisplayMode::Separated { separator } => separated(self.value, *separator),
CalculatorDisplayMode::Scientific { precision } => scientific(self.value, *precision),
CalculatorDisplayMode::Engineering { precision } => engineering(self.value, *precision),
CalculatorDisplayMode::Fixed { precision } => {
format!("{:0>.precision$}", self.value, precision = precision)
}
}
}
fn is_valid(&self) -> bool {
true
}
fn validate(self) -> CalculatorResult<Entry> {
if self.is_valid() {
Ok(Entry::Number(self))
} else {
Err(CalculatorError::ArithmeticError)
}
}
fn negate(&self) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self { value: -self.value }))
}
fn abs(&self) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self {
value: self.value.abs(),
}))
}
fn inverse(&self) -> CalculatorResult<Entry> {
Self {
value: constants::ONE
.checked_div(self.value)
.ok_or(CalculatorError::ArithmeticError)?,
}
.validate()
}
fn transpose(&self) -> CalculatorResult<Entry> {
Err(CalculatorError::TypeMismatch)
}
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self {
value: match angle_mode {
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
.checked_sin()
.ok_or(CalculatorError::ArithmeticError)?,
CalculatorAngleMode::Radians => self
.value
.checked_sin()
.ok_or(CalculatorError::ArithmeticError)?,
CalculatorAngleMode::Grads => (self.value * constants::GRAD_TO_RAD_MULTIPLIER)
.checked_sin()
.ok_or(CalculatorError::ArithmeticError)?,
},
}))
}
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self {
value: match angle_mode {
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
.checked_cos()
.ok_or(CalculatorError::ArithmeticError)?,
CalculatorAngleMode::Radians => self
.value
.checked_cos()
.ok_or(CalculatorError::ArithmeticError)?,
CalculatorAngleMode::Grads => (self.value * constants::GRAD_TO_RAD_MULTIPLIER)
.checked_cos()
.ok_or(CalculatorError::ArithmeticError)?,
},
}))
}
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self {
value: match angle_mode {
CalculatorAngleMode::Degrees => (self.value * constants::DEC_TO_RAD_MULTIPLIER)
.checked_tan()
.ok_or(CalculatorError::ArithmeticError)?,
CalculatorAngleMode::Radians => self
.value
.checked_tan()
.ok_or(CalculatorError::ArithmeticError)?,
CalculatorAngleMode::Grads => (self.value * constants::GRAD_TO_RAD_MULTIPLIER)
.checked_tan()
.ok_or(CalculatorError::ArithmeticError)?,
},
}))
}
fn asin(&self, _angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
// TODO: Implement this
Err(CalculatorError::NotYetImplemented)
// Ok(Entry::Number(Self {
// value: match angle_mode {
// CalculatorAngleMode::Degrees => self.value.asin().to_degrees(),
// CalculatorAngleMode::Radians => self.value.asin(),
// CalculatorAngleMode::Grads => self.value.asin() * 200.0 / std::f64::consts::PI,
// },
// }))
}
fn acos(&self, _angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
// TODO: Implement this
Err(CalculatorError::NotYetImplemented)
// Ok(Entry::Number(Self {
// value: match angle_mode {
// CalculatorAngleMode::Degrees => self.value.acos().to_degrees(),
// CalculatorAngleMode::Radians => self.value.acos(),
// CalculatorAngleMode::Grads => self.value.acos() * 200.0 / std::f64::consts::PI,
// },
// }))
}
fn atan(&self, _angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
// TODO: Implement this
Err(CalculatorError::NotYetImplemented)
// Ok(Entry::Number(Self {
// value: match angle_mode {
// CalculatorAngleMode::Degrees => self.value.atan().to_degrees(),
// CalculatorAngleMode::Radians => self.value.atan(),
// CalculatorAngleMode::Grads => self.value.atan() * 200.0 / std::f64::consts::PI,
// },
// }))
}
fn sqrt(&self) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self {
value: self.value.sqrt().ok_or(CalculatorError::ArithmeticError)?,
}))
}
fn log(&self) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self {
value: self
.value
.checked_log10()
.ok_or(CalculatorError::ArithmeticError)?,
}))
}
fn ln(&self) -> CalculatorResult<Entry> {
Ok(Entry::Number(Self {
value: self
.value
.checked_ln()
.ok_or(CalculatorError::ArithmeticError)?,
}))
}
fn add(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::add),
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::add),
Entry::Number(number) => Self {
value: self.value + number.value,
}
.validate(),
}
}
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::sub),
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::sub),
Entry::Number(number) => Self {
value: self.value - number.value,
}
.validate(),
}
}
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::mul),
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::mul),
Entry::Number(number) => Self {
value: self.value * number.value,
}
.validate(),
}
}
fn div(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::div),
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::div),
Entry::Number(number) => Self {
value: self.value / number.value,
}
.validate(),
}
}
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::int_divide),
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::int_divide),
Entry::Number(number) => {
let q = (self.value / number.value).trunc();
Self {
// Implementation based on https://doc.rust-lang.org/src/std/f64.rs.html#263-269
value: if self.value % number.value < constants::ZERO {
if number.value > constants::ZERO {
q - constants::ONE
} else {
q + constants::ONE
}
} else {
q
},
}
}
.validate(),
}
}
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::modulo),
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::modulo),
Entry::Number(number) => Self {
value: self.value % number.value,
}
.validate(),
}
}
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(matrix) => self.iterated_binary_mat(matrix, Self::pow),
Entry::Vector(vector) => self.iterated_binary_vec(vector, Self::pow),
Entry::Number(number) => Self {
value: self
.value
.checked_powd(number.value)
.ok_or(CalculatorError::ArithmeticError)?,
}
.validate(),
}
}
}
impl Number {
pub const ZERO: Self = Self {
value: constants::ZERO,
};
fn iterated_binary_vec(
self,
vector: &Vector,
op: impl Fn(&Self, &Entry) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
Ok(Entry::Vector(Vector {
values: vector
.values
.iter()
.map(|n| op(&self, &Entry::Number(*n)))
.map(|e| match e {
// Only numbers are valid in a vector
Ok(Entry::Number(number)) => Ok(number),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Self>>>()?,
direction: vector.direction,
}))
}
fn iterated_binary_mat(
self,
matrix: &Matrix,
op: impl Fn(&Self, &Entry) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
Matrix {
vectors: matrix
.vectors
.iter()
.map(|v| op(&self, &Entry::Vector(v.clone())))
.map(|e| match e {
// Only numbers are valid in a vector
Ok(Entry::Vector(vector)) => Ok(vector),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Vector>>>()?,
dimensions: matrix.dimensions.clone(),
}
.validate()
}
}
impl fmt::Display for Number {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.value)
}
}
// Based on https://stackoverflow.com/a/65266882
fn scientific(f: Decimal, precision: usize) -> String {
let mut ret = format!("{:.precision$E}", f, precision = precision);
let exp = ret.split_off(ret.find('E').unwrap_or(0));
let (exp_sign, exp) = exp
.strip_prefix("E-")
.map_or_else(|| ('+', &exp[1..]), |stripped| ('-', stripped));
let sign = if ret.starts_with('-') { "" } else { " " };
format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2)
}
fn engineering(f: Decimal, precision: usize) -> String {
// Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string
// 1,000 => 1000E3
let all = format!(" {:.precision$E}", f, precision = precision)
// Remove . since it can be moved
.replacen(".", "", 1)
// Add 00E before E here so the length is enough for slicing below
.replacen("E", "00E", 1);
// Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E
// 1000E3 => (1000, E3)
let (num_str, exp_str) = all.split_at(all.find('E').unwrap());
// Extract the exponent as an isize. This should always be true because Entry max will be ~400
// E3 => 3 as isize
let exp = exp_str[1..].parse::<isize>().unwrap();
// Sign of the exponent. If string representation starts with E-, then negative
let display_exp_sign = if exp_str.strip_prefix("E-").is_some() {
'-'
} else {
'+'
};
// The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above
// 100 => 0, 1000 => 3, .1 => 3 (but will show as -3)
let display_exp = (exp.div_euclid(3) * 3).abs();
// Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility
let num_whole_digits = exp.rem_euclid(3) as usize + 1;
// If this is a negative number, strip off the added space, otherwise keep the space (and next digit)
let num_str = if num_str.strip_prefix(" -").is_some() {
&num_str[1..]
} else {
num_str
};
// Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit)
// Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10
let whole = &num_str[0..=num_whole_digits];
// Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2
let decimal = &num_str[(num_whole_digits + 1)..=(precision + num_whole_digits)];
// Right align whole portion, always have decimal point
format!(
"{: >4}.{} E{}{:0>pad$}",
// display_sign,
whole,
decimal,
display_exp_sign,
display_exp,
pad = 2
)
}
fn separated(f: Decimal, sep: char) -> String {
let mut ret = f.to_string();
let start = if ret.starts_with('-') { 1 } else { 0 };
let end = ret.find('.').unwrap_or_else(|| ret.len());
for i in 0..((end - start - 1).div_euclid(3)) {
ret.insert(end - (i + 1) * 3, sep);
}
ret
}

296
src/calc/entries/vector.rs Normal file
View File

@ -0,0 +1,296 @@
use super::{Entry, Matrix, Number};
use crate::{
calc::{
entries::CalculatorEntry,
errors::{CalculatorError, CalculatorResult},
types::{CalculatorAngleMode, CalculatorDisplayMode},
},
constants,
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(PartialEq, Copy, Clone, Debug, Serialize, Deserialize)]
pub enum VectorDirection {
Row,
Column,
}
impl VectorDirection {
pub const fn swap(&self) -> Self {
match self {
Self::Row => Self::Column,
Self::Column => Self::Row,
}
}
pub const fn get_separator(&self) -> &str {
match self {
Self::Row => " ",
Self::Column => "; ",
}
}
}
impl Default for VectorDirection {
fn default() -> Self {
// Column vectors are the default
Self::Column
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Vector {
pub values: Vec<Number>,
pub direction: VectorDirection,
}
impl CalculatorEntry for Vector {
// Misc
fn to_editable_string(&self) -> CalculatorResult<String> {
// TODO: Eventualy we can parse and edit a vector as a string
Err(CalculatorError::TypeMismatch)
}
fn is_valid(&self) -> bool {
self.values.iter().all(|number| number.is_valid())
}
fn validate(self) -> CalculatorResult<Entry> {
if self.is_valid() {
Ok(Entry::Vector(self))
} else {
Err(CalculatorError::ArithmeticError)
}
}
fn format_entry(&self, display_mode: &CalculatorDisplayMode) -> String {
format!(
"[{}]",
self.values
.iter()
.map(|number| number.format_entry(display_mode))
.collect::<Vec<String>>()
.join(self.direction.get_separator())
)
}
// Mathematical operations
fn negate(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Number::negate)
}
fn abs(&self) -> CalculatorResult<Entry> {
let value: Entry =
self.values
.iter()
.try_fold(Entry::Number(Number::ZERO), |acc, n2| {
acc.add(&n2.pow(&Entry::Number(Number {
value: constants::TWO,
}))?)
})?;
value.sqrt()
}
fn inverse(&self) -> CalculatorResult<Entry> {
Err(CalculatorError::TypeMismatch)
}
fn transpose(&self) -> CalculatorResult<Entry> {
Ok(Entry::Vector(Self {
values: self.values.clone(),
direction: self.direction.swap(),
}))
}
fn sin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|n| n.sin(angle_mode))
}
fn cos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|n| n.cos(angle_mode))
}
fn tan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|n| n.tan(angle_mode))
}
fn asin(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|n| n.asin(angle_mode))
}
fn acos(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|n| n.acos(angle_mode))
}
fn atan(&self, angle_mode: CalculatorAngleMode) -> CalculatorResult<Entry> {
self.iterated_unary(|n| n.atan(angle_mode))
}
fn sqrt(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Number::sqrt)
}
fn log(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Number::log)
}
fn ln(&self) -> CalculatorResult<Entry> {
self.iterated_unary(Number::ln)
}
fn add(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::add),
Entry::Number(number) => self.iterated_binary_num(number, Number::add),
}
}
fn sub(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::sub),
Entry::Number(number) => self.iterated_binary_num(number, Number::sub),
}
}
fn mul(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_matrix) => Matrix::from(&[Entry::Vector(self.clone())])?.mul(arg),
Entry::Vector(v2) => {
if self.values.len() != v2.values.len() {
return Err(CalculatorError::DimensionMismatch);
}
match (self.direction, v2.direction) {
(VectorDirection::Row, VectorDirection::Column) => {
// Row by column -- will produce a scalar
self.values
.iter()
.zip(v2.values.iter())
.try_fold(Entry::Number(Number::ZERO), |acc, (n1, n2)| {
acc.add(&n1.mul(&Entry::Number(*n2))?)
})
}
(VectorDirection::Column, VectorDirection::Row) => {
Matrix::from(&[Entry::Vector(self.clone())])?
.mul(&Matrix::from(&[arg.clone()])?)
}
(VectorDirection::Row, VectorDirection::Row)
| (VectorDirection::Column, VectorDirection::Column) => {
Err(CalculatorError::DimensionMismatch)
}
}
}
Entry::Number(number) => self.iterated_binary_num(number, Number::mul),
}
}
fn div(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::div),
Entry::Number(number) => self.iterated_binary_num(number, Number::div),
}
}
fn int_divide(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::int_divide),
Entry::Number(number) => self.iterated_binary_num(number, Number::int_divide),
}
}
fn modulo(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::modulo),
Entry::Number(number) => self.iterated_binary_num(number, Number::modulo),
}
}
fn pow(&self, arg: &Entry) -> CalculatorResult<Entry> {
match arg {
Entry::Matrix(_) => Err(CalculatorError::TypeMismatch),
Entry::Vector(v2) => self.iterated_binary_vec(v2, Number::pow),
Entry::Number(number) => self.iterated_binary_num(number, Number::pow),
}
}
}
impl Vector {
pub fn from(entries: &[Entry]) -> CalculatorResult<Entry> {
if entries.is_empty() {
return Err(CalculatorError::NotEnoughStackEntries);
}
Self {
values: entries
.iter()
.map(|e| match e {
Entry::Matrix(_) | Entry::Vector(_) => Err(CalculatorError::TypeMismatch),
Entry::Number(number) => Ok(*number),
})
.collect::<CalculatorResult<Vec<Number>>>()?,
direction: VectorDirection::default(),
}
.validate()
}
fn iterated_unary(
&self,
op: impl Fn(&Number) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
Ok(Entry::Vector(Self {
values: self
.values
.iter()
.map(|n| op(n))
.map(|e| match e {
// Only numbers are valid in a vector
Ok(Entry::Number(number)) => Ok(number),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Number>>>()?,
direction: self.direction,
}))
}
fn iterated_binary_vec(
&self,
v2: &Self,
op: impl Fn(&Number, &Entry) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
if self.values.len() != v2.values.len() {
return Err(CalculatorError::DimensionMismatch);
}
if self.direction != v2.direction {
return Err(CalculatorError::DimensionMismatch);
}
Ok(Entry::Vector(Self {
values: self
.values
.iter()
.zip(v2.values.iter())
.map(|(n1, n2)| op(n1, &Entry::Number(*n2)))
.map(|e| match e {
// Only numbers are valid in a vector
Ok(Entry::Number(number)) => Ok(number),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Number>>>()?,
direction: self.direction,
}))
}
fn iterated_binary_num(
&self,
n2: &Number,
op: impl Fn(&Number, &Entry) -> CalculatorResult<Entry>,
) -> CalculatorResult<Entry> {
Ok(Entry::Vector(Self {
values: self
.values
.iter()
.map(|n| op(n, &Entry::Number(*n2)))
.map(|e| match e {
// Only numbers are valid in a vector
Ok(Entry::Number(number)) => Ok(number),
_ => Err(CalculatorError::ArithmeticError),
})
.collect::<CalculatorResult<Vec<Number>>>()?,
direction: self.direction,
}))
}
}
impl fmt::Display for Vector {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"[{}]",
self.values
.iter()
.map(|number| format!("{}", number))
.collect::<Vec<String>>()
.join("; ")
)
}
}

View File

@ -1,6 +1,5 @@
use confy::ConfyError;
use std::error;
use std::fmt;
use std::{error, fmt};
pub type CalculatorResult<T> = Result<T, CalculatorError>;
@ -11,6 +10,14 @@ pub enum CalculatorError {
ArithmeticError,
/// Not enough stck entries for operation
NotEnoughStackEntries,
/// Requested type does not match target type
TypeMismatch,
/// Dimensions must match
DimensionMismatch,
/// Plans are in place to implement, but we are not there yet
NotYetImplemented,
/// Internal computation error
InternalError(String),
/// Thrown when an undo or redo cannot be performed
CorruptStateChange(String),
/// Cannot undo or redo
@ -44,6 +51,12 @@ impl fmt::Display for CalculatorError {
match self {
Self::ArithmeticError => write!(f, "Arithmetic Error"),
Self::NotEnoughStackEntries => write!(f, "Not enough items in the stack"),
Self::TypeMismatch => write!(f, "Type mismatch"),
Self::DimensionMismatch => write!(f, "Dimension mismatch"),
Self::NotYetImplemented => write!(f, "Not yet implemented"),
Self::InternalError(msg) => {
write!(f, "Internal error: {}", msg)
}
Self::CorruptStateChange(msg) => {
write!(f, "Corrupt state change: {}", msg)
}
@ -57,12 +70,12 @@ impl fmt::Display for CalculatorError {
Self::ParseError => write!(f, "Parse error"),
Self::PrecisionTooHigh => write!(f, "Precision too high"),
Self::SaveError(None) => write!(f, "Could not save"),
Self::SaveError(Some(ConfyError::SerializeTomlError(e))) => {
Self::SaveError(Some(ConfyError::SerializeYamlError(e))) => {
write!(f, "Save serialization error: {}", e)
}
Self::SaveError(Some(e)) => write!(f, "Could not save: {}", e),
Self::LoadError(None) => write!(f, "Could not load"),
Self::LoadError(Some(ConfyError::SerializeTomlError(e))) => {
Self::LoadError(Some(ConfyError::SerializeYamlError(e))) => {
write!(f, "Load serialization error: {}", e)
}
Self::LoadError(Some(e)) => write!(f, "Could not load: {}", e),

View File

@ -1,7 +1,8 @@
use super::entries::Entry;
use serde::{Deserialize, Serialize};
/// Operations that can be sent to the calculator such as +, -, or undo
#[derive(PartialEq, Debug, Serialize, Deserialize)]
pub enum CalculatorOperation {
#[derive(PartialEq, Debug, Serialize, Deserialize, Copy, Clone)]
pub enum ArithmeticOperation {
Add,
Subtract,
Multiply,
@ -11,10 +12,6 @@ pub enum CalculatorOperation {
Inverse,
Modulo,
IntegerDivide,
//Remainder,
Drop,
Dup,
Swap,
Sin,
Cos,
Tan,
@ -22,18 +19,28 @@ pub enum CalculatorOperation {
ACos,
ATan,
Sqrt,
Undo,
Redo,
Pow,
// Factorial,
Log,
Ln,
E,
}
/// Operations that can be sent to the calculator such as +, -, or undo
#[derive(PartialEq, Debug, Serialize, Deserialize, Copy, Clone)]
pub enum CalculatorOperation {
ArithmeticOperation(ArithmeticOperation),
BuildVector,
BuildMatrix,
Deconstruct,
Transpose,
Undo,
Redo,
Drop,
Dup,
Swap,
Macro(MacroState),
}
/// Macro bundary; defined by the start or end of a macro invocation
#[derive(PartialEq, Debug, Serialize, Deserialize)]
#[derive(PartialEq, Debug, Serialize, Deserialize, Copy, Clone)]
pub enum MacroState {
Start,
End,
@ -45,9 +52,11 @@ pub enum OpArgs {
/// This is a macro start and end noop
Macro(MacroState),
/// Operation takes 1 argument, ex: sqrt or negate
Unary(f64),
Unary(Entry),
/// Operation takes 2 arguments, ex: + or -
Binary([f64; 2]),
Binary([Entry; 2]),
/// Some variable number of changes
Variable(Vec<Entry>),
/// Operation takes no arguments, ex: push
None,
}

View File

@ -1,6 +1,6 @@
use super::entries::Entry;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::{collections::HashMap, fmt};
/// The calculator state
#[derive(Debug, Serialize, Deserialize)]
@ -29,7 +29,7 @@ pub enum RegisterState {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CalculatorConstant {
pub help: String,
pub value: f64,
pub value: Entry,
}
/// One calculator macro containing a messsage and value
@ -46,10 +46,10 @@ pub type CalculatorConstants = HashMap<char, CalculatorConstant>;
pub type CalculatorMacros = HashMap<char, CalculatorMacro>;
/// Map of chars to registers
pub type CalculatorRegisters = HashMap<char, f64>;
pub type CalculatorRegisters = HashMap<char, Entry>;
/// Possible calculator angle modes
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "angle_mode")]
pub enum CalculatorAngleMode {
Degrees,
@ -77,7 +77,7 @@ impl fmt::Display for CalculatorAngleMode {
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(tag = "display_mode")]
pub enum CalculatorDisplayMode {
/// Rust's default f64 format
/// Rust's default Entry format
Default,
/// Thousands separator
Separated { separator: char },
@ -109,92 +109,7 @@ impl Default for CalculatorDisplayMode {
}
}
impl CalculatorDisplayMode {
pub fn format_number(&self, number: f64) -> String {
match self {
Self::Default => format!("{}", number),
Self::Separated { separator } => Self::separated(number, *separator),
Self::Scientific { precision } => Self::scientific(number, *precision),
Self::Engineering { precision } => Self::engineering(number, *precision),
Self::Fixed { precision } => {
format!("{:0>.precision$}", number, precision = precision)
}
}
}
// Based on https://stackoverflow.com/a/65266882
fn scientific(f: f64, precision: usize) -> String {
let mut ret = format!("{:.precision$E}", f, precision = precision);
let exp = ret.split_off(ret.find('E').unwrap_or(0));
let (exp_sign, exp) = exp
.strip_prefix("E-")
.map_or_else(|| ('+', &exp[1..]), |stripped| ('-', stripped));
let sign = if ret.starts_with('-') { "" } else { " " };
format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2)
}
fn engineering(f: f64, precision: usize) -> String {
// Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string
// 1,000 => 1000E3
let all = format!(" {:.precision$E}", f, precision = precision)
// Remove . since it can be moved
.replacen(".", "", 1)
// Add 00E before E here so the length is enough for slicing below
.replacen("E", "00E", 1);
// Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E
// 1000E3 => (1000, E3)
let (num_str, exp_str) = all.split_at(all.find('E').unwrap());
// Extract the exponent as an isize. This should always be true because f64 max will be ~400
// E3 => 3 as isize
let exp = exp_str[1..].parse::<isize>().unwrap();
// Sign of the exponent. If string representation starts with E-, then negative
let display_exp_sign = if exp_str.strip_prefix("E-").is_some() {
'-'
} else {
'+'
};
// The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above
// 100 => 0, 1000 => 3, .1 => 3 (but will show as -3)
let display_exp = (exp.div_euclid(3) * 3).abs();
// Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility
let num_whole_digits = exp.rem_euclid(3) as usize + 1;
// If this is a negative number, strip off the added space, otherwise keep the space (and next digit)
let num_str = if num_str.strip_prefix(" -").is_some() {
&num_str[1..]
} else {
num_str
};
// Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit)
// Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10
let whole = &num_str[0..=num_whole_digits];
// Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2
let decimal = &num_str[(num_whole_digits + 1)..=(precision + num_whole_digits)];
// Right align whole portion, always have decimal point
format!(
"{: >4}.{} E{}{:0>pad$}",
// display_sign,
whole,
decimal,
display_exp_sign,
display_exp,
pad = 2
)
}
fn separated(f: f64, sep: char) -> String {
let mut ret = f.to_string();
let start = if ret.starts_with('-') { 1 } else { 0 };
let end = ret.find('.').unwrap_or_else(|| ret.len());
for i in 0..((end - start - 1).div_euclid(3)) {
ret.insert(end - (i + 1) * 3, sep);
}
ret
}
}
impl CalculatorDisplayMode {}
/// Left or right calculator alignment
#[derive(Clone, Debug, Serialize, Deserialize)]
@ -218,92 +133,92 @@ impl fmt::Display for CalculatorAlignment {
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scientific() {
for (f, precision, s) in vec![
// Basic
(1.0, 0, " 1 E+00"),
(-1.0, 0, "-1 E+00"),
(100.0, 0, " 1 E+02"),
(0.1, 0, " 1 E-01"),
(0.01, 0, " 1 E-02"),
(-0.1, 0, "-1 E-01"),
// i
(1.0, 0, " 1 E+00"),
// Precision
(-0.123456789, 3, "-1.235 E-01"),
(-0.123456789, 2, "-1.23 E-01"),
(-0.123456789, 2, "-1.23 E-01"),
(-1e99, 2, "-1.00 E+99"),
(-1e100, 2, "-1.00 E+100"),
// Rounding
(0.5, 2, " 5.00 E-01"),
(0.5, 1, " 5.0 E-01"),
(0.5, 0, " 5 E-01"),
(1.5, 2, " 1.50 E+00"),
(1.5, 1, " 1.5 E+00"),
(1.5, 0, " 2 E+00"),
] {
assert_eq!(
CalculatorDisplayMode::Scientific { precision }.format_number(f),
s
);
}
}
// #[cfg(test)]
// mod tests {
// use super::*;
// #[test]
// fn test_scientific() {
// for (f, precision, s) in vec![
// // Basic
// (1.0, 0, " 1 E+00"),
// (-1.0, 0, "-1 E+00"),
// (100.0, 0, " 1 E+02"),
// (0.1, 0, " 1 E-01"),
// (0.01, 0, " 1 E-02"),
// (-0.1, 0, "-1 E-01"),
// // i
// (1.0, 0, " 1 E+00"),
// // Precision
// (-0.123_456_789, 3, "-1.235 E-01"),
// (-0.123_456_789, 2, "-1.23 E-01"),
// (-0.123_456_789, 2, "-1.23 E-01"),
// (-1e99, 2, "-1.00 E+99"),
// (-1e100, 2, "-1.00 E+100"),
// // Rounding
// (0.5, 2, " 5.00 E-01"),
// (0.5, 1, " 5.0 E-01"),
// (0.5, 0, " 5 E-01"),
// (1.5, 2, " 1.50 E+00"),
// (1.5, 1, " 1.5 E+00"),
// (1.5, 0, " 2 E+00"),
// ] {
// assert_eq!(
// CalculatorDisplayMode::Scientific { precision }.format_number(f),
// s
// );
// }
// }
#[test]
fn test_separated() {
for (f, separator, s) in vec![
(100.0, ',', "100"),
(100.0, ',', "100"),
(-100.0, ',', "-100"),
(1_000.0, ',', "1,000"),
(-1_000.0, ',', "-1,000"),
(10_000.0, ',', "10,000"),
(-10_000.0, ',', "-10,000"),
(100_000.0, ',', "100,000"),
(-100_000.0, ',', "-100,000"),
(1_000_000.0, ',', "1,000,000"),
(-1_000_000.0, ',', "-1,000,000"),
(1_000_000.123456789, ',', "1,000,000.123456789"),
(-1_000_000.123456789, ',', "-1,000,000.123456789"),
(1_000_000.123456789, ' ', "1 000 000.123456789"),
(1_000_000.123456789, ' ', "1 000 000.123456789"),
] {
assert_eq!(
CalculatorDisplayMode::Separated { separator }.format_number(f),
s
);
}
}
// #[test]
// fn test_separated() {
// for (f, separator, s) in vec![
// (100.0, ',', "100"),
// (100.0, ',', "100"),
// (-100.0, ',', "-100"),
// (1_000.0, ',', "1,000"),
// (-1_000.0, ',', "-1,000"),
// (10_000.0, ',', "10,000"),
// (-10_000.0, ',', "-10,000"),
// (100_000.0, ',', "100,000"),
// (-100_000.0, ',', "-100,000"),
// (1_000_000.0, ',', "1,000,000"),
// (-1_000_000.0, ',', "-1,000,000"),
// (1_000_000.123_456_789, ',', "1,000,000.123456789"),
// (-1_000_000.123_456_789, ',', "-1,000,000.123456789"),
// (1_000_000.123_456_789, ' ', "1 000 000.123456789"),
// (1_000_000.123_456_789, ' ', "1 000 000.123456789"),
// ] {
// assert_eq!(
// CalculatorDisplayMode::Separated { separator }.format_number(f),
// s
// );
// }
// }
#[test]
fn test_engineering() {
for (f, precision, s) in vec![
(100.0, 3, " 100.000 E+00"),
(100.0, 3, " 100.000 E+00"),
(-100.0, 3, "-100.000 E+00"),
(100.0, 0, " 100. E+00"),
(-100.0, 0, "-100. E+00"),
(0.1, 2, " 100.00 E-03"),
(0.01, 2, " 10.00 E-03"),
(0.001, 2, " 1.00 E-03"),
(0.0001, 2, " 100.00 E-06"),
// Rounding
(0.5, 2, " 500.00 E-03"),
(0.5, 1, " 500.0 E-03"),
(0.5, 0, " 500. E-03"),
(1.5, 2, " 1.50 E+00"),
(1.5, 1, " 1.5 E+00"),
(1.5, 0, " 2. E+00"),
] {
assert_eq!(
CalculatorDisplayMode::Engineering { precision }.format_number(f),
s
);
}
}
}
// #[test]
// fn test_engineering() {
// for (f, precision, s) in vec![
// (100.0, 3, " 100.000 E+00"),
// (100.0, 3, " 100.000 E+00"),
// (-100.0, 3, "-100.000 E+00"),
// (100.0, 0, " 100. E+00"),
// (-100.0, 0, "-100. E+00"),
// (0.1, 2, " 100.00 E-03"),
// (0.01, 2, " 10.00 E-03"),
// (0.001, 2, " 1.00 E-03"),
// (0.0001, 2, " 100.00 E-06"),
// // Rounding
// (0.5, 2, " 500.00 E-03"),
// (0.5, 1, " 500.0 E-03"),
// (0.5, 0, " 500. E-03"),
// (1.5, 2, " 1.50 E+00"),
// (1.5, 1, " 1.5 E+00"),
// (1.5, 0, " 2. E+00"),
// ] {
// assert_eq!(
// CalculatorDisplayMode::Engineering { precision }.format_number(f),
// s
// );
// }
// }
// }

26
src/constants.rs Normal file
View File

@ -0,0 +1,26 @@
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
pub const ZERO: Decimal = Decimal::ZERO;
pub const ONE: Decimal = Decimal::ONE;
pub const TWO: Decimal = Decimal::TWO;
pub const PI: Decimal = Decimal::PI;
pub const E: Decimal = Decimal::E;
pub const TAU: Decimal = Decimal::TWO_PI;
pub const RAD_TO_DEC_MULTIPLIER: Decimal = dec!(57.295779513082320876798154814);
pub const DEC_TO_RAD_MULTIPLIER: Decimal = dec!(0.0174532925199432957692369077);
pub const GRAD_TO_RAD_MULTIPLIER: Decimal = dec!(0.0157079632679489661923132169);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_constants() {
assert_eq!(RAD_TO_DEC_MULTIPLIER, dec!(180.0) / PI);
assert_eq!(DEC_TO_RAD_MULTIPLIER, PI / dec!(180.0));
assert_eq!(GRAD_TO_RAD_MULTIPLIER, PI / dec!(200.0));
}
}

2
src/lib.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod calc;
pub mod constants;