#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![deny(clippy::use_self)]
#![forbid(clippy::needless_borrow)]
#![forbid(unreachable_pub)]
#![forbid(elided_lifetimes_in_paths)]
#![allow(clippy::tabs_in_doc_comments)]
mod ast;
mod date;
mod error;
mod eval;
mod format;
mod ident;
mod inline_substitutions;
mod interrupt;
pub mod json;
mod lexer;
mod num;
mod parser;
mod result;
mod scope;
mod serialize;
mod units;
mod value;
use std::sync::Arc;
use std::{collections::HashMap, fmt, io};
use error::FendError;
pub(crate) use eval::Attrs;
pub use interrupt::Interrupt;
use result::FResult;
use serialize::{Deserialize, Serialize};
#[derive(PartialEq, Eq, Debug)]
pub struct FendResult {
plain_result: String,
span_result: Vec<Span>,
is_unit: bool, attrs: eval::Attrs,
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[non_exhaustive]
pub enum SpanKind {
Number,
BuiltInFunction,
Keyword,
String,
Date,
Whitespace,
Ident,
Boolean,
Other,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct Span {
string: String,
kind: SpanKind,
}
impl Span {
fn from_string(s: String) -> Self {
Self {
string: s,
kind: SpanKind::Other,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SpanRef<'a> {
string: &'a str,
kind: SpanKind,
}
impl<'a> SpanRef<'a> {
#[must_use]
pub fn kind(self) -> SpanKind {
self.kind
}
#[must_use]
pub fn string(self) -> &'a str {
self.string
}
}
impl FendResult {
#[must_use]
pub fn get_main_result(&self) -> &str {
self.plain_result.as_str()
}
pub fn get_main_result_spans(&self) -> impl Iterator<Item = SpanRef<'_>> {
self.span_result.iter().map(|span| SpanRef {
string: &span.string,
kind: span.kind,
})
}
#[must_use]
pub fn is_unit_type(&self) -> bool {
self.is_unit
}
fn empty() -> Self {
Self {
plain_result: String::new(),
span_result: vec![],
is_unit: true,
attrs: Attrs::default(),
}
}
#[must_use]
pub fn has_trailing_newline(&self) -> bool {
self.attrs.trailing_newline
}
}
#[derive(Clone, Debug)]
struct CurrentTimeInfo {
elapsed_unix_time_ms: u64,
timezone_offset_secs: i64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum FCMode {
CelsiusFahrenheit,
CoulombFarad,
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum OutputMode {
SimpleText,
TerminalFixedWidth,
}
pub trait ExchangeRateFn {
fn relative_to_base_currency(
&self,
currency: &str,
) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>;
}
impl<T> ExchangeRateFn for T
where
T: Fn(&str) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>,
{
fn relative_to_base_currency(
&self,
currency: &str,
) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>> {
self(currency)
}
}
#[derive(Clone)]
pub struct Context {
current_time: Option<CurrentTimeInfo>,
variables: HashMap<String, value::Value>,
fc_mode: FCMode,
random_u32: Option<fn() -> u32>,
output_mode: OutputMode,
get_exchange_rate: Option<Arc<dyn ExchangeRateFn + Send + Sync>>,
custom_units: Vec<(String, String, String)>,
}
impl fmt::Debug for Context {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context")
.field("current_time", &self.current_time)
.field("variables", &self.variables)
.field("fc_mode", &self.fc_mode)
.field("random_u32", &self.random_u32)
.field("output_mode", &self.output_mode)
.finish_non_exhaustive()
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
impl Context {
#[must_use]
pub fn new() -> Self {
Self {
current_time: None,
variables: HashMap::new(),
fc_mode: FCMode::CelsiusFahrenheit,
random_u32: None,
output_mode: OutputMode::SimpleText,
get_exchange_rate: None,
custom_units: vec![],
}
}
pub fn set_current_time_v1(&mut self, _ms_since_1970: u64, _tz_offset_secs: i64) {
self.current_time = None;
}
pub fn use_coulomb_and_farad(&mut self) {
self.fc_mode = FCMode::CoulombFarad;
}
pub fn set_random_u32_fn(&mut self, random_u32: fn() -> u32) {
self.random_u32 = Some(random_u32);
}
pub fn disable_rng(&mut self) {
self.random_u32 = None;
}
pub fn set_output_mode_terminal(&mut self) {
self.output_mode = OutputMode::TerminalFixedWidth;
}
fn serialize_variables_internal(&self, write: &mut impl io::Write) -> FResult<()> {
self.variables.len().serialize(write)?;
for (k, v) in &self.variables {
k.as_str().serialize(write)?;
v.serialize(write)?;
}
Ok(())
}
pub fn serialize_variables(&self, write: &mut impl io::Write) -> Result<(), String> {
match self.serialize_variables_internal(write) {
Ok(()) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
fn deserialize_variables_internal(&mut self, read: &mut impl io::Read) -> FResult<()> {
let len = usize::deserialize(read)?;
self.variables.clear();
self.variables.reserve(len);
for _ in 0..len {
let s = String::deserialize(read)?;
let v = value::Value::deserialize(read)?;
self.variables.insert(s, v);
}
Ok(())
}
pub fn deserialize_variables(&mut self, read: &mut impl io::Read) -> Result<(), String> {
match self.deserialize_variables_internal(read) {
Ok(()) => Ok(()),
Err(e) => Err(e.to_string()),
}
}
pub fn set_exchange_rate_handler_v1<T: ExchangeRateFn + 'static + Send + Sync>(
&mut self,
get_exchange_rate: T,
) {
self.get_exchange_rate = Some(Arc::new(get_exchange_rate));
}
pub fn define_custom_unit_v1(
&mut self,
singular: &str,
plural: &str,
definition: &str,
attribute: &CustomUnitAttribute,
) {
let definition_prefix = match attribute {
CustomUnitAttribute::None => "",
CustomUnitAttribute::AllowLongPrefix => "l@",
CustomUnitAttribute::AllowShortPrefix => "s@",
CustomUnitAttribute::IsLongPrefix => "lp@",
CustomUnitAttribute::Alias => "=",
};
self.custom_units.push((
singular.to_string(),
plural.to_string(),
format!("{definition_prefix}{definition}"),
));
}
}
#[non_exhaustive]
pub enum CustomUnitAttribute {
None,
AllowLongPrefix,
AllowShortPrefix,
IsLongPrefix,
Alias,
}
pub fn evaluate(input: &str, context: &mut Context) -> Result<FendResult, String> {
evaluate_with_interrupt(input, context, &interrupt::Never)
}
fn evaluate_with_interrupt_internal(
input: &str,
context: &mut Context,
int: &impl Interrupt,
) -> Result<FendResult, String> {
if input.is_empty() {
return Ok(FendResult::empty());
}
let (result, is_unit, attrs) = match eval::evaluate_to_spans(input, None, context, int) {
Ok(value) => value,
Err(e) => return Err(e.to_string()),
};
let mut plain_result = String::new();
for s in &result {
plain_result.push_str(&s.string);
}
Ok(FendResult {
plain_result,
span_result: result,
is_unit,
attrs,
})
}
pub fn evaluate_with_interrupt(
input: &str,
context: &mut Context,
int: &impl Interrupt,
) -> Result<FendResult, String> {
evaluate_with_interrupt_internal(input, context, int)
}
pub fn evaluate_preview_with_interrupt(
input: &str,
context: &mut Context,
int: &impl Interrupt,
) -> FendResult {
let empty = FendResult::empty();
let context_clone = context.clone();
context.random_u32 = None;
context.get_exchange_rate = None;
let result = evaluate_with_interrupt_internal(input, context, int);
*context = context_clone;
let Ok(result) = result else {
return empty;
};
let s = result.get_main_result();
if s.is_empty()
|| result.is_unit_type()
|| s.len() > 50
|| s.trim() == input.trim()
|| s.contains(|c| c < ' ')
{
return empty;
}
result
}
#[derive(Debug)]
pub struct Completion {
display: String,
insert: String,
}
impl Completion {
#[must_use]
pub fn display(&self) -> &str {
&self.display
}
#[must_use]
pub fn insert(&self) -> &str {
&self.insert
}
}
static GREEK_LOWERCASE_LETTERS: [(&str, &str); 24] = [
("alpha", "α"),
("beta", "β"),
("gamma", "γ"),
("delta", "δ"),
("epsilon", "ε"),
("zeta", "ζ"),
("eta", "η"),
("theta", "θ"),
("iota", "ι"),
("kappa", "κ"),
("lambda", "λ"),
("mu", "μ"),
("nu", "ν"),
("xi", "ξ"),
("omicron", "ο"),
("pi", "π"),
("rho", "ρ"),
("sigma", "σ"),
("tau", "τ"),
("upsilon", "υ"),
("phi", "φ"),
("chi", "χ"),
("psi", "ψ"),
("omega", "ω"),
];
static GREEK_UPPERCASE_LETTERS: [(&str, &str); 24] = [
("Alpha", "Α"),
("Beta", "Β"),
("Gamma", "Γ"),
("Delta", "Δ"),
("Epsilon", "Ε"),
("Zeta", "Ζ"),
("Eta", "Η"),
("Theta", "Θ"),
("Iota", "Ι"),
("Kappa", "Κ"),
("Lambda", "Λ"),
("Mu", "Μ"),
("Nu", "Ν"),
("Xi", "Ξ"),
("Omicron", "Ο"),
("Pi", "Π"),
("Rho", "Ρ"),
("Sigma", "Σ"),
("Tau", "Τ"),
("Upsilon", "Υ"),
("Phi", "Φ"),
("Chi", "Χ"),
("Psi", "Ψ"),
("Omega", "Ω"),
];
#[must_use]
pub fn get_completions_for_prefix(mut prefix: &str) -> (usize, Vec<Completion>) {
if let Some((prefix, letter)) = prefix.rsplit_once('\\') {
if letter.starts_with(|c: char| c.is_ascii_alphabetic()) && letter.len() <= 7 {
return if letter.starts_with(|c: char| c.is_ascii_uppercase()) {
GREEK_UPPERCASE_LETTERS
} else {
GREEK_LOWERCASE_LETTERS
}
.iter()
.find(|l| l.0 == letter)
.map_or((0, vec![]), |l| {
(
prefix.len(),
vec![Completion {
display: prefix.to_string(),
insert: l.1.to_string(),
}],
)
});
}
}
let mut prepend = "";
let position = prefix.len();
if let Some((a, b)) = prefix.rsplit_once(' ') {
prepend = a;
prefix = b;
}
if prefix.is_empty() {
return (0, vec![]);
}
let mut res = units::get_completions_for_prefix(prefix);
for c in &mut res {
c.display.insert_str(0, prepend);
}
(position, res)
}
pub use inline_substitutions::substitute_inline_fend_expressions;
const fn get_version_as_str() -> &'static str {
env!("CARGO_PKG_VERSION")
}
#[must_use]
pub fn get_version() -> String {
get_version_as_str().to_string()
}
pub mod test_utils {
pub fn dummy_currency_handler(
currency: &str,
) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>> {
Ok(match currency {
"EUR" | "USD" => 1.0,
"GBP" => 0.9,
"NZD" => 1.5,
"HKD" => 8.0,
"AUD" => 1.3,
"PLN" => 0.2,
"JPY" => 149.9,
_ => panic!("unknown currency {currency}"),
})
}
}