Compare commits

..

7 Commits

15 changed files with 1200 additions and 567 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
/Cargo.lock
perf.data
flamegraph.svg
result

340
Cargo.lock generated
View File

@ -128,6 +128,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "as-raw-xcb-connection"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b"
[[package]]
name = "atomic-waker"
version = "1.1.2"
@ -190,6 +196,21 @@ dependencies = [
"serde",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
]
[[package]]
name = "bit-vec"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "1.3.2"
@ -291,6 +312,18 @@ dependencies = [
"tracing",
]
[[package]]
name = "calloop-wayland-source"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20"
dependencies = [
"calloop 0.13.0",
"rustix 0.38.44",
"wayland-backend",
"wayland-client",
]
[[package]]
name = "calloop-wayland-source"
version = "0.4.1"
@ -369,7 +402,7 @@ checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
dependencies = [
"bitflags 1.3.2",
"textwrap",
"unicode-width",
"unicode-width 0.1.14",
]
[[package]]
@ -381,6 +414,15 @@ dependencies = [
"error-code",
]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
"unicode-width 0.2.2",
]
[[package]]
name = "combine"
version = "4.6.7"
@ -714,6 +756,7 @@ dependencies = [
"bytemuck",
"document-features",
"egui",
"egui-wgpu",
"egui-winit",
"egui_glow",
"glow",
@ -756,6 +799,25 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "egui-wgpu"
version = "0.33.2"
source = "git+https://github.com/titaniumtown/egui.git#63106bc9faab805197ba88820d6f11bc8c5c4657"
dependencies = [
"ahash",
"bytemuck",
"document-features",
"egui",
"epaint",
"log",
"profiling",
"thiserror 2.0.17",
"type-map",
"web-time",
"wgpu",
"winit",
]
[[package]]
name = "egui-winit"
version = "0.33.2"
@ -789,6 +851,7 @@ dependencies = [
"profiling",
"wasm-bindgen",
"web-sys",
"winit",
]
[[package]]
@ -941,6 +1004,12 @@ dependencies = [
"miniz_oxide",
]
[[package]]
name = "foldhash"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "foreign-types"
version = "0.5.0"
@ -1085,6 +1154,7 @@ dependencies = [
"cgl",
"dispatch2",
"glutin_egl_sys",
"glutin_glx_sys",
"glutin_wgl_sys",
"libloading",
"objc2 0.6.3",
@ -1093,7 +1163,9 @@ dependencies = [
"objc2-foundation 0.3.2",
"once_cell",
"raw-window-handle",
"wayland-sys",
"windows-sys 0.52.0",
"x11-dl",
]
[[package]]
@ -1118,6 +1190,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "glutin_glx_sys"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185"
dependencies = [
"gl_generator",
"x11-dl",
]
[[package]]
name = "glutin_wgl_sys"
version = "0.6.1"
@ -1141,6 +1223,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
dependencies = [
"cfg-if",
"crunchy",
"num-traits",
"zerocopy",
]
@ -1149,6 +1232,9 @@ name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"foldhash",
]
[[package]]
name = "hermit-abi"
@ -1165,6 +1251,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hexf-parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
@ -1485,6 +1577,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "libm"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
[[package]]
name = "libredox"
version = "0.1.10"
@ -1609,6 +1707,31 @@ dependencies = [
"pxfm",
]
[[package]]
name = "naga"
version = "27.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8"
dependencies = [
"arrayvec",
"bit-set",
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"codespan-reporting",
"half 2.7.1",
"hashbrown",
"hexf-parse",
"indexmap",
"libm",
"log",
"num-traits",
"once_cell",
"rustc-hash 1.1.0",
"thiserror 2.0.17",
"unicode-ident",
]
[[package]]
name = "ndk"
version = "0.9.0"
@ -1746,6 +1869,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@ -2286,6 +2410,21 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"portable-atomic",
]
[[package]]
name = "potential_utf"
version = "0.1.4"
@ -2500,6 +2639,12 @@ version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "rgb"
version = "0.8.52"
@ -2524,6 +2669,18 @@ version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustc-hash"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustix"
version = "0.38.44"
@ -2718,6 +2875,31 @@ version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "smithay-client-toolkit"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016"
dependencies = [
"bitflags 2.10.0",
"calloop 0.13.0",
"calloop-wayland-source 0.3.0",
"cursor-icon",
"libc",
"log",
"memmap2 0.9.9",
"rustix 0.38.44",
"thiserror 1.0.69",
"wayland-backend",
"wayland-client",
"wayland-csd-frame",
"wayland-cursor",
"wayland-protocols",
"wayland-protocols-wlr",
"wayland-scanner",
"xkeysym",
]
[[package]]
name = "smithay-client-toolkit"
version = "0.20.0"
@ -2726,7 +2908,7 @@ checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0"
dependencies = [
"bitflags 2.10.0",
"calloop 0.14.3",
"calloop-wayland-source",
"calloop-wayland-source 0.4.1",
"cursor-icon",
"libc",
"log",
@ -2752,7 +2934,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226"
dependencies = [
"libc",
"smithay-client-toolkit",
"smithay-client-toolkit 0.20.0",
"wayland-backend",
]
@ -2856,7 +3038,7 @@ version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
dependencies = [
"unicode-width",
"unicode-width 0.1.14",
]
[[package]]
@ -3057,6 +3239,15 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "type-map"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90"
dependencies = [
"rustc-hash 2.1.1",
]
[[package]]
name = "typenum"
version = "1.19.0"
@ -3087,6 +3278,12 @@ version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "unicode-width"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "unicode-xid"
version = "0.2.6"
@ -3318,6 +3515,19 @@ dependencies = [
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-plasma"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07a14257c077ab3279987c4f8bb987851bf57081b93710381daea94f2c2c032"
dependencies = [
"bitflags 2.10.0",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-scanner",
]
[[package]]
name = "wayland-protocols-wlr"
version = "0.3.9"
@ -3396,6 +3606,102 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "wgpu"
version = "27.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77"
dependencies = [
"arrayvec",
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"document-features",
"hashbrown",
"log",
"portable-atomic",
"profiling",
"raw-window-handle",
"smallvec",
"static_assertions",
"wgpu-core",
"wgpu-hal",
"wgpu-types",
]
[[package]]
name = "wgpu-core"
version = "27.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7"
dependencies = [
"arrayvec",
"bit-set",
"bit-vec",
"bitflags 2.10.0",
"bytemuck",
"cfg_aliases",
"document-features",
"hashbrown",
"indexmap",
"log",
"naga",
"once_cell",
"parking_lot",
"portable-atomic",
"profiling",
"raw-window-handle",
"rustc-hash 1.1.0",
"smallvec",
"thiserror 2.0.17",
"wgpu-core-deps-windows-linux-android",
"wgpu-hal",
"wgpu-types",
]
[[package]]
name = "wgpu-core-deps-windows-linux-android"
version = "27.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3"
dependencies = [
"wgpu-hal",
]
[[package]]
name = "wgpu-hal"
version = "27.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases",
"libloading",
"log",
"naga",
"portable-atomic",
"portable-atomic-util",
"raw-window-handle",
"renderdoc-sys",
"thiserror 2.0.17",
"wgpu-types",
]
[[package]]
name = "wgpu-types"
version = "27.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb"
dependencies = [
"bitflags 2.10.0",
"bytemuck",
"js-sys",
"log",
"thiserror 2.0.17",
"web-sys",
]
[[package]]
name = "winapi"
version = "0.3.9"
@ -3723,10 +4029,12 @@ version = "0.30.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66d4b9ed69c4009f6321f762d6e61ad8a2389cd431b97cb1e146812e9e6c732"
dependencies = [
"ahash",
"android-activity",
"atomic-waker",
"bitflags 2.10.0",
"block2",
"bytemuck",
"calloop 0.13.0",
"cfg_aliases",
"concurrent-queue",
@ -3736,24 +4044,33 @@ dependencies = [
"dpi",
"js-sys",
"libc",
"memmap2 0.9.9",
"ndk",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
"objc2-ui-kit",
"orbclient",
"percent-encoding",
"pin-project",
"raw-window-handle",
"redox_syscall 0.4.1",
"rustix 0.38.44",
"smithay-client-toolkit 0.19.2",
"smol_str",
"tracing",
"unicode-segmentation",
"wasm-bindgen",
"wasm-bindgen-futures",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"wayland-protocols-plasma",
"web-sys",
"web-time",
"windows-sys 0.52.0",
"x11-dl",
"x11rb",
"xkbcommon-dl",
]
@ -3778,13 +4095,28 @@ version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "x11-dl"
version = "2.21.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f"
dependencies = [
"libc",
"once_cell",
"pkg-config",
]
[[package]]
name = "x11rb"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414"
dependencies = [
"as-raw-xcb-connection",
"gethostname",
"libc",
"libloading",
"once_cell",
"rustix 1.1.2",
"x11rb-protocol",
]

View File

@ -9,6 +9,10 @@ description = "Crossplatform (and web-compatible) graphing calculator"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
# Enable x11/wayland support for running tests on native targets
native-test = ["eframe/x11", "eframe/wayland"]
[profile.release]
debug = false
codegen-units = 1

16
TODO.md
View File

@ -1,18 +1,20 @@
## TODO:
1. Function management
- Integrals between functions (too hard to implement, maybe will shelve)
- Display intersection between functions (would have to rewrite a lot of the function plotting handling)
- [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI to re-order functions
- Hide/disable functions
- Prevent user from making too many function entries
- Display function errors as tooltips or a warning box (not preventing the display of the graph)
- Clone functions
a. Integrals between functions (too hard to implement, maybe will shelve)
b. Display intersection between functions (would have to rewrite a lot of the function plotting handling)
c. [Drag and drop support](https://github.com/emilk/egui/discussions/1530) in the UI to re-order functions
d. Hide/disable functions
e. Prevent user from making too many function entries
f. Display function errors as tooltips or a warning box (not preventing the display of the graph)
g. Clone functions
2. Smart display of graph
- Display of intersections between functions
3. Allow constants in min/max integral input (like pi or euler's number)
4. Sliding values for functions (like a user-interactable slider that adjusts a variable in the function, like desmos)
5. Fix integral display
6. Better handling of roots and extrema finding
a. For instance, persistance, the roots shouldn't be recalculated for each movement of the viewport
b. If applicable, the roots/extrema should be expressed in terms of constants such as a root of a number, pi, or something else.
7. Add closing animation for function entry
8. Fix mobile text input
9. Write custom plotter

View File

@ -59,8 +59,42 @@
buildInputs = with pkgs; [
openssl
zstd
# Required for running tests with native windowing support
libxkbcommon
libGL
wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.libXrandr
];
# Run all tests on native target before building wasm
# Note: Tests run without --release because the release profile uses
# panic=abort which is incompatible with the test harness.
checkPhase =
let
libPath = pkgs.lib.makeLibraryPath (
with pkgs;
[
libxkbcommon
libGL
wayland
xorg.libX11
xorg.libXcursor
xorg.libXi
xorg.libXrandr
]
);
in
''
runHook preCheck
export HOME=$TMPDIR
export LD_LIBRARY_PATH="${libPath}:$LD_LIBRARY_PATH"
cargo test --workspace --features native-test
runHook postCheck
'';
buildPhase = ''
runHook preBuild
@ -81,7 +115,7 @@
runHook postInstall
'';
doCheck = false;
doCheck = true;
};
# Final web package with wasm-bindgen processing

View File

@ -5,46 +5,46 @@ use std::path::Path;
/// REMEMBER TO UPDATE THIS IF EXMEX ADDS NEW FUNCTIONS
const SUPPORTED_FUNCTIONS: [&str; 22] = [
"abs", "signum", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "floor",
"round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10",
"abs", "signum", "sin", "cos", "tan", "asin", "acos", "atan", "sinh", "cosh", "tanh", "floor",
"round", "ceil", "trunc", "fract", "exp", "sqrt", "cbrt", "ln", "log2", "log10",
];
fn main() {
println!("cargo:rerun-if-changed=src/*");
println!("cargo:rerun-if-changed=src/*");
generate_hashmap();
generate_hashmap();
}
fn generate_hashmap() {
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs");
let mut file = BufWriter::new(File::create(path).expect("Could not create file"));
let path = Path::new(&env::var("OUT_DIR").unwrap()).join("codegen.rs");
let mut file = BufWriter::new(File::create(path).expect("Could not create file"));
let string_hashmap =
compile_hashmap(SUPPORTED_FUNCTIONS.iter().map(|a| a.to_string()).collect());
let string_hashmap =
compile_hashmap(SUPPORTED_FUNCTIONS.iter().map(|a| a.to_string()).collect());
let mut hashmap = phf_codegen::Map::new();
let mut hashmap = phf_codegen::Map::new();
for (key, value) in string_hashmap.iter() {
hashmap.entry(key, value);
}
for (key, value) in string_hashmap.iter() {
hashmap.entry(key, value);
}
write!(
&mut file,
"static COMPLETION_HASHMAP: phf::Map<&'static str, Hint> = {};",
hashmap.build()
)
.expect("Could not write to file");
write!(
&mut file,
"static COMPLETION_HASHMAP: phf::Map<&'static str, Hint> = {};",
hashmap.build()
)
.expect("Could not write to file");
write!(
&mut file,
"#[allow(dead_code)] pub const SUPPORTED_FUNCTIONS: [&str; {}] = {:?};",
SUPPORTED_FUNCTIONS.len(),
SUPPORTED_FUNCTIONS.to_vec()
)
.expect("Could not write to file");
write!(
&mut file,
"#[allow(dead_code)] pub const SUPPORTED_FUNCTIONS: [&str; {}] = {:?};",
SUPPORTED_FUNCTIONS.len(),
SUPPORTED_FUNCTIONS.to_vec()
)
.expect("Could not write to file");
}
include!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/autocomplete_hashmap.rs"
env!("CARGO_MANIFEST_DIR"),
"/src/autocomplete_hashmap.rs"
));

View File

@ -4,113 +4,113 @@ use crate::{generate_hint, Hint, HINT_EMPTY};
#[derive(PartialEq, Debug)]
pub enum Movement {
Complete,
#[allow(dead_code)]
Down,
#[allow(dead_code)]
Up,
None,
Complete,
#[allow(dead_code)]
Down,
#[allow(dead_code)]
Up,
None,
}
impl Movement {
pub const fn is_none(&self) -> bool {
matches!(&self, &Self::None)
}
pub const fn is_none(&self) -> bool {
matches!(&self, &Self::None)
}
pub const fn is_complete(&self) -> bool {
matches!(&self, &Self::Complete)
}
pub const fn is_complete(&self) -> bool {
matches!(&self, &Self::Complete)
}
}
impl Default for Movement {
fn default() -> Self {
Self::None
}
fn default() -> Self {
Self::None
}
}
#[derive(Clone, PartialEq)]
pub struct AutoComplete<'a> {
pub i: usize,
pub hint: &'a Hint<'a>,
pub string: String,
pub i: usize,
pub hint: &'a Hint<'a>,
pub string: String,
}
impl<'a> Default for AutoComplete<'a> {
fn default() -> AutoComplete<'a> {
AutoComplete::EMPTY
}
fn default() -> AutoComplete<'a> {
AutoComplete::EMPTY
}
}
impl<'a> AutoComplete<'a> {
pub const EMPTY: AutoComplete<'a> = Self {
i: 0,
hint: &HINT_EMPTY,
string: String::new(),
};
pub const EMPTY: AutoComplete<'a> = Self {
i: 0,
hint: &HINT_EMPTY,
string: String::new(),
};
#[allow(dead_code)]
pub fn update_string(&mut self, string: &str) {
if self.string != string {
// catch empty strings here to avoid call to `generate_hint` and unnecessary logic
if string.is_empty() {
*self = Self::EMPTY;
} else {
self.string = string.to_owned();
self.do_update_logic();
}
}
}
#[allow(dead_code)]
pub fn update_string(&mut self, string: &str) {
if self.string != string {
// catch empty strings here to avoid call to `generate_hint` and unnecessary logic
if string.is_empty() {
*self = Self::EMPTY;
} else {
self.string = string.to_owned();
self.do_update_logic();
}
}
}
/// Runs update logic assuming that a change to `self.string` has been made
fn do_update_logic(&mut self) {
self.i = 0;
self.hint = generate_hint(&self.string);
}
/// Runs update logic assuming that a change to `self.string` has been made
fn do_update_logic(&mut self) {
self.i = 0;
self.hint = generate_hint(&self.string);
}
#[allow(dead_code)]
pub fn register_movement(&mut self, movement: &Movement) {
if movement.is_none() | self.hint.is_none() {
return;
}
#[allow(dead_code)]
pub fn register_movement(&mut self, movement: &Movement) {
if movement.is_none() | self.hint.is_none() {
return;
}
match self.hint {
Hint::Many(hints) => {
// Impossible for plural hints to be singular or non-existant
debug_assert!(hints.len() > 1); // check on debug
match self.hint {
Hint::Many(hints) => {
// Impossible for plural hints to be singular or non-existant
debug_assert!(hints.len() > 1); // check on debug
match movement {
Movement::Up => {
// Wrap self.i to maximum `i` value if needed
if self.i == 0 {
self.i = hints.len() - 1;
} else {
self.i -= 1;
}
}
Movement::Down => {
// Add one, if resulting value is above maximum `i` value, set `i` to 0
self.i += 1;
if self.i > (hints.len() - 1) {
self.i = 0;
}
}
Movement::Complete => {
self.apply_hint(unsafe { hints.get_unchecked(self.i) });
}
_ => unsafe { unreachable_unchecked() },
}
}
Hint::Single(hint) => {
if movement.is_complete() {
self.apply_hint(hint);
}
}
Hint::None => unsafe { unreachable_unchecked() },
}
}
match movement {
Movement::Up => {
// Wrap self.i to maximum `i` value if needed
if self.i == 0 {
self.i = hints.len() - 1;
} else {
self.i -= 1;
}
}
Movement::Down => {
// Add one, if resulting value is above maximum `i` value, set `i` to 0
self.i += 1;
if self.i > (hints.len() - 1) {
self.i = 0;
}
}
Movement::Complete => {
self.apply_hint(unsafe { hints.get_unchecked(self.i) });
}
_ => unsafe { unreachable_unchecked() },
}
}
Hint::Single(hint) => {
if movement.is_complete() {
self.apply_hint(hint);
}
}
Hint::None => unsafe { unreachable_unchecked() },
}
}
pub fn apply_hint(&mut self, hint: &str) {
self.string.push_str(hint);
self.do_update_logic();
}
pub fn apply_hint(&mut self, hint: &str) {
self.string.push_str(hint);
self.do_update_logic();
}
}

View File

@ -3,81 +3,82 @@ use std::collections::HashSet;
/// https://www.dotnetperls.com/sort-rust
fn compare_len_reverse_alpha(a: &String, b: &String) -> Ordering {
match a.len().cmp(&b.len()) {
Ordering::Equal => b.cmp(a),
order => order,
}
match a.len().cmp(&b.len()) {
Ordering::Equal => b.cmp(a),
order => order,
}
}
/// Generates hashmap (well really a vector of tuple of strings that are then turned into a hashmap by phf)
#[allow(dead_code)]
pub fn compile_hashmap(data: Vec<String>) -> Vec<(String, String)> {
let mut seen = HashSet::new();
let mut seen = HashSet::new();
let tuple_list_1: Vec<(String, String)> = data
.iter()
.map(|e| e.to_string() + "(")
.flat_map(|func| all_possible_splits(func, &mut seen))
.collect();
let tuple_list_1: Vec<(String, String)> = data
.iter()
.map(|e| e.to_string() + "(")
.flat_map(|func| all_possible_splits(func, &mut seen))
.collect();
let keys: Vec<&String> = tuple_list_1.iter().map(|(a, _)| a).collect();
let mut output: Vec<(String, String)> = Vec::new();
let mut seen_3: HashSet<String> = HashSet::new();
let keys: Vec<&String> = tuple_list_1.iter().map(|(a, _)| a).collect();
let mut output: Vec<(String, String)> = Vec::new();
let mut seen_3: HashSet<String> = HashSet::new();
for (key, value) in tuple_list_1.iter() {
if seen_3.contains(key) {
continue;
}
for (key, value) in tuple_list_1.iter() {
if seen_3.contains(key) {
continue;
}
seen_3.insert(key.clone());
seen_3.insert(key.clone());
let count_keys = keys.iter().filter(|a| a == &&key).count();
let count_keys = keys.iter().filter(|a| a == &&key).count();
match count_keys.cmp(&1usize) {
Ordering::Less => {
panic!("Number of values for {key} is 0!");
}
Ordering::Greater => {
let mut multi_data = tuple_list_1
.iter()
.filter(|(a, _)| a == key)
.map(|(_, b)| b)
.collect::<Vec<&String>>();
multi_data.sort_unstable_by(|a, b| compare_len_reverse_alpha(a, b));
output.push((key.clone(), format!("Hint::Many(&{:?})", multi_data)));
}
Ordering::Equal => output.push((key.clone(), format!(r#"Hint::Single("{}")"#, value))),
}
}
match count_keys.cmp(&1usize) {
Ordering::Less => {
panic!("Number of values for {key} is 0!");
}
Ordering::Greater => {
let mut multi_data = tuple_list_1
.iter()
.filter(|(a, _)| a == key)
.map(|(_, b)| b)
.collect::<Vec<&String>>();
multi_data.sort_unstable_by(|a, b| compare_len_reverse_alpha(a, b));
output.push((key.clone(), format!("Hint::Many(&{:?})", multi_data)));
}
Ordering::Equal => output.push((key.clone(), format!(r#"Hint::Single("{}")"#, value))),
}
}
// sort
output.sort_unstable_by(|a, b| {
let new_a = format!(r#"("{}", {})"#, a.0, a.1);
let new_b = format!(r#"("{}", {})"#, b.0, b.1);
// sort
output.sort_unstable_by(|a, b| {
let new_a = format!(r#"("{}", {})"#, a.0, a.1);
let new_b = format!(r#"("{}", {})"#, b.0, b.1);
compare_len_reverse_alpha(&new_b, &new_a)
});
compare_len_reverse_alpha(&new_b, &new_a)
});
output
output
}
/// Returns a vector of all possible splitting combinations of a strings
#[allow(dead_code)]
fn all_possible_splits(
func: String, seen: &mut HashSet<(String, String)>,
func: String,
seen: &mut HashSet<(String, String)>,
) -> Vec<(String, String)> {
(1..func.len())
.map(|i| {
let (first, last) = func.split_at(i);
(first.to_string(), last.to_string())
})
.flat_map(|(first, last)| {
if seen.contains(&(first.clone(), last.clone())) {
return None;
}
seen.insert((first.to_string(), last.to_string()));
(1..func.len())
.map(|i| {
let (first, last) = func.split_at(i);
(first.to_string(), last.to_string())
})
.flat_map(|(first, last)| {
if seen.contains(&(first.clone(), last.clone())) {
return None;
}
seen.insert((first.to_string(), last.to_string()));
Some((first, last))
})
.collect::<Vec<(String, String)>>()
Some((first, last))
})
.collect::<Vec<(String, String)>>()
}

View File

@ -5,9 +5,9 @@ mod splitting;
mod suggestions;
pub use crate::{
autocomplete::{AutoComplete, Movement},
autocomplete_hashmap::compile_hashmap,
parsing::{process_func_str, BackingFunction, FlatExWrapper},
splitting::{split_function, split_function_chars, SplitType},
suggestions::{generate_hint, get_last_term, Hint, HINT_EMPTY, SUPPORTED_FUNCTIONS},
autocomplete::{AutoComplete, Movement},
autocomplete_hashmap::compile_hashmap,
parsing::{process_func_str, BackingFunction, FlatExWrapper},
splitting::{split_function, split_function_chars, SplitType},
suggestions::{generate_hint, get_last_term, Hint, HINT_EMPTY, SUPPORTED_FUNCTIONS},
};

View File

@ -3,176 +3,176 @@ use std::collections::HashMap;
#[derive(Clone, PartialEq)]
pub struct FlatExWrapper {
func: Option<FlatEx<f64>>,
func_str: Option<String>,
func: Option<FlatEx<f64>>,
func_str: Option<String>,
}
impl FlatExWrapper {
const EMPTY: FlatExWrapper = FlatExWrapper {
func: None,
func_str: None,
};
const EMPTY: FlatExWrapper = FlatExWrapper {
func: None,
func_str: None,
};
#[inline]
const fn new(f: FlatEx<f64>) -> Self {
Self {
func: Some(f),
func_str: None,
}
}
#[inline]
const fn new(f: FlatEx<f64>) -> Self {
Self {
func: Some(f),
func_str: None,
}
}
#[inline]
const fn is_none(&self) -> bool {
self.func.is_none()
}
#[inline]
const fn is_none(&self) -> bool {
self.func.is_none()
}
#[inline]
pub fn eval(&self, x: &[f64]) -> f64 {
self.func
.as_ref()
.map(|f| f.eval(x).unwrap_or(f64::NAN))
.unwrap_or(f64::NAN)
}
#[inline]
pub fn eval(&self, x: &[f64]) -> f64 {
self.func
.as_ref()
.map(|f| f.eval(x).unwrap_or(f64::NAN))
.unwrap_or(f64::NAN)
}
#[inline]
fn partial(&self, x: usize) -> Self {
self.func
.as_ref()
.map(|f| f.clone().partial(x).map(Self::new).unwrap_or(Self::EMPTY))
.unwrap_or(Self::EMPTY)
}
#[inline]
fn partial(&self, x: usize) -> Self {
self.func
.as_ref()
.map(|f| f.clone().partial(x).map(Self::new).unwrap_or(Self::EMPTY))
.unwrap_or(Self::EMPTY)
}
#[inline]
fn get_string(&mut self) -> String {
match self.func_str {
Some(ref func_str) => func_str.clone(),
None => {
let calculated = self.func.as_ref().map(|f| f.unparse()).unwrap_or("");
self.func_str = Some(calculated.to_owned());
calculated.to_owned()
}
}
}
#[inline]
fn get_string(&mut self) -> String {
match self.func_str {
Some(ref func_str) => func_str.clone(),
None => {
let calculated = self.func.as_ref().map(|f| f.unparse()).unwrap_or("");
self.func_str = Some(calculated.to_owned());
calculated.to_owned()
}
}
}
#[inline]
fn partial_iter(&self, n: usize) -> Self {
self.func
.as_ref()
.map(|f| {
f.clone()
.partial_iter((0..=n).map(|_| 0))
.map(Self::new)
.unwrap_or(Self::EMPTY)
})
.unwrap_or(Self::EMPTY)
}
#[inline]
fn partial_iter(&self, n: usize) -> Self {
self.func
.as_ref()
.map(|f| {
f.clone()
.partial_iter((0..n).map(|_| 0))
.map(Self::new)
.unwrap_or(Self::EMPTY)
})
.unwrap_or(Self::EMPTY)
}
}
impl Default for FlatExWrapper {
fn default() -> FlatExWrapper {
FlatExWrapper::EMPTY
}
fn default() -> FlatExWrapper {
FlatExWrapper::EMPTY
}
}
/// Function that includes f(x), f'(x), f'(x)'s string representation, and f''(x)
#[derive(Clone, PartialEq)]
pub struct BackingFunction {
/// f(x)
function: FlatExWrapper,
/// f(x)
function: FlatExWrapper,
/// Temporary cache for nth derivative
nth_derivative: HashMap<usize, FlatExWrapper>,
/// Temporary cache for nth derivative
nth_derivative: HashMap<usize, FlatExWrapper>,
}
impl Default for BackingFunction {
fn default() -> Self {
Self::new("").unwrap()
}
fn default() -> Self {
Self::new("").unwrap()
}
}
impl BackingFunction {
pub const fn is_none(&self) -> bool {
self.function.is_none()
}
pub const fn is_none(&self) -> bool {
self.function.is_none()
}
/// Create new [`BackingFunction`] instance
pub fn new(func_str: &str) -> Result<Self, String> {
if func_str.is_empty() {
return Ok(Self {
function: FlatExWrapper::EMPTY,
nth_derivative: HashMap::new(),
});
}
/// Create new [`BackingFunction`] instance
pub fn new(func_str: &str) -> Result<Self, String> {
if func_str.is_empty() {
return Ok(Self {
function: FlatExWrapper::EMPTY,
nth_derivative: HashMap::new(),
});
}
let function = FlatExWrapper::new({
let parse_result = exmex::parse::<f64>(func_str);
let function = FlatExWrapper::new({
let parse_result = exmex::parse::<f64>(func_str);
match &parse_result {
Err(e) => return Err(e.to_string()),
Ok(ok_result) => {
let var_names = ok_result.var_names().to_vec();
match &parse_result {
Err(e) => return Err(e.to_string()),
Ok(ok_result) => {
let var_names = ok_result.var_names().to_vec();
if var_names != ["x"] {
let var_names_not_x: Vec<&String> = var_names
.iter()
.filter(|ele| ele != &"x")
.collect::<Vec<&String>>();
if var_names != ["x"] {
let var_names_not_x: Vec<&String> = var_names
.iter()
.filter(|ele| ele != &"x")
.collect::<Vec<&String>>();
return Err(format!(
"Error: invalid variable{}",
match var_names_not_x.len() {
1 => String::from(": ") + var_names_not_x[0].as_str(),
_ => format!("s: {:?}", var_names_not_x),
}
));
}
}
}
unsafe { parse_result.unwrap_unchecked() }
});
return Err(format!(
"Error: invalid variable{}",
match var_names_not_x.len() {
1 => String::from(": ") + var_names_not_x[0].as_str(),
_ => format!("s: {:?}", var_names_not_x),
}
));
}
}
}
unsafe { parse_result.unwrap_unchecked() }
});
Ok(Self {
function,
Ok(Self {
function,
nth_derivative: HashMap::new(),
})
}
nth_derivative: HashMap::new(),
})
}
// TODO rewrite this logic, it's a mess
pub fn generate_derivative(&mut self, derivative: usize) {
if derivative == 0 {
return;
}
// TODO rewrite this logic, it's a mess
pub fn generate_derivative(&mut self, derivative: usize) {
if derivative == 0 {
return;
}
if !self.nth_derivative.contains_key(&derivative) {
let new_func = self.function.partial_iter(derivative);
self.nth_derivative.insert(derivative, new_func.clone());
}
}
if !self.nth_derivative.contains_key(&derivative) {
let new_func = self.function.partial_iter(derivative);
self.nth_derivative.insert(derivative, new_func.clone());
}
}
pub fn get_function_derivative(&self, derivative: usize) -> &FlatExWrapper {
if derivative == 0 {
return &self.function;
} else {
return self
.nth_derivative
.get(&derivative)
.unwrap_or(&FlatExWrapper::EMPTY);
}
}
pub fn get_function_derivative(&self, derivative: usize) -> &FlatExWrapper {
if derivative == 0 {
return &self.function;
} else {
return self
.nth_derivative
.get(&derivative)
.unwrap_or(&FlatExWrapper::EMPTY);
}
}
pub fn get(&mut self, derivative: usize, x: f64) -> f64 {
self.get_function_derivative(derivative).eval(&[x])
}
pub fn get(&mut self, derivative: usize, x: f64) -> f64 {
self.get_function_derivative(derivative).eval(&[x])
}
}
fn prettyify_function_str(func: &str) -> String {
let new_str = func.replace("{x}", "x");
let new_str = func.replace("{x}", "x");
if &new_str == "0/0" {
"Undefined".to_owned()
} else {
new_str
}
if &new_str == "0/0" {
"Undefined".to_owned()
} else {
new_str
}
}
// pub const VALID_VARIABLES: [char; 3] = ['x', 'e', 'π'];
@ -180,15 +180,15 @@ fn prettyify_function_str(func: &str) -> String {
/// Case insensitive checks for if `c` is a character used to represent a variable
#[inline]
pub const fn is_variable(c: &char) -> bool {
let c = c.to_ascii_lowercase();
(c == 'x') | (c == 'e') | (c == 'π')
let c = c.to_ascii_lowercase();
(c == 'x') | (c == 'e') | (c == 'π')
}
/// Adds asterisks where needed in a function
pub fn process_func_str(function_in: &str) -> String {
if function_in.is_empty() {
return String::new();
}
if function_in.is_empty() {
return String::new();
}
crate::split_function(function_in, crate::SplitType::Multiplication).join("*")
crate::split_function(function_in, crate::SplitType::Multiplication).join("*")
}

View File

@ -1,204 +1,208 @@
use crate::parsing::is_variable;
pub fn split_function(input: &str, split: SplitType) -> Vec<String> {
split_function_chars(
&input
.replace("pi", "π") // replace "pi" text with pi symbol
.replace("**", "^") // support alternate manner of expressing exponents
.replace("exp", "\u{1fc93}") // stop-gap solution to fix the `exp` function
.chars()
.collect::<Vec<char>>(),
split,
)
.iter()
.map(|x| x.replace('\u{1fc93}', "exp")) // Convert back to `exp` text
.collect::<Vec<String>>()
split_function_chars(
&input
.replace("pi", "π") // replace "pi" text with pi symbol
.replace("**", "^") // support alternate manner of expressing exponents
.replace("exp", "\u{1fc93}") // stop-gap solution to fix the `exp` function
.chars()
.collect::<Vec<char>>(),
split,
)
.iter()
.map(|x| x.replace('\u{1fc93}', "exp")) // Convert back to `exp` text
.collect::<Vec<String>>()
}
// Specifies how to split a function
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum SplitType {
Multiplication,
Term,
Multiplication,
Term,
}
/// Used to store info about a character
struct BoolSlice {
closing_parens: bool,
open_parens: bool,
number: bool,
letter: bool,
variable: bool,
masked_num: bool,
masked_var: bool,
closing_parens: bool,
open_parens: bool,
number: bool,
letter: bool,
variable: bool,
masked_num: bool,
masked_var: bool,
}
impl BoolSlice {
const fn from_char(c: &char, prev_masked_num: bool, prev_masked_var: bool) -> Self {
let isnumber = c.is_ascii_digit();
let isvariable = is_variable(c);
Self {
closing_parens: *c == ')',
open_parens: *c == '(',
number: isnumber,
letter: c.is_ascii_alphabetic(),
variable: isvariable,
masked_num: match isnumber {
true => prev_masked_num,
false => false,
},
masked_var: match isvariable {
true => prev_masked_var,
false => false,
},
}
}
const fn from_char(c: &char, prev_masked_num: bool, prev_masked_var: bool) -> Self {
let isnumber = c.is_ascii_digit();
let isvariable = is_variable(c);
Self {
closing_parens: *c == ')',
open_parens: *c == '(',
number: isnumber,
letter: c.is_ascii_alphabetic(),
variable: isvariable,
masked_num: match isnumber {
true => prev_masked_num,
false => false,
},
masked_var: match isvariable {
true => prev_masked_var,
false => false,
},
}
}
const fn is_unmasked_variable(&self) -> bool { self.variable && !self.masked_var }
const fn is_unmasked_variable(&self) -> bool {
self.variable && !self.masked_var
}
const fn is_unmasked_number(&self) -> bool { self.number && !self.masked_num }
const fn is_unmasked_number(&self) -> bool {
self.number && !self.masked_num
}
const fn calculate_mask(&mut self, other: &BoolSlice) {
if other.masked_num && self.number {
// If previous char was a masked number, and current char is a number, mask current char's variable status
self.masked_num = true;
} else if other.masked_var && self.variable {
// If previous char was a masked variable, and current char is a variable, mask current char's variable status
self.masked_var = true;
} else if other.letter && !other.is_unmasked_variable() {
self.masked_num = self.number;
self.masked_var = self.variable;
}
}
const fn calculate_mask(&mut self, other: &BoolSlice) {
if other.masked_num && self.number {
// If previous char was a masked number, and current char is a number, mask current char's variable status
self.masked_num = true;
} else if other.masked_var && self.variable {
// If previous char was a masked variable, and current char is a variable, mask current char's variable status
self.masked_var = true;
} else if other.letter && !other.is_unmasked_variable() {
self.masked_num = self.number;
self.masked_var = self.variable;
}
}
const fn splitable(&self, c: &char, other: &BoolSlice, split: &SplitType) -> bool {
if (*c == '*') | (matches!(split, &SplitType::Term) && other.open_parens) {
true
} else if other.closing_parens {
// Cases like `)x`, `)2`, and `)(`
return (*c == '(')
| (self.letter && !self.is_unmasked_variable())
| self.is_unmasked_variable()
| self.is_unmasked_number();
} else if *c == '(' {
// Cases like `x(` and `2(`
return (other.is_unmasked_variable() | other.is_unmasked_number()) && !other.letter;
} else if other.is_unmasked_number() {
// Cases like `2x` and `2sin(x)`
return self.is_unmasked_variable() | self.letter;
} else if self.is_unmasked_variable() | self.letter {
// Cases like `e2` and `xx`
return other.is_unmasked_number()
| (other.is_unmasked_variable() && self.is_unmasked_variable())
| other.is_unmasked_variable();
} else if (self.is_unmasked_number() | self.letter | self.is_unmasked_variable())
&& (other.is_unmasked_number() | other.letter)
{
return true;
} else {
return self.is_unmasked_number() && other.is_unmasked_variable();
}
}
const fn splitable(&self, c: &char, other: &BoolSlice, split: &SplitType) -> bool {
if (*c == '*') | (matches!(split, &SplitType::Term) && other.open_parens) {
true
} else if other.closing_parens {
// Cases like `)x`, `)2`, and `)(`
return (*c == '(')
| (self.letter && !self.is_unmasked_variable())
| self.is_unmasked_variable()
| self.is_unmasked_number();
} else if *c == '(' {
// Cases like `x(` and `2(`
return (other.is_unmasked_variable() | other.is_unmasked_number()) && !other.letter;
} else if other.is_unmasked_number() {
// Cases like `2x` and `2sin(x)`
return self.is_unmasked_variable() | self.letter;
} else if self.is_unmasked_variable() | self.letter {
// Cases like `e2` and `xx`
return other.is_unmasked_number()
| (other.is_unmasked_variable() && self.is_unmasked_variable())
| other.is_unmasked_variable();
} else if (self.is_unmasked_number() | self.letter | self.is_unmasked_variable())
&& (other.is_unmasked_number() | other.letter)
{
return true;
} else {
return self.is_unmasked_number() && other.is_unmasked_variable();
}
}
}
// Splits a function (which is represented as an array of characters) based off of the value of SplitType
pub fn split_function_chars(chars: &[char], split: SplitType) -> Vec<String> {
// Catch some basic cases
match chars.len() {
0 => return Vec::new(),
1 => return vec![chars[0].to_string()],
_ => {}
}
// Catch some basic cases
match chars.len() {
0 => return Vec::new(),
1 => return vec![chars[0].to_string()],
_ => {}
}
// Resulting split-up data
let mut data: Vec<String> = std::vec::from_elem(chars[0].to_string(), 1);
// Resulting split-up data
let mut data: Vec<String> = std::vec::from_elem(chars[0].to_string(), 1);
// Setup first char here
let mut prev_char: BoolSlice = BoolSlice::from_char(&chars[0], false, false);
// Setup first char here
let mut prev_char: BoolSlice = BoolSlice::from_char(&chars[0], false, false);
let mut last = unsafe { data.last_mut().unwrap_unchecked() };
let mut last = unsafe { data.last_mut().unwrap_unchecked() };
// Iterate through all chars excluding the first one
for c in chars.iter().skip(1) {
// Set data about current character
let mut curr_c = BoolSlice::from_char(c, prev_char.masked_num, prev_char.masked_var);
// Iterate through all chars excluding the first one
for c in chars.iter().skip(1) {
// Set data about current character
let mut curr_c = BoolSlice::from_char(c, prev_char.masked_num, prev_char.masked_var);
curr_c.calculate_mask(&prev_char);
curr_c.calculate_mask(&prev_char);
// Append split
if curr_c.splitable(c, &prev_char, &split) {
// create new buffer
data.push(String::new());
last = unsafe { data.last_mut().unwrap_unchecked() };
}
// Append split
if curr_c.splitable(c, &prev_char, &split) {
// create new buffer
data.push(String::new());
last = unsafe { data.last_mut().unwrap_unchecked() };
}
// Exclude asterisks
if c != &'*' {
last.push(*c);
}
// Exclude asterisks
if c != &'*' {
last.push(*c);
}
// Move current character data to `prev_char`
prev_char = curr_c;
}
// Move current character data to `prev_char`
prev_char = curr_c;
}
data
data
}
#[cfg(test)]
fn assert_test(input: &str, expected: &[&str], split: SplitType) {
let output = split_function(input, split);
let expected_owned = expected
.iter()
.map(|&x| x.to_owned())
.collect::<Vec<String>>();
if output != expected_owned {
panic!(
"split type: {:?} of {} resulted in {:?} not {:?}",
split, input, output, expected
);
}
let output = split_function(input, split);
let expected_owned = expected
.iter()
.map(|&x| x.to_owned())
.collect::<Vec<String>>();
if output != expected_owned {
panic!(
"split type: {:?} of {} resulted in {:?} not {:?}",
split, input, output, expected
);
}
}
#[test]
fn split_function_test() {
assert_test(
"sin(x)cos(x)",
&["sin(x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"sin(x)cos(x)",
&["sin(x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"tanh(cos(x)xx)cos(x)",
&["tanh(cos(x)", "x", "x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"tanh(cos(x)xx)cos(x)",
&["tanh(cos(x)", "x", "x)", "cos(x)"],
SplitType::Multiplication,
);
assert_test(
"tanh(sin(cos(x)xsin(x)))",
&["tanh(sin(cos(x)", "x", "sin(x)))"],
SplitType::Multiplication,
);
assert_test(
"tanh(sin(cos(x)xsin(x)))",
&["tanh(sin(cos(x)", "x", "sin(x)))"],
SplitType::Multiplication,
);
// Some test cases from https://github.com/GraphiteEditor/Graphite/blob/2515620a77478e57c255cd7d97c13cc7065dd99d/frontend/wasm/src/editor_api.rs#L829-L840
assert_test("2pi", &["2", "π"], SplitType::Multiplication);
assert_test("sin(2pi)", &["sin(2", "π)"], SplitType::Multiplication);
assert_test("2sin(pi)", &["2", "sin(π)"], SplitType::Multiplication);
assert_test(
"2sin(3(4 + 5))",
&["2", "sin(3", "(4 + 5))"],
SplitType::Multiplication,
);
assert_test("3abs(-4)", &["3", "abs(-4)"], SplitType::Multiplication);
assert_test("-1(4)", &["-1", "(4)"], SplitType::Multiplication);
assert_test("(-1)4", &["(-1)", "4"], SplitType::Multiplication);
assert_test(
"(((-1)))(4)",
&["(((-1)))", "(4)"],
SplitType::Multiplication,
);
assert_test(
"2sin(π) + 2cos(tau)",
&["2", "sin(π) + 2", "cos(tau)"],
SplitType::Multiplication,
);
// Some test cases from https://github.com/GraphiteEditor/Graphite/blob/2515620a77478e57c255cd7d97c13cc7065dd99d/frontend/wasm/src/editor_api.rs#L829-L840
assert_test("2pi", &["2", "π"], SplitType::Multiplication);
assert_test("sin(2pi)", &["sin(2", "π)"], SplitType::Multiplication);
assert_test("2sin(pi)", &["2", "sin(π)"], SplitType::Multiplication);
assert_test(
"2sin(3(4 + 5))",
&["2", "sin(3", "(4 + 5))"],
SplitType::Multiplication,
);
assert_test("3abs(-4)", &["3", "abs(-4)"], SplitType::Multiplication);
assert_test("-1(4)", &["-1", "(4)"], SplitType::Multiplication);
assert_test("(-1)4", &["(-1)", "4"], SplitType::Multiplication);
assert_test(
"(((-1)))(4)",
&["(((-1)))", "(4)"],
SplitType::Multiplication,
);
assert_test(
"2sin(π) + 2cos(tau)",
&["2", "sin(π) + 2", "cos(tau)"],
SplitType::Multiplication,
);
}

View File

@ -14,106 +14,112 @@ macro_rules! test_print {
/// Generate a hint based on the input `input`, returns an `Option<String>`
pub fn generate_hint<'a>(input: &str) -> &'a Hint<'a> {
if input.is_empty() {
&HINT_EMPTY
} else {
let chars: Vec<char> = input.chars().collect::<Vec<char>>();
if input.is_empty() {
&HINT_EMPTY
} else {
let chars: Vec<char> = input.chars().collect::<Vec<char>>();
let key = get_last_term(&chars);
match key {
Some(key) => {
if let Some(hint) = COMPLETION_HASHMAP.get(&key) {
return hint;
}
}
None => {
return &Hint::None;
}
}
let key = get_last_term(&chars);
match key {
Some(key) => {
if let Some(hint) = COMPLETION_HASHMAP.get(&key) {
return hint;
}
}
None => {
return &Hint::None;
}
}
let mut open_parens: usize = 0;
let mut closed_parens: usize = 0;
chars.iter().for_each(|chr| match *chr {
'(' => open_parens += 1,
')' => closed_parens += 1,
_ => {}
});
let mut open_parens: usize = 0;
let mut closed_parens: usize = 0;
chars.iter().for_each(|chr| match *chr {
'(' => open_parens += 1,
')' => closed_parens += 1,
_ => {}
});
if open_parens > closed_parens {
return &HINT_CLOSED_PARENS;
}
if open_parens > closed_parens {
return &HINT_CLOSED_PARENS;
}
&Hint::None
}
&Hint::None
}
}
pub fn get_last_term(chars: &[char]) -> Option<String> {
if chars.is_empty() {
return None;
}
if chars.is_empty() {
return None;
}
let mut result = split_function_chars(chars, SplitType::Term);
result.pop()
let mut result = split_function_chars(chars, SplitType::Term);
result.pop()
}
#[derive(PartialEq, Clone, Copy)]
pub enum Hint<'a> {
Single(&'a str),
Many(&'a [&'a str]),
None,
Single(&'a str),
Many(&'a [&'a str]),
None,
}
impl<'a> std::fmt::Display for Hint<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Hint::Single(single_data) => {
write!(f, "{}", single_data)
}
Hint::Many(multi_data) => {
write!(f, "{:?}", multi_data)
}
Hint::None => {
write!(f, "None")
}
}
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Hint::Single(single_data) => {
write!(f, "{}", single_data)
}
Hint::Many(multi_data) => {
write!(f, "{:?}", multi_data)
}
Hint::None => {
write!(f, "None")
}
}
}
}
impl<'a> std::fmt::Debug for Hint<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl<'a> Hint<'a> {
#[inline]
pub const fn is_none(&self) -> bool { matches!(&self, &Hint::None) }
#[inline]
pub const fn is_none(&self) -> bool {
matches!(&self, &Hint::None)
}
#[inline]
#[allow(dead_code)]
pub const fn is_some(&self) -> bool { !self.is_none() }
#[inline]
#[allow(dead_code)]
pub const fn is_some(&self) -> bool {
!self.is_none()
}
#[inline]
#[allow(dead_code)]
pub const fn is_single(&self) -> bool { matches!(&self, &Hint::Single(_)) }
#[inline]
#[allow(dead_code)]
pub const fn is_single(&self) -> bool {
matches!(&self, &Hint::Single(_))
}
#[inline]
#[allow(dead_code)]
pub const fn single(&self) -> Option<&str> {
match self {
Hint::Single(data) => Some(data),
_ => None,
}
}
#[inline]
#[allow(dead_code)]
pub const fn single(&self) -> Option<&str> {
match self {
Hint::Single(data) => Some(data),
_ => None,
}
}
#[inline]
#[allow(dead_code)]
pub const fn many(&self) -> Option<&[&str]> {
match self {
Hint::Many(data) => Some(data),
_ => None,
}
}
#[inline]
#[allow(dead_code)]
pub const fn many(&self) -> Option<&[&str]> {
match self {
Hint::Many(data) => Some(data),
_ => None,
}
}
}
include!(concat!(env!("OUT_DIR"), "/codegen.rs"));

View File

@ -21,6 +21,6 @@ fn main() -> eframe::Result<()> {
eframe::run_native(
"(Yet-to-be-named) Graphing Software",
eframe::NativeOptions::default(),
Box::new(|cc| Box::new(math_app::MathApp::new(cc))),
Box::new(|cc| Ok(Box::new(math_app::MathApp::new(cc)))),
)
}

View File

@ -84,7 +84,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a[i].0 as f32, DERIVATIVE_TARGET[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a[i].1 as f32, DERIVATIVE_TARGET[i].1 as f32, f32::EPSILON)
{
panic!("Expected: {:?}\nGot: {:?}", a, DERIVATIVE_TARGET);
panic!("Expected: {:?}\nGot: {:?}", DERIVATIVE_TARGET, a);
}
}
@ -98,7 +98,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a_1[i].0 as f32, BACK_TARGET[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a_1[i].1 as f32, BACK_TARGET[i].1 as f32, f32::EPSILON)
{
panic!("Expected: {:?}\nGot: {:?}", a_1, BACK_TARGET);
panic!("Expected: {:?}\nGot: {:?}", BACK_TARGET, a_1);
}
}
}
@ -131,7 +131,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON)
{
panic!("Expected: {:?}\nGot: {:?}", a, b);
panic!("Expected: {:?}\nGot: {:?}", b, a);
}
}
@ -160,7 +160,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON)
{
panic!("Expected: {:?}\nGot: {:?}", a_1, b_1);
panic!("Expected: {:?}\nGot: {:?}", b_1, a_1);
}
}
}
@ -193,7 +193,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a[i].0 as f32, b[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a[i].1 as f32, b[i].1 as f32, f32::EPSILON)
{
panic!("Expected: {:?}\nGot: {:?}", a, b);
panic!("Expected: {:?}\nGot: {:?}", b, a);
}
}
@ -222,7 +222,7 @@ fn do_test(sum: Riemann, area_target: f64) {
if !emath::almost_equal(a_1[i].0 as f32, b_1[i].0 as f32, f32::EPSILON)
| !emath::almost_equal(a_1[i].1 as f32, b_1[i].1 as f32, f32::EPSILON)
{
panic!("Expected: {:?}\nGot: {:?}", a_1, b_1);
panic!("Expected: {:?}\nGot: {:?}", b_1, a_1);
}
}
}
@ -271,3 +271,252 @@ fn middle_function() {
fn right_function() {
do_test(Riemann::Right, 0.8800000000000001);
}
#[test]
fn test_extrema() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_extrema = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4"); // Parabola with vertex at (0, -4)
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x^2 - 4, f'(x) = 2x
// Extrema occurs where f'(x) = 0, so at x = 0
assert!(!function.extrema_data.is_empty());
// Should have exactly one extremum at x = 0
assert_eq!(function.extrema_data.len(), 1);
let extremum = function.extrema_data[0];
assert!(emath::almost_equal(extremum.x as f32, 0.0, f32::EPSILON));
assert!(emath::almost_equal(extremum.y as f32, -4.0, f32::EPSILON));
}
#[test]
fn test_extrema_multiple() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_extrema = true;
let mut function = FunctionEntry::default();
function.update_string("x^3 - 3*x"); // Cubic with local max and min
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x^3 - 3x, f'(x) = 3x^2 - 3
// Extrema occur where f'(x) = 0, so at x = ±1
assert!(!function.extrema_data.is_empty());
// Should have exactly two extrema
assert_eq!(function.extrema_data.len(), 2);
// Sort by x coordinate for consistent testing
let mut extrema = function.extrema_data.clone();
extrema.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
// First extremum at x = -1, f(-1) = -1 + 3 = 2
assert!(emath::almost_equal(extrema[0].x as f32, -1.0, 0.01));
assert!(emath::almost_equal(extrema[0].y as f32, 2.0, 0.01));
// Second extremum at x = 1, f(1) = 1 - 3 = -2
assert!(emath::almost_equal(extrema[1].x as f32, 1.0, 0.01));
assert!(emath::almost_equal(extrema[1].y as f32, -2.0, 0.01));
}
#[test]
fn test_extrema_disabled() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_extrema = false; // Disable extrema
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4");
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Extrema data should be empty when disabled
assert!(function.extrema_data.is_empty());
}
#[test]
fn test_roots() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4"); // Parabola crossing x-axis at ±2
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x^2 - 4, roots occur where x^2 = 4, so at x = ±2
assert!(!function.root_data.is_empty());
// Should have exactly two roots
assert_eq!(function.root_data.len(), 2);
// Sort by x coordinate for consistent testing
let mut roots = function.root_data.clone();
roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
// First root at x = -2
assert!(emath::almost_equal(roots[0].x as f32, -2.0, 0.01));
assert!(emath::almost_equal(roots[0].y as f32, 0.0, 0.001));
// Second root at x = 2
assert!(emath::almost_equal(roots[1].x as f32, 2.0, 0.01));
assert!(emath::almost_equal(roots[1].y as f32, 0.0, 0.001));
}
#[test]
fn test_roots_single() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x - 1"); // Linear function crossing x-axis at x = 1
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// For f(x) = x - 1, root occurs at x = 1
assert!(!function.root_data.is_empty());
// Should have exactly one root
assert_eq!(function.root_data.len(), 1);
let root = function.root_data[0];
assert!(emath::almost_equal(root.x as f32, 1.0, 0.01));
assert!(emath::almost_equal(root.y as f32, 0.0, f32::EPSILON));
}
#[test]
fn test_roots_disabled() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_roots = false; // Disable roots
let mut function = FunctionEntry::default();
function.update_string("x^2 - 4");
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Root data should be empty when disabled
assert!(function.root_data.is_empty());
}
#[test]
fn test_extrema_and_roots_together() {
let mut settings = app_settings_constructor(Riemann::Middle, -3.0, 3.0, 200, 200, -3.0, 3.0);
settings.do_extrema = true;
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 - 1"); // Parabola with vertex at (0, -1) and roots at ±1
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Should have one extremum at x = 0
assert!(!function.extrema_data.is_empty());
assert_eq!(function.extrema_data.len(), 1);
let extremum = function.extrema_data[0];
assert!(emath::almost_equal(extremum.x as f32, 0.0, 0.01));
assert!(emath::almost_equal(extremum.y as f32, -1.0, 0.01));
// Should have two roots at x = ±1
assert!(!function.root_data.is_empty());
assert_eq!(function.root_data.len(), 2);
let mut roots = function.root_data.clone();
roots.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
assert!(emath::almost_equal(roots[0].x as f32, -1.0, 0.01));
assert!(emath::almost_equal(roots[1].x as f32, 1.0, 0.01));
}
#[test]
fn test_extrema_no_extrema() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_extrema = true;
let mut function = FunctionEntry::default();
function.update_string("x"); // Linear function has no extrema
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Linear function should have no extrema
assert!(function.extrema_data.is_empty());
}
#[test]
fn test_roots_no_roots() {
let mut settings = app_settings_constructor(Riemann::Middle, -2.0, 2.0, 100, 100, -2.0, 2.0);
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("x^2 + 1"); // Parabola that never crosses x-axis
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Function that never crosses x-axis should have no roots
assert!(function.root_data.is_empty());
}
#[test]
fn test_extrema_and_roots_with_trig() {
let mut settings = app_settings_constructor(Riemann::Middle, -4.0, 4.0, 300, 300, -4.0, 4.0);
settings.do_extrema = true;
settings.do_roots = true;
let mut function = FunctionEntry::default();
function.update_string("sin(x)"); // Sine function has extrema at odd multiples of π/2
function.integral = false;
function.derivative = false;
function.calculate(true, true, false, settings);
// Sine function should have extrema in the given range
assert!(!function.extrema_data.is_empty());
// Should have multiple extrema (local max/min)
assert!(function.extrema_data.len() >= 2);
// Check that extrema are at approximately the right locations
// Local max at π/2 ≈ 1.57, local min at 3π/2 ≈ 4.71 (outside range)
// Local min at -π/2 ≈ -1.57, local max at -3π/2 ≈ -4.71 (outside range)
let extrema_x: Vec<f32> = function.extrema_data.iter().map(|p| p.x as f32).collect();
// Should have extrema near ±π/2
assert!(extrema_x
.iter()
.any(|&x| emath::almost_equal(x, std::f32::consts::PI / 2.0, 0.1)));
assert!(extrema_x
.iter()
.any(|&x| emath::almost_equal(x, -std::f32::consts::PI / 2.0, 0.1)));
let roots_x: Vec<f32> = function.root_data.iter().map(|p| p.x as f32).collect();
assert!(roots_x
.iter()
.any(|&x| emath::almost_equal(x, std::f32::consts::PI, 0.1)));
assert!(roots_x
.iter()
.any(|&x| emath::almost_equal(x, -std::f32::consts::PI, 0.1)));
assert!(roots_x.iter().any(|&x| emath::almost_equal(x, 0.0, 0.1)));
}

View File

@ -152,7 +152,7 @@ fn newtons_method() {
let data = newtons_method(
&get_flatexwrapper("x^2 -1"),
&get_flatexwrapper("2x"),
&get_flatexwrapper("2*x"),
3.0,
&(0.0..5.0),
f64::EPSILON,