0.1.11
This commit is contained in:
@@ -0,0 +1,861 @@
|
||||
//! Engineering (scientific) calculator — formula entry with trig/log/powers.
|
||||
|
||||
use std::f64::consts::{E, PI};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum AngleMode {
|
||||
Deg,
|
||||
Rad,
|
||||
}
|
||||
|
||||
impl AngleMode {
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
AngleMode::Deg => "DEG",
|
||||
AngleMode::Rad => "RAD",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cycle(self) -> Self {
|
||||
match self {
|
||||
AngleMode::Deg => AngleMode::Rad,
|
||||
AngleMode::Rad => AngleMode::Deg,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_rad(self, x: f64) -> f64 {
|
||||
match self {
|
||||
AngleMode::Deg => x.to_radians(),
|
||||
AngleMode::Rad => x,
|
||||
}
|
||||
}
|
||||
|
||||
fn from_rad(self, x: f64) -> f64 {
|
||||
match self {
|
||||
AngleMode::Deg => x.to_degrees(),
|
||||
AngleMode::Rad => x,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SciError {
|
||||
DivByZero,
|
||||
Domain,
|
||||
Overflow,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
impl SciError {
|
||||
pub fn message(self) -> &'static str {
|
||||
match self {
|
||||
SciError::DivByZero => "Cannot divide by zero",
|
||||
SciError::Domain => "Invalid input",
|
||||
SciError::Overflow => "Overflow",
|
||||
SciError::Invalid => "Invalid input",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Engineering {
|
||||
entry: String,
|
||||
typing: bool,
|
||||
formula: String,
|
||||
expression: String,
|
||||
done: bool,
|
||||
error: Option<SciError>,
|
||||
angle: AngleMode,
|
||||
/// 2nd function layer (asin instead of sin, …).
|
||||
second: bool,
|
||||
memory: f64,
|
||||
memory_set: bool,
|
||||
}
|
||||
|
||||
impl Default for Engineering {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Engineering {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entry: "0".into(),
|
||||
typing: false,
|
||||
formula: String::new(),
|
||||
expression: String::new(),
|
||||
done: false,
|
||||
error: None,
|
||||
angle: AngleMode::Deg,
|
||||
second: false,
|
||||
memory: 0.0,
|
||||
memory_set: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn display(&self) -> &str {
|
||||
if let Some(err) = self.error {
|
||||
return err.message();
|
||||
}
|
||||
&self.entry
|
||||
}
|
||||
|
||||
pub fn expression(&self) -> &str {
|
||||
&self.expression
|
||||
}
|
||||
|
||||
pub fn angle_mode(&self) -> AngleMode {
|
||||
self.angle
|
||||
}
|
||||
|
||||
pub fn second(&self) -> bool {
|
||||
self.second
|
||||
}
|
||||
|
||||
pub fn has_memory(&self) -> bool {
|
||||
self.memory_set
|
||||
}
|
||||
|
||||
pub fn cycle_angle(&mut self) {
|
||||
self.angle = self.angle.cycle();
|
||||
}
|
||||
|
||||
pub fn toggle_second(&mut self) {
|
||||
self.second = !self.second;
|
||||
}
|
||||
|
||||
pub fn clear_all(&mut self) {
|
||||
let angle = self.angle;
|
||||
let mem = (self.memory, self.memory_set);
|
||||
*self = Self::new();
|
||||
self.angle = angle;
|
||||
self.memory = mem.0;
|
||||
self.memory_set = mem.1;
|
||||
}
|
||||
|
||||
pub fn clear_entry(&mut self) {
|
||||
self.error = None;
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self) {
|
||||
if self.error.is_some() {
|
||||
self.clear_entry();
|
||||
return;
|
||||
}
|
||||
if self.done {
|
||||
return;
|
||||
}
|
||||
if self.typing {
|
||||
self.entry.pop();
|
||||
if self.entry.is_empty() || self.entry == "-" {
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
}
|
||||
self.sync_expr();
|
||||
return;
|
||||
}
|
||||
let mut s = self.formula.trim_end().to_string();
|
||||
if s.ends_with('(') {
|
||||
s.pop();
|
||||
while s
|
||||
.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_alphanumeric() || c == '_')
|
||||
{
|
||||
s.pop();
|
||||
}
|
||||
} else if s.chars().last().is_some_and(|c| "+−×÷^)".contains(c)) {
|
||||
s.pop();
|
||||
} else {
|
||||
// pop trailing number / constant
|
||||
while s
|
||||
.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_digit() || c == '.' || c == 'π' || c == 'e')
|
||||
{
|
||||
s.pop();
|
||||
}
|
||||
}
|
||||
while s.ends_with(' ') {
|
||||
s.pop();
|
||||
}
|
||||
self.formula = s;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn input_digit(&mut self, d: char) {
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
self.begin_entry_if_done();
|
||||
if !self.typing {
|
||||
self.entry = d.to_string();
|
||||
self.typing = true;
|
||||
} else if self.entry == "0" {
|
||||
self.entry = d.to_string();
|
||||
} else if self.entry == "-0" {
|
||||
self.entry = format!("-{d}");
|
||||
} else if digit_count(&self.entry) < 16 {
|
||||
self.entry.push(d);
|
||||
}
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn input_dot(&mut self) {
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
self.begin_entry_if_done();
|
||||
if !self.typing {
|
||||
self.entry = "0.".into();
|
||||
self.typing = true;
|
||||
} else if !self.entry.contains('.') {
|
||||
self.entry.push('.');
|
||||
}
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn negate(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if self.done {
|
||||
if let Ok(v) = self.entry.parse::<f64>() {
|
||||
self.set_result(-v);
|
||||
self.done = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.typing || self.entry != "0" {
|
||||
if let Some(stripped) = self.entry.strip_prefix('-') {
|
||||
self.entry = stripped.to_string();
|
||||
} else if self.entry != "0" {
|
||||
self.entry = format!("-{}", self.entry);
|
||||
}
|
||||
self.typing = true;
|
||||
self.sync_expr();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_op(&mut self, op: &str) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if self.done {
|
||||
self.formula.clear();
|
||||
self.done = false;
|
||||
self.typing = true;
|
||||
}
|
||||
self.flush_entry();
|
||||
// Replace trailing binary op.
|
||||
let t = self.formula.trim_end();
|
||||
if t.chars().last().is_some_and(|c| "+−×÷^".contains(c)) {
|
||||
while self.formula.ends_with(' ') {
|
||||
self.formula.pop();
|
||||
}
|
||||
self.formula.pop();
|
||||
while self.formula.ends_with(' ') {
|
||||
self.formula.pop();
|
||||
}
|
||||
}
|
||||
if !self.formula.is_empty() {
|
||||
self.formula.push(' ');
|
||||
}
|
||||
self.formula.push_str(op);
|
||||
self.formula.push(' ');
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
self.second = false;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn paren_open(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
if self.done {
|
||||
self.formula.clear();
|
||||
self.done = false;
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
}
|
||||
if self.typing {
|
||||
self.flush_entry();
|
||||
let t = self.formula.trim_end();
|
||||
if t.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
|
||||
{
|
||||
self.formula.push_str(" × ");
|
||||
}
|
||||
} else {
|
||||
let t = self.formula.trim_end();
|
||||
if t.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_digit() || c == ')')
|
||||
{
|
||||
self.formula.push_str(" × ");
|
||||
}
|
||||
}
|
||||
self.formula.push('(');
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn paren_close(&mut self) {
|
||||
if self.error.is_some() || self.done {
|
||||
return;
|
||||
}
|
||||
let opens = self.formula.chars().filter(|&c| c == '(').count();
|
||||
let closes = self.formula.chars().filter(|&c| c == ')').count();
|
||||
if opens <= closes {
|
||||
return;
|
||||
}
|
||||
if self.typing || (!self.formula.ends_with('(') && self.entry != "0") {
|
||||
self.flush_entry();
|
||||
}
|
||||
self.formula.push(')');
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
/// Insert a function prefix like `sin(` or constant `π`.
|
||||
pub fn insert_func(&mut self, name: &str) {
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
if self.done {
|
||||
self.formula.clear();
|
||||
self.done = false;
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
}
|
||||
if self.typing {
|
||||
self.flush_entry();
|
||||
let t = self.formula.trim_end();
|
||||
if t.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
|
||||
{
|
||||
self.formula.push_str(" × ");
|
||||
}
|
||||
} else {
|
||||
let t = self.formula.trim_end();
|
||||
if t.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
|
||||
{
|
||||
self.formula.push_str(" × ");
|
||||
}
|
||||
}
|
||||
self.formula.push_str(name);
|
||||
self.formula.push('(');
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
self.second = false;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn insert_const(&mut self, name: &str) {
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
if self.done {
|
||||
self.formula.clear();
|
||||
self.done = false;
|
||||
}
|
||||
if self.typing {
|
||||
self.flush_entry();
|
||||
self.formula.push_str(" × ");
|
||||
} else {
|
||||
let t = self.formula.trim_end();
|
||||
if t.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_digit() || c == ')' || c == 'π' || c == 'e')
|
||||
{
|
||||
self.formula.push_str(" × ");
|
||||
}
|
||||
}
|
||||
self.formula.push_str(name);
|
||||
self.entry = "0".into();
|
||||
self.typing = false;
|
||||
self.second = false;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn equals(&mut self) {
|
||||
if self.error.is_some() {
|
||||
return;
|
||||
}
|
||||
let mut src = self.formula.clone();
|
||||
let ends_paren = src.trim_end().ends_with(')');
|
||||
if !ends_paren {
|
||||
if !src.is_empty() && !src.ends_with(' ') && !src.ends_with('(') {
|
||||
src.push(' ');
|
||||
}
|
||||
// If formula ends with func( and entry is being typed, append entry then )
|
||||
if src.ends_with('(') || self.typing || src.is_empty() {
|
||||
src.push_str(&self.entry);
|
||||
} else if !src
|
||||
.trim_end()
|
||||
.chars()
|
||||
.last()
|
||||
.is_some_and(|c| c.is_ascii_digit() || c == 'π' || c == 'e' || c == ')')
|
||||
{
|
||||
src.push_str(&self.entry);
|
||||
}
|
||||
} else if src.is_empty() {
|
||||
src = self.entry.clone();
|
||||
}
|
||||
|
||||
let opens = src.chars().filter(|&c| c == '(').count();
|
||||
let closes = src.chars().filter(|&c| c == ')').count();
|
||||
for _ in 0..opens.saturating_sub(closes) {
|
||||
src.push(')');
|
||||
}
|
||||
|
||||
match eval_sci(&src, self.angle) {
|
||||
Ok(v) => {
|
||||
self.expression = format!("{src} =");
|
||||
self.set_result(v);
|
||||
self.formula.clear();
|
||||
self.typing = false;
|
||||
self.done = true;
|
||||
self.second = false;
|
||||
}
|
||||
Err(e) => self.fail(e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memory_clear(&mut self) {
|
||||
self.memory = 0.0;
|
||||
self.memory_set = false;
|
||||
}
|
||||
|
||||
pub fn memory_recall(&mut self) {
|
||||
if self.error.is_some() {
|
||||
self.clear_all();
|
||||
}
|
||||
if self.done {
|
||||
self.formula.clear();
|
||||
self.done = false;
|
||||
}
|
||||
self.entry = format_num(self.memory);
|
||||
self.typing = true;
|
||||
self.sync_expr();
|
||||
}
|
||||
|
||||
pub fn memory_add(&mut self) {
|
||||
if let Ok(v) = self.current() {
|
||||
self.memory += v;
|
||||
self.memory_set = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memory_sub(&mut self) {
|
||||
if let Ok(v) = self.current() {
|
||||
self.memory -= v;
|
||||
self.memory_set = true;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn memory_store(&mut self) {
|
||||
if let Ok(v) = self.current() {
|
||||
self.memory = v;
|
||||
self.memory_set = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn current(&self) -> Result<f64, SciError> {
|
||||
self.entry.parse().map_err(|_| SciError::Invalid)
|
||||
}
|
||||
|
||||
fn begin_entry_if_done(&mut self) {
|
||||
if self.done {
|
||||
self.formula.clear();
|
||||
self.expression.clear();
|
||||
self.done = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn flush_entry(&mut self) {
|
||||
if !self.typing && self.entry == "0" && self.formula.trim_end().ends_with(')') {
|
||||
return;
|
||||
}
|
||||
if !self.typing && self.entry == "0" && self.formula.trim_end().ends_with('π') {
|
||||
return;
|
||||
}
|
||||
if !self.typing && self.entry == "0" && self.formula.trim_end().ends_with('e') {
|
||||
// constant e already there
|
||||
let t = self.formula.trim_end();
|
||||
if t.ends_with('e')
|
||||
&& t.len() > 1
|
||||
&& !t.chars().nth_back(1).unwrap_or(' ').is_ascii_alphanumeric()
|
||||
{
|
||||
return;
|
||||
}
|
||||
if t == "e" || t.ends_with(" e") {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if !self.formula.is_empty()
|
||||
&& !self.formula.ends_with(' ')
|
||||
&& !self.formula.ends_with('(')
|
||||
{
|
||||
self.formula.push(' ');
|
||||
}
|
||||
self.formula.push_str(&self.entry);
|
||||
self.typing = false;
|
||||
}
|
||||
|
||||
fn sync_expr(&mut self) {
|
||||
if self.done {
|
||||
return;
|
||||
}
|
||||
let mut s = self.formula.clone();
|
||||
if self.typing {
|
||||
if !s.is_empty() && !s.ends_with(' ') && !s.ends_with('(') {
|
||||
s.push(' ');
|
||||
}
|
||||
s.push_str(&self.entry);
|
||||
}
|
||||
self.expression = s;
|
||||
}
|
||||
|
||||
fn set_result(&mut self, v: f64) {
|
||||
if !v.is_finite() {
|
||||
self.fail(SciError::Overflow);
|
||||
return;
|
||||
}
|
||||
self.entry = format_num(v);
|
||||
self.error = None;
|
||||
}
|
||||
|
||||
fn fail(&mut self, e: SciError) {
|
||||
self.error = Some(e);
|
||||
self.formula.clear();
|
||||
self.expression.clear();
|
||||
self.typing = false;
|
||||
self.done = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn digit_count(s: &str) -> usize {
|
||||
s.chars().filter(|c| c.is_ascii_digit()).count()
|
||||
}
|
||||
|
||||
fn format_num(v: f64) -> String {
|
||||
if !v.is_finite() {
|
||||
return "0".into();
|
||||
}
|
||||
if (v - v.round()).abs() < 1e-10 && v.abs() < 1e15 {
|
||||
return format!("{}", v.round() as i64);
|
||||
}
|
||||
let s = format!("{:.12}", v);
|
||||
let s = s.trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
if s.is_empty() || s == "-" {
|
||||
"0".into()
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
// --- expression evaluator ---
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Tok {
|
||||
Num(f64),
|
||||
Op(char), // + − × ÷ ^
|
||||
LParen,
|
||||
RParen,
|
||||
Ident(String),
|
||||
}
|
||||
|
||||
fn eval_sci(src: &str, angle: AngleMode) -> Result<f64, SciError> {
|
||||
let tokens = tokenize(src)?;
|
||||
let mut idx = 0;
|
||||
let v = parse_expr(&tokens, &mut idx, angle)?;
|
||||
if idx != tokens.len() {
|
||||
return Err(SciError::Invalid);
|
||||
}
|
||||
if !v.is_finite() {
|
||||
return Err(SciError::Overflow);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
fn tokenize(src: &str) -> Result<Vec<Tok>, SciError> {
|
||||
let chars: Vec<char> = src.chars().collect();
|
||||
let mut i = 0;
|
||||
let mut out = Vec::new();
|
||||
while i < chars.len() {
|
||||
let c = chars[i];
|
||||
if c.is_whitespace() {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
match c {
|
||||
'+' => {
|
||||
out.push(Tok::Op('+'));
|
||||
i += 1;
|
||||
}
|
||||
'−' | '-' => {
|
||||
out.push(Tok::Op('−'));
|
||||
i += 1;
|
||||
}
|
||||
'×' | '*' => {
|
||||
out.push(Tok::Op('×'));
|
||||
i += 1;
|
||||
}
|
||||
'÷' | '/' => {
|
||||
out.push(Tok::Op('÷'));
|
||||
i += 1;
|
||||
}
|
||||
'^' => {
|
||||
out.push(Tok::Op('^'));
|
||||
i += 1;
|
||||
}
|
||||
'(' => {
|
||||
out.push(Tok::LParen);
|
||||
i += 1;
|
||||
}
|
||||
')' => {
|
||||
out.push(Tok::RParen);
|
||||
i += 1;
|
||||
}
|
||||
'π' => {
|
||||
out.push(Tok::Num(PI));
|
||||
i += 1;
|
||||
}
|
||||
'0'..='9' | '.' => {
|
||||
let start = i;
|
||||
i += 1;
|
||||
while i < chars.len() && (chars[i].is_ascii_digit() || chars[i] == '.') {
|
||||
i += 1;
|
||||
}
|
||||
let lit: String = chars[start..i].iter().collect();
|
||||
let v: f64 = lit.parse().map_err(|_| SciError::Invalid)?;
|
||||
out.push(Tok::Num(v));
|
||||
}
|
||||
'a'..='z' | 'A'..='Z' | '_' => {
|
||||
let start = i;
|
||||
i += 1;
|
||||
while i < chars.len() && (chars[i].is_ascii_alphanumeric() || chars[i] == '_') {
|
||||
i += 1;
|
||||
}
|
||||
let name: String = chars[start..i].iter().collect();
|
||||
if name == "e" || name == "E" {
|
||||
out.push(Tok::Num(E));
|
||||
} else if name == "pi" || name == "PI" {
|
||||
out.push(Tok::Num(PI));
|
||||
} else {
|
||||
out.push(Tok::Ident(name.to_ascii_lowercase()));
|
||||
}
|
||||
}
|
||||
_ => return Err(SciError::Invalid),
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_expr(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
|
||||
let mut left = parse_term(tokens, idx, angle)?;
|
||||
while let Some(Tok::Op(op @ ('+' | '−'))) = tokens.get(*idx).cloned() {
|
||||
*idx += 1;
|
||||
let right = parse_term(tokens, idx, angle)?;
|
||||
left = if op == '+' { left + right } else { left - right };
|
||||
}
|
||||
Ok(left)
|
||||
}
|
||||
|
||||
fn parse_term(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
|
||||
let mut left = parse_power(tokens, idx, angle)?;
|
||||
while let Some(Tok::Op(op @ ('×' | '÷'))) = tokens.get(*idx).cloned() {
|
||||
*idx += 1;
|
||||
let right = parse_power(tokens, idx, angle)?;
|
||||
left = if op == '×' {
|
||||
left * right
|
||||
} else {
|
||||
if right == 0.0 {
|
||||
return Err(SciError::DivByZero);
|
||||
}
|
||||
left / right
|
||||
};
|
||||
}
|
||||
Ok(left)
|
||||
}
|
||||
|
||||
fn parse_power(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
|
||||
let base = parse_unary(tokens, idx, angle)?;
|
||||
if let Some(Tok::Op('^')) = tokens.get(*idx).cloned() {
|
||||
*idx += 1;
|
||||
// right-associative
|
||||
let exp = parse_power(tokens, idx, angle)?;
|
||||
let r = base.powf(exp);
|
||||
if !r.is_finite() {
|
||||
return Err(SciError::Overflow);
|
||||
}
|
||||
Ok(r)
|
||||
} else {
|
||||
Ok(base)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_unary(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
|
||||
if let Some(Tok::Op('−')) = tokens.get(*idx).cloned() {
|
||||
*idx += 1;
|
||||
return Ok(-parse_unary(tokens, idx, angle)?);
|
||||
}
|
||||
if let Some(Tok::Op('+')) = tokens.get(*idx).cloned() {
|
||||
*idx += 1;
|
||||
return parse_unary(tokens, idx, angle);
|
||||
}
|
||||
if let Some(Tok::Ident(name)) = tokens.get(*idx).cloned() {
|
||||
*idx += 1;
|
||||
// function call: name (
|
||||
match tokens.get(*idx) {
|
||||
Some(Tok::LParen) => {
|
||||
*idx += 1;
|
||||
let arg = parse_expr(tokens, idx, angle)?;
|
||||
match tokens.get(*idx) {
|
||||
Some(Tok::RParen) => *idx += 1,
|
||||
_ => return Err(SciError::Invalid),
|
||||
}
|
||||
apply_func(&name, arg, angle)
|
||||
}
|
||||
_ => Err(SciError::Invalid),
|
||||
}
|
||||
} else {
|
||||
parse_primary(tokens, idx, angle)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_primary(tokens: &[Tok], idx: &mut usize, angle: AngleMode) -> Result<f64, SciError> {
|
||||
match tokens.get(*idx).cloned() {
|
||||
Some(Tok::Num(v)) => {
|
||||
*idx += 1;
|
||||
Ok(v)
|
||||
}
|
||||
Some(Tok::LParen) => {
|
||||
*idx += 1;
|
||||
let v = parse_expr(tokens, idx, angle)?;
|
||||
match tokens.get(*idx) {
|
||||
Some(Tok::RParen) => {
|
||||
*idx += 1;
|
||||
Ok(v)
|
||||
}
|
||||
_ => Err(SciError::Invalid),
|
||||
}
|
||||
}
|
||||
_ => Err(SciError::Invalid),
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_func(name: &str, x: f64, angle: AngleMode) -> Result<f64, SciError> {
|
||||
let r = match name {
|
||||
"sin" => angle.to_rad(x).sin(),
|
||||
"cos" => angle.to_rad(x).cos(),
|
||||
"tan" => angle.to_rad(x).tan(),
|
||||
"asin" => {
|
||||
if !(-1.0..=1.0).contains(&x) {
|
||||
return Err(SciError::Domain);
|
||||
}
|
||||
angle.from_rad(x.asin())
|
||||
}
|
||||
"acos" => {
|
||||
if !(-1.0..=1.0).contains(&x) {
|
||||
return Err(SciError::Domain);
|
||||
}
|
||||
angle.from_rad(x.acos())
|
||||
}
|
||||
"atan" => angle.from_rad(x.atan()),
|
||||
"sinh" => x.sinh(),
|
||||
"cosh" => x.cosh(),
|
||||
"tanh" => x.tanh(),
|
||||
"ln" => {
|
||||
if x <= 0.0 {
|
||||
return Err(SciError::Domain);
|
||||
}
|
||||
x.ln()
|
||||
}
|
||||
"log" | "log10" => {
|
||||
if x <= 0.0 {
|
||||
return Err(SciError::Domain);
|
||||
}
|
||||
x.log10()
|
||||
}
|
||||
"exp" => x.exp(),
|
||||
"sqrt" => {
|
||||
if x < 0.0 {
|
||||
return Err(SciError::Domain);
|
||||
}
|
||||
x.sqrt()
|
||||
}
|
||||
"cbrt" => x.cbrt(),
|
||||
"sq" | "sqr" => x * x,
|
||||
"cube" => x * x * x,
|
||||
"inv" => {
|
||||
if x == 0.0 {
|
||||
return Err(SciError::DivByZero);
|
||||
}
|
||||
1.0 / x
|
||||
}
|
||||
"abs" => x.abs(),
|
||||
"fact" | "factorial" => factorial(x)?,
|
||||
"tenpow" | "pow10" => 10f64.powf(x),
|
||||
_ => return Err(SciError::Invalid),
|
||||
};
|
||||
if !r.is_finite() {
|
||||
Err(SciError::Overflow)
|
||||
} else {
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
|
||||
fn factorial(x: f64) -> Result<f64, SciError> {
|
||||
if x < 0.0 || x != x.floor() || x > 170.0 {
|
||||
return Err(SciError::Domain);
|
||||
}
|
||||
let n = x as u32;
|
||||
let mut r = 1.0;
|
||||
for i in 2..=n {
|
||||
r *= f64::from(i);
|
||||
}
|
||||
if !r.is_finite() {
|
||||
Err(SciError::Overflow)
|
||||
} else {
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sin_90_deg() {
|
||||
let mut e = Engineering::new();
|
||||
e.insert_func("sin");
|
||||
e.input_digit('9');
|
||||
e.input_digit('0');
|
||||
e.equals();
|
||||
assert_eq!(e.display(), "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn power() {
|
||||
let mut e = Engineering::new();
|
||||
e.input_digit('2');
|
||||
e.set_op("^");
|
||||
e.input_digit('1');
|
||||
e.input_digit('0');
|
||||
e.equals();
|
||||
assert_eq!(e.display(), "1024");
|
||||
}
|
||||
}
|
||||
+160
-9
@@ -4,6 +4,7 @@
|
||||
mod convert;
|
||||
mod crc;
|
||||
mod engine;
|
||||
mod engineering;
|
||||
mod programmer;
|
||||
|
||||
use std::cell::RefCell;
|
||||
@@ -12,6 +13,7 @@ use std::rc::Rc;
|
||||
use convert::{ConvField, ConvPanel, Converter, Endian, RatioWidth};
|
||||
use crc::CrcTool;
|
||||
use engine::{Calculator, Op, StdPanel};
|
||||
use engineering::Engineering;
|
||||
use programmer::{Base, ProgOp, Programmer};
|
||||
use slint::{ComponentHandle, ModelRc, VecModel};
|
||||
|
||||
@@ -20,6 +22,7 @@ slint::include_modules!();
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Standard,
|
||||
Engineering,
|
||||
Programmer,
|
||||
Convert,
|
||||
Crc,
|
||||
@@ -28,6 +31,7 @@ enum Mode {
|
||||
struct State {
|
||||
mode: Mode,
|
||||
std: Calculator,
|
||||
eng: Engineering,
|
||||
prog: Programmer,
|
||||
conv: Converter,
|
||||
crc: CrcTool,
|
||||
@@ -40,6 +44,7 @@ impl Default for State {
|
||||
Self {
|
||||
mode: Mode::Standard,
|
||||
std: Calculator::new(),
|
||||
eng: Engineering::new(),
|
||||
prog: Programmer::new(),
|
||||
conv: Converter::new(),
|
||||
crc: CrcTool::new(),
|
||||
@@ -49,12 +54,34 @@ impl Default for State {
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), slint::PlatformError> {
|
||||
fn main() {
|
||||
install_panic_hook();
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// OpenGL/femtovg often fails on remote desktop / old drivers; software is portable.
|
||||
if std::env::var_os("SLINT_BACKEND").is_none() {
|
||||
// SAFETY: single-threaded init before any other threads.
|
||||
unsafe { std::env::set_var("SLINT_BACKEND", "winit-software") };
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(e) = run_app() {
|
||||
let msg = format!("rcalc failed to start:\n{e}\n\nTip: try setting SLINT_BACKEND=winit-software");
|
||||
log_startup_error(&msg);
|
||||
#[cfg(windows)]
|
||||
windows_error_dialog(&msg);
|
||||
eprintln!("{msg}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn run_app() -> Result<(), slint::PlatformError> {
|
||||
// Platform must exist first — calling set_xdg_app_id before AppWindow::new
|
||||
// returns NoPlatform and the Wayland app_id stays unset (KDE shows the "W" icon).
|
||||
let ui = AppWindow::new()?;
|
||||
// Must match the .desktop basename (rcalc.desktop → "rcalc"), before show/run.
|
||||
slint::set_xdg_app_id("rcalc")?;
|
||||
let _ = slint::set_xdg_app_id("rcalc");
|
||||
ui.set_app_title(format!("Rcalc-{}", env!("APP_VERSION")).into());
|
||||
|
||||
let state = Rc::new(RefCell::new(State::default()));
|
||||
@@ -84,6 +111,56 @@ fn main() -> Result<(), slint::PlatformError> {
|
||||
ui.run()
|
||||
}
|
||||
|
||||
fn install_panic_hook() {
|
||||
let default = std::panic::take_hook();
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let msg = format!("rcalc panic:\n{info}");
|
||||
log_startup_error(&msg);
|
||||
#[cfg(windows)]
|
||||
windows_error_dialog(&msg);
|
||||
default(info);
|
||||
}));
|
||||
}
|
||||
|
||||
fn log_startup_error(msg: &str) {
|
||||
let path = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.parent().map(|d| d.join("rcalc-crash.log")))
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("rcalc-crash.log"));
|
||||
let _ = std::fs::write(&path, msg);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn windows_error_dialog(msg: &str) {
|
||||
use std::ffi::OsStr;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
fn wide(s: &str) -> Vec<u16> {
|
||||
OsStr::new(s).encode_wide().chain(std::iter::once(0)).collect()
|
||||
}
|
||||
|
||||
#[link(name = "user32")]
|
||||
unsafe extern "system" {
|
||||
fn MessageBoxW(
|
||||
hwnd: *mut core::ffi::c_void,
|
||||
text: *const u16,
|
||||
caption: *const u16,
|
||||
flags: u32,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
let text = wide(msg);
|
||||
let caption = wide("Rcalc");
|
||||
unsafe {
|
||||
MessageBoxW(
|
||||
std::ptr::null_mut(),
|
||||
text.as_ptr(),
|
||||
caption.as_ptr(),
|
||||
0x10, // MB_ICONERROR
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn bit_row_model(row: &[(bool, u32)]) -> ModelRc<BitInfo> {
|
||||
let items: Vec<BitInfo> = row
|
||||
.iter()
|
||||
@@ -98,9 +175,10 @@ fn bit_row_model(row: &[(bool, u32)]) -> ModelRc<BitInfo> {
|
||||
fn window_profile(state: &State) -> u32 {
|
||||
let mode = match state.mode {
|
||||
Mode::Standard => 0,
|
||||
Mode::Programmer => 1,
|
||||
Mode::Convert => 2,
|
||||
Mode::Crc => 3,
|
||||
Mode::Engineering => 1,
|
||||
Mode::Programmer => 2,
|
||||
Mode::Convert => 3,
|
||||
Mode::Crc => 4,
|
||||
};
|
||||
let bits = if state.show_bits {
|
||||
state.prog.word_size().bits()
|
||||
@@ -123,7 +201,8 @@ fn fit_window(ui: &AppWindow, state: &mut State) {
|
||||
state.win_profile = profile;
|
||||
|
||||
let (w, h) = match state.mode {
|
||||
Mode::Standard => (340.0, 560.0),
|
||||
Mode::Standard => (360.0, 580.0),
|
||||
Mode::Engineering => (420.0, 720.0),
|
||||
Mode::Programmer => {
|
||||
let bit_h = if state.show_bits {
|
||||
match state.prog.word_size().bits() {
|
||||
@@ -159,10 +238,22 @@ fn apply_crc_results(ui: &AppWindow, state: &State) {
|
||||
fn refresh(ui: &AppWindow, state: &mut State) {
|
||||
ui.set_mode(match state.mode {
|
||||
Mode::Standard => 0,
|
||||
Mode::Programmer => 1,
|
||||
Mode::Convert => 2,
|
||||
Mode::Crc => 3,
|
||||
Mode::Engineering => 1,
|
||||
Mode::Programmer => 2,
|
||||
Mode::Convert => 3,
|
||||
Mode::Crc => 4,
|
||||
});
|
||||
ui.set_mode_title(
|
||||
match state.mode {
|
||||
Mode::Standard => "Standard",
|
||||
Mode::Engineering => "Engineering",
|
||||
Mode::Programmer => "Programmer",
|
||||
Mode::Convert => "Convert",
|
||||
Mode::Crc => "CRC",
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
ui.set_menu_open(false);
|
||||
|
||||
fit_window(ui, state);
|
||||
|
||||
@@ -176,6 +267,13 @@ fn refresh(ui: &AppWindow, state: &mut State) {
|
||||
StdPanel::Formula => 1,
|
||||
});
|
||||
}
|
||||
Mode::Engineering => {
|
||||
ui.set_display_text(state.eng.display().into());
|
||||
ui.set_expression_text(state.eng.expression().into());
|
||||
ui.set_has_memory(state.eng.has_memory());
|
||||
ui.set_eng_angle_label(state.eng.angle_mode().label().into());
|
||||
ui.set_eng_second(state.eng.second());
|
||||
}
|
||||
Mode::Programmer => {
|
||||
ui.set_display_text(state.prog.display().into());
|
||||
ui.set_expression_text(state.prog.expression().into());
|
||||
@@ -246,6 +344,10 @@ fn handle_key(state: &mut State, id: &str) {
|
||||
state.mode = Mode::Standard;
|
||||
return;
|
||||
}
|
||||
"mode:eng" => {
|
||||
state.mode = Mode::Engineering;
|
||||
return;
|
||||
}
|
||||
"mode:prog" => {
|
||||
state.mode = Mode::Programmer;
|
||||
return;
|
||||
@@ -263,12 +365,61 @@ fn handle_key(state: &mut State, id: &str) {
|
||||
|
||||
match state.mode {
|
||||
Mode::Standard => handle_standard(&mut state.std, id),
|
||||
Mode::Engineering => handle_engineering(&mut state.eng, id),
|
||||
Mode::Programmer => handle_programmer(state, id),
|
||||
Mode::Convert => handle_convert(&mut state.conv, id),
|
||||
Mode::Crc => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_engineering(calc: &mut Engineering, id: &str) {
|
||||
match id {
|
||||
"0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" => {
|
||||
calc.input_digit(id.chars().next().unwrap());
|
||||
}
|
||||
"." | "," => calc.input_dot(),
|
||||
"+" => calc.set_op("+"),
|
||||
"-" | "−" => calc.set_op("−"),
|
||||
"*" | "×" => calc.set_op("×"),
|
||||
"/" | "÷" => calc.set_op("÷"),
|
||||
"^" => calc.set_op("^"),
|
||||
"=" => calc.equals(),
|
||||
"(" => calc.paren_open(),
|
||||
")" => calc.paren_close(),
|
||||
"CE" => calc.clear_entry(),
|
||||
"C" | "clear" => calc.clear_all(),
|
||||
"BS" => calc.backspace(),
|
||||
"neg" => calc.negate(),
|
||||
"eng:angle" => calc.cycle_angle(),
|
||||
"eng:2nd" => calc.toggle_second(),
|
||||
"eng:pi" => calc.insert_const("π"),
|
||||
"eng:e" => calc.insert_const("e"),
|
||||
"eng:sin" => calc.insert_func("sin"),
|
||||
"eng:cos" => calc.insert_func("cos"),
|
||||
"eng:tan" => calc.insert_func("tan"),
|
||||
"eng:asin" => calc.insert_func("asin"),
|
||||
"eng:acos" => calc.insert_func("acos"),
|
||||
"eng:atan" => calc.insert_func("atan"),
|
||||
"eng:sqrt" => calc.insert_func("sqrt"),
|
||||
"eng:cbrt" => calc.insert_func("cbrt"),
|
||||
"eng:sq" => calc.insert_func("sq"),
|
||||
"eng:cube" => calc.insert_func("cube"),
|
||||
"eng:inv" => calc.insert_func("inv"),
|
||||
"eng:abs" => calc.insert_func("abs"),
|
||||
"eng:fact" => calc.insert_func("fact"),
|
||||
"eng:exp" => calc.insert_func("exp"),
|
||||
"eng:tenpow" => calc.insert_func("tenpow"),
|
||||
"eng:log" => calc.insert_func("log"),
|
||||
"eng:ln" => calc.insert_func("ln"),
|
||||
"MC" => calc.memory_clear(),
|
||||
"MR" => calc.memory_recall(),
|
||||
"M+" => calc.memory_add(),
|
||||
"M-" => calc.memory_sub(),
|
||||
"MS" => calc.memory_store(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_standard(calc: &mut Calculator, id: &str) {
|
||||
match id {
|
||||
"panel:std" => calc.set_panel(StdPanel::Standard),
|
||||
|
||||
Reference in New Issue
Block a user