Merge branch 'refactor'

This commit is contained in:
Valerie Wolfe 2024-08-29 14:00:49 -04:00
commit 21b373af0e
8 changed files with 302 additions and 240 deletions

View file

@ -1,6 +1,6 @@
[package] [package]
name = "quickmath" name = "quickmath"
version = "0.2.4" version = "0.3.1"
edition = "2021" edition = "2021"
authors = [ "Valerie Wolfe <sleeplessval@gmail.com>" ] authors = [ "Valerie Wolfe <sleeplessval@gmail.com>" ]
description = "A quick command-line math evaluator." description = "A quick command-line math evaluator."
@ -14,6 +14,8 @@ path = "src/main.rs"
[dependencies] [dependencies]
evalexpr = "11.0.0" evalexpr = "11.0.0"
pico-args = "0.5.0"
rustyline = "14.0.0"
termion = "1.5.6" termion = "1.5.6"
[profile.release] [profile.release]

140
src/context/helper.rs Normal file
View file

@ -0,0 +1,140 @@
use evalexpr::{
Value,
EvalexprError
};
use crate::util;
pub type EvalResult = Result<Value, EvalexprError>;
// Mathematics
pub fn cosine(arg: &Value) -> EvalResult {
Ok(
if let Value::Float(float) = arg { float.clone() }
else if let Value::Int(int) = arg { int.clone() as f64 }
else { return Err(EvalexprError::expected_number(arg.clone())) }
.cos().into()
)
}
pub fn fix(arg: &Value) -> EvalResult {
if let Value::Tuple(args) = arg {
let len = args.len();
if len == 2 {
let value =
if let Value::Float(float) = args[0] { float.clone() }
else { return Err(EvalexprError::expected_float(args[0].clone())); };
let operand = 10u64.pow(
if let Value::Int(int) = args[1] { int.clone() }
else { return Err(EvalexprError::expected_int(args[1].clone())); }
as u32
) as f64;
Ok( ((value * operand).round() / operand).into() )
} else { Err(EvalexprError::wrong_function_argument_amount(len, 2)) }
} else { Err(EvalexprError::wrong_function_argument_amount(1, 2)) }
}
pub fn logarithm(arg: &Value) -> EvalResult {
let value: f64;
let base: Option<f64>;
match arg {
Value::Tuple(tuple)
=> {
let len = tuple.len();
if len != 2 { return Err(EvalexprError::WrongOperatorArgumentAmount { expected: 2, actual: len }) }
let i_value = tuple.get(0).unwrap();
if let Value::Float(float) = i_value { value = float.clone(); }
else if let Value::Int(int) = i_value { value = int.clone() as f64; }
else { return Err(EvalexprError::expected_number(i_value.clone())); }
let i_base = tuple.get(1).unwrap();
if let Value::Float(float) = i_value { base = Some(float.clone()); }
else if let Value::Int(int) = i_value { base = Some(int.clone() as f64); }
else { return Err(EvalexprError::expected_number(i_base.clone())); }
},
Value::Float(float)
=> {
value = float.clone();
base = None;
},
Value::Int(int)
=> {
value = int.clone() as f64;
base = None;
}
_ => return Err(EvalexprError::CustomMessage("Expected numbers.".to_owned()))
}
if let Some(base) = base { Ok(value.log(base).into()) }
else { Ok(value.ln().into()) }
}
pub fn sine(arg: &Value) -> EvalResult {
Ok(
if let Value::Float(float) = arg { float.clone() }
else if let Value::Int(int) = arg { int.clone() as f64 }
else { return Err(EvalexprError::expected_number(arg.clone())) }
.sin().into()
)
}
pub fn square_root(arg: &Value) -> EvalResult {
Ok(
if let Value::Float(float) = arg { float.clone() }
else if let Value::Int(int) = arg { int.clone() as f64 }
else { return Err(EvalexprError::expected_number(arg.clone())) }
.sqrt().into()
)
}
pub fn tangent(arg: &Value) -> EvalResult {
Ok(
if let Value::Float(float) = arg { float.clone() }
else if let Value::Int(int) = arg { int.clone() as f64 }
else { return Err(EvalexprError::expected_number(arg.clone())) }
.tan().into()
)
}
// Data Science
pub fn average(arg: &Value) -> EvalResult {
if let Value::Tuple(args) = arg {
let len = args.len() as f64;
let mut total = 0f64;
for arg in args {
total +=
if let Value::Float(float) = arg { float.clone() }
else if let Value::Int(int) = arg { int.clone() as f64 }
else { todo!() };
}
Ok( (total / len).into() )
} else { todo!() }
}
// Radix conversion
pub fn binary(arg: &Value) -> EvalResult {
if let Value::Int(int) = arg { Ok( format!("0b{:b}", int).into() ) }
else if let Value::String(string) = arg { util::parse_radix("0b", 2, string) }
else { Err(EvalexprError::expected_number_or_string(arg.clone())) }
}
pub fn hexadecimal(arg: &Value) -> EvalResult {
if let Value::Int(int) = arg { Ok( format!("0x{:X}", int).into() ) }
else if let Value::String(string) = arg { util::parse_radix("0x", 16, string) }
else { Err(EvalexprError::expected_number_or_string(arg.clone())) }
}
pub fn octal(arg: &Value) -> EvalResult {
if let Value::Int(int) = arg { Ok( format!("0o{:#o}", int).into() ) }
else if let Value::String(string) = arg { util::parse_radix("0o", 8, string) }
else { Err(EvalexprError::expected_number_or_string(arg.clone())) }
}

68
src/context/mod.rs Normal file
View file

@ -0,0 +1,68 @@
pub mod global;
pub mod helper;
use pico_args::Arguments;
use evalexpr::{
context_map,
ContextWithMutableVariables,
HashMapContext,
Value
};
use crate::flag;
pub fn build(args: &mut Arguments) -> HashMapContext {
let mut output =
if !args.contains(flag::EMPTY_CONTEXT) {
context_map! {
// globals
"c" => global::LIGHT_SPEED,
"e" => global::EULER,
"phi" => global::GOLDEN_RATIO,
"pi" => global::PI,
"√2" => global::ROOT_TWO,
// math functions
"cos" => Function::new(|arg| helper::cosine(arg)),
"fix" => Function::new(|arg| helper::fix(arg)),
"log" => Function::new(|arg| helper::logarithm(arg)),
"sin" => Function::new(|arg| helper::sine(arg)),
"sqrt" => Function::new(|arg| helper::square_root(arg)),
"tan" => Function::new(|arg| helper::tangent(arg)),
// data science functions
"avg" => Function::new(|arg| helper::average(arg)),
// radix functions
"bin" => Function::new(|arg| helper::binary(arg)),
"hex" => Function::new(|arg| helper::hexadecimal(arg)),
"oct" => Function::new(|arg| helper::octal(arg)),
// character aliases
"ϕ" => global::GOLDEN_RATIO,
"π" => global::PI,
"" => Function::new(|arg| helper::square_root(arg))
}.unwrap()
} else { HashMapContext::new() };
while let Ok(value) = args.value_from_str::<&str, String>(flag::SET) {
let split: Vec<&str> = value.split('=').collect();
if split.len() == 2 {
let key = split[0].to_owned();
let value_str = split[1];
let value =
if let Ok(integer) = value_str.parse::<i64>() { Value::Int(integer) }
else if let Ok(float) = value_str.parse::<f64>() { Value::Float(float) }
else { Value::from(value_str) };
output.set_value(key, value).ok(); }
else { std::process::exit(1); }
}
output
}

7
src/flag.rs Normal file
View file

@ -0,0 +1,7 @@
pub const EMPTY_CONTEXT: [&str;2] = [ "-E", "--empty"];
pub const HELP: [&str;2] = [ "-h", "--help" ];
pub const QUIET: [&str;2] = [ "-q", "--quiet" ];
pub const SET: &str = "--set";
pub const VERSION: [&str;2] = [ "-V", "--version" ];

View file

@ -1,147 +0,0 @@
use evalexpr::{
Value,
EvalexprError
};
use crate::util;
pub type EvalResult = Result<Value, EvalexprError>;
// Mathematics
pub fn fix(arg: &Value) -> EvalResult {
let args = arg.as_tuple()?;
let count = args.len();
if count != 2 {
return Err(EvalexprError::WrongFunctionArgumentAmount { expected: 2, actual: count });
}
let float = args[0].as_float()?;
let figures = args[1].as_int()?;
let operand: f64 = i64::pow(10, figures as u32) as f64;
let output = f64::round(float * operand) / operand;
return Ok(output.into());
}
pub fn logarithm(arg: &Value) -> EvalResult {
let arguments: Vec<Value>;
let count: usize;
if arg.is_tuple() {
arguments = arg.as_tuple()?;
count = arguments.len();
} else if arg.is_float() {
arguments = vec!(arg.as_float()?.into());
count = 1;
} else if arg.is_int() {
arguments = vec!((arg.as_int()? as f64).into());
count = 1;
} else {
return Err(EvalexprError::CustomMessage("Expected numbers".to_string()));
}
let output: Value;
match count {
1 => {
let argument = &arguments[0];
if !argument.is_number() {
return Err(EvalexprError::CustomMessage("Expected number".to_string()));
}
let number = if argument.is_float() { argument.as_float()? } else { argument.as_int()? as f64 };
output = number.ln().into();
},
2 => {
let arg_value = &arguments[0];
let arg_base = &arguments[1];
if !(arg_value.is_number() && arg_base.is_number()) {
return Err(EvalexprError::CustomMessage("Expected two numbers".to_string()));
}
let value: f64 = if arg_value.is_float() { arg_value.as_float()? } else { arg_value.as_int()? as f64 };
let base: f64 = if arg_base.is_float() { arg_base.as_float()? } else { arg_base.as_int()? as f64 };
output = value.log(base).into();
},
_ => {
return Err(EvalexprError::WrongFunctionArgumentAmount { expected: 2, actual: count });
}
}
return Ok(output);
}
pub fn square_root(arg: &Value) -> EvalResult {
if !arg.is_number() {
return Err(EvalexprError::CustomMessage("Expected a number.".to_string()));
}
let value: f64 = if arg.is_float() { arg.as_float()? } else { arg.as_int()? as f64 };
return Ok(value.sqrt().into());
}
// Data Science
pub fn average(arg: &Value) -> EvalResult {
let arguments = arg.as_tuple()?;
let count = arguments.len() as i64;
let mut is_float = false;
let mut total_i = 0i64;
let mut total_f = 0f64;
for argument in arguments {
if let Value::Float(float) = argument {
if !is_float {
total_f = total_i as f64;
is_float = true;
}
total_f += float;
} else if let Value::Int(int) = argument {
if is_float {
total_f += int as f64;
} else {
total_i += int;
}
}
}
let result_i: i64;
let result_f: f64;
if !is_float {
is_float = total_i % count == 0;
total_f = total_i as f64;
}
if is_float {
result_f = total_f / (count as f64);
return Ok(result_f.into());
} else {
result_i = total_i / count;
return Ok(result_i.into());
}
}
// Radix conversion
pub fn binary(arg: &Value) -> EvalResult {
if !arg.is_string() {
let num = arg.as_int()?;
let fmt = format!("0b{:b}", num);
return Ok(fmt.into());
}
util::parse_radix("0b", 2, arg)
}
pub fn hexadecimal(arg: &Value) -> EvalResult {
if !arg.is_string() {
let num = arg.as_int()?;
let fmt = format!("0x{:X}", num);
return Ok(fmt.into());
}
util::parse_radix("0x", 16, arg)
}
pub fn octal(arg: &Value) -> EvalResult {
if !arg.is_string() {
let num = arg.as_int()?;
let fmt = format!("{:#o}", num);
return Ok(fmt.into());
}
util::parse_radix("0o", 8, arg)
}

View file

@ -4,109 +4,104 @@ use std::{
stdin, stdin,
stdout, stdout,
IsTerminal,
Read,
Write Write
} },
process::exit
}; };
use evalexpr::{ use evalexpr::{
context_map,
eval_with_context_mut, eval_with_context_mut,
ContextWithMutableVariables, ContextWithMutableVariables,
EvalexprError,
HashMapContext, HashMapContext,
Value Value
}; };
use pico_args::Arguments;
use rustyline::DefaultEditor;
use termion::{ use termion::{
color, color,
style style
}; };
mod global; mod context;
mod helper; mod flag;
mod util; mod util;
pub const VERSION: &str = env!("CARGO_PKG_VERSION"); pub const VERSION: &str = env!("CARGO_PKG_VERSION");
fn main() { fn main() {
// build eval context let mut args = Arguments::from_env();
let mut context = context_map! {
// globals
"c" => global::LIGHT_SPEED,
"e" => global::EULER,
"phi" => global::GOLDEN_RATIO,
"pi" => global::PI,
"√2" => global::ROOT_TWO,
// math functions // handle breaking flags
"fix" => Function::new(|arg| helper::fix(arg)), if args.contains(flag::HELP) {
"log" => Function::new(|arg| helper::logarithm(arg)), help_text();
"sqrt" => Function::new(|arg| helper::square_root(arg)), return;
}
if args.contains(flag::VERSION) {
version_text();
return;
}
// data science functions let mut context = context::build(&mut args);
"avg" => Function::new(|arg| helper::average(arg)), let is_terminal = stdin().is_terminal() && stdout().is_terminal();
let quiet = args.contains(flag::QUIET) || !is_terminal;
// radix functions
"bin" => Function::new(|arg| helper::binary(arg)),
"hex" => Function::new(|arg| helper::hexadecimal(arg)),
"oct" => Function::new(|arg| helper::octal(arg)),
// character aliases
"ϕ" => global::GOLDEN_RATIO,
"π" => global::PI,
"" => Function::new(|arg| helper::square_root(arg))
}.unwrap();
// collect args and evaluate if present // collect args and evaluate if present
let expressions: Vec<String> = env::args().skip(1).collect(); let expressions = args.finish();
if expressions.len() > 0 { if expressions.len() > 0 {
for expression in expressions { for expression in expressions {
match expression.as_str() { let expression: String = expression.to_string_lossy().into();
"help" => help_text(), eval(&expression, &mut context, quiet);
_ => do_eval(expression, &mut context)
}
} }
} else { } else {
// enter interactive mode if no args are given if !is_terminal {
println!("{}quickmaths v{}{}\n{}Interactive Mode{}", style::Bold, VERSION, style::Reset, style::Faint, style::Reset); let mut buffer = String::with_capacity(0);
loop { if let Ok(_) = stdin().read_to_string(&mut buffer) {
print!("> "); for expression in buffer.lines() {
stdout().flush().unwrap(); eval(&expression.to_string(), &mut context, quiet);
let mut i_line = String::new();
let line_result = stdin().read_line(&mut i_line);
if line_result.is_err() {
break;
} }
let line = i_line.trim().to_string(); }
} else if let Ok(mut rl) = DefaultEditor::new() {
// enter interactive mode if no args are given
version_text();
println!("{}Interactive Mode{}", style::Faint, style::Reset);
loop {
if let Ok(line) = rl.readline("> ") {
match line.as_str() { match line.as_str() {
"" | "" |
"exit" => break, "exit" => break,
_ => do_eval(line, &mut context) _ => eval(&line, &mut context, quiet)
}
} }
reset(); reset();
} }
} }
} }
}
fn do_eval(i_expression: String, context: &mut HashMapContext) { fn eval(expression: &str, context: &mut HashMapContext, quiet: bool) {
let expression = i_expression.as_str();
let result = eval_with_context_mut(expression, context); let result = eval_with_context_mut(expression, context);
if let Ok(result) = result {
// display empty result if let Ok(ref result) = result { context.set_value("$".to_owned(), result.clone()).ok(); }
if result.is_empty() {
println!( if quiet {
"{green}✓ {bold}{expression}", if let Ok(result) = result { println!("{result}") }
bold = style::Bold, else { exit(1) }
green = color::Fg(color::Green) } else { format(expression, result) }
);
return;
} }
// get appropriate symbol for result by type fn format(expression: &str, result: Result<Value, EvalexprError>) {
let delimiter; if let Ok(result) = result {
if !result.is_empty() {
let delimiter =
match result { match result {
Value::Boolean(_bool) => delimiter = "is", Value::Boolean(_) => "is",
Value::String(ref _str) => delimiter = "=>", Value::String(_) => "=>",
_ => delimiter = "=" _ => "="
} };
println!( println!(
"{faint}{italic}{expression}{reset} {delimiter} {bold}{result}", "{faint}{italic}{expression}{reset} {delimiter} {bold}{result}",
bold = style::Bold, bold = style::Bold,
@ -114,9 +109,13 @@ fn do_eval(i_expression: String, context: &mut HashMapContext) {
italic = style::Italic, italic = style::Italic,
reset = style::Reset reset = style::Reset
); );
} else {
// set "last" variable println!(
context.set_value("$".to_string(), result).ok(); "{green}✓ {bold}{expression}",
bold = style::Bold,
green = color::Fg(color::Green)
);
}
} else { } else {
println!( println!(
"{red}✕ {bold}{expression}", "{red}✕ {bold}{expression}",
@ -131,16 +130,17 @@ fn reset() {
stdout().flush().unwrap(); stdout().flush().unwrap();
} }
fn version_text() {
println!("qm v{VERSION}");
}
fn help_text() { fn help_text() {
version_text();
println!( println!(
"{bold}quickmaths v{version}{reset} "Valerie Wolfe <sleeplessval@gmail.com>
Valerie Wolfe <sleeplessval@gmail.com>
A mathematical expression evaluator written in Rust. A mathematical expression evaluator written in Rust.
usage: usage:
qm [EXPRESSION]...", qm [EXPRESSION]..."
bold = style::Bold,
reset = style::Reset,
version = crate::VERSION
); );
} }

View file

@ -1,18 +1,10 @@
use evalexpr::{
Value,
EvalexprError use evalexpr::{ EvalexprError, Value };
};
pub(crate) fn parse_radix(prefix: &str, base: u32, arg: &Value) -> Result<Value, EvalexprError> { pub fn parse_radix(prefix: &str, base: u32, arg: &str) -> Result<Value, EvalexprError> {
let i_parse = arg.as_string()?; let parse = arg.strip_prefix(prefix).unwrap_or(arg);
let parse = i_parse.strip_prefix(prefix).unwrap_or(i_parse.as_str());
let i_result = i64::from_str_radix(parse, base); if let Ok(int) = i64::from_str_radix(parse, base) { Ok(int.into()) }
if i_result.is_err() { else { Err(EvalexprError::CustomMessage("failed to parse integer from string".to_string())) }
return Err(EvalexprError::CustomMessage("failed to parse integer from string".to_string()));
} }
let result = i_result.ok();
return Ok(result.unwrap().into());
}