1#![forbid(unsafe_code)]
2#![deny(clippy::all)]
3#![deny(clippy::pedantic)]
4#![deny(clippy::use_self)]
5#![forbid(clippy::needless_borrow)]
6#![forbid(unreachable_pub)]
7#![forbid(elided_lifetimes_in_paths)]
8#![allow(clippy::tabs_in_doc_comments)]
9
10mod ast;
25mod date;
26mod error;
27mod eval;
28mod format;
29mod ident;
30mod inline_substitutions;
31mod interrupt;
32#[doc(hidden)]
34pub mod json;
35mod lexer;
36mod num;
37mod parser;
38mod result;
39mod scope;
40mod serialize;
41mod units;
42mod value;
43
44use std::error::Error;
45use std::fmt::Write;
46use std::mem;
47use std::sync::Arc;
48use std::{collections::HashMap, fmt, io};
49
50use error::FendError;
51pub(crate) use eval::Attrs;
52pub use interrupt::Interrupt;
53use result::FResult;
54use serialize::{Deserialize, Serialize};
55
56#[derive(PartialEq, Eq, Debug)]
58pub struct FendResult {
59 plain_result: String,
60 span_result: Vec<Span>,
61 attrs: eval::Attrs,
62}
63
64#[derive(Debug, Clone, Copy, Eq, PartialEq)]
65#[non_exhaustive]
66pub enum SpanKind {
67 Number,
68 BuiltInFunction,
69 Keyword,
70 String,
71 Date,
72 Whitespace,
73 Ident,
74 Boolean,
75 Other,
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
79struct Span {
80 string: String,
81 kind: SpanKind,
82}
83
84impl Span {
85 fn from_string(s: String) -> Self {
86 Self {
87 string: s,
88 kind: SpanKind::Other,
89 }
90 }
91}
92
93#[derive(Clone, Copy, Debug, PartialEq, Eq)]
94pub struct SpanRef<'a> {
95 string: &'a str,
96 kind: SpanKind,
97}
98
99impl<'a> SpanRef<'a> {
100 #[must_use]
101 pub fn kind(self) -> SpanKind {
102 self.kind
103 }
104
105 #[must_use]
106 pub fn string(self) -> &'a str {
107 self.string
108 }
109}
110
111impl FendResult {
112 #[must_use]
114 pub fn get_main_result(&self) -> &str {
115 self.plain_result.as_str()
116 }
117
118 pub fn get_main_result_spans(&self) -> impl Iterator<Item = SpanRef<'_>> {
121 self.span_result.iter().map(|span| SpanRef {
122 string: &span.string,
123 kind: span.kind,
124 })
125 }
126
127 #[must_use]
128 #[deprecated(note = "use `output_is_empty()` instead")]
129 pub fn is_unit_type(&self) -> bool {
130 self.output_is_empty()
131 }
132
133 #[must_use]
137 pub fn output_is_empty(&self) -> bool {
138 self.span_result.is_empty()
139 }
140
141 fn empty() -> Self {
142 Self {
143 plain_result: String::new(),
144 span_result: vec![],
145 attrs: Attrs::default(),
146 }
147 }
148
149 #[must_use]
153 pub fn has_trailing_newline(&self) -> bool {
154 self.attrs.trailing_newline
155 }
156}
157
158#[derive(Clone, Debug)]
159struct CurrentTimeInfo {
160 elapsed_unix_time_ms: u64,
161 timezone_offset_secs: i64,
162}
163
164#[derive(Clone, Debug, PartialEq, Eq)]
165enum FCMode {
166 CelsiusFahrenheit,
167 CoulombFarad,
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
171enum OutputMode {
172 SimpleText,
173 TerminalFixedWidth,
174}
175
176#[deprecated(note = "Use `ExchangeRateFnV2` instead")]
178pub trait ExchangeRateFn {
179 #[deprecated(note = "Use `ExchangeRateFnV2::relative_to_base_currency` instead")]
187 fn relative_to_base_currency(
188 &self,
189 currency: &str,
190 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>;
191}
192
193#[allow(deprecated)]
194impl<T> ExchangeRateFn for T
195where
196 T: Fn(&str) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>,
197{
198 fn relative_to_base_currency(
199 &self,
200 currency: &str,
201 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>> {
202 self(currency)
203 }
204}
205
206pub struct ExchangeRateFnV2Options {
208 is_preview: bool,
209}
210
211impl ExchangeRateFnV2Options {
212 #[must_use]
214 pub fn is_preview(&self) -> bool {
215 self.is_preview
216 }
217}
218
219pub trait ExchangeRateFnV2 {
221 fn relative_to_base_currency(
234 &self,
235 currency: &str,
236 options: &ExchangeRateFnV2Options,
237 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>>;
238}
239
240struct ExchangeRateV1CompatWrapper {
241 #[allow(deprecated)]
242 get_exchange_rate_v1: Arc<dyn ExchangeRateFn + Send + Sync>,
243}
244
245impl ExchangeRateFnV2 for ExchangeRateV1CompatWrapper {
246 fn relative_to_base_currency(
247 &self,
248 currency: &str,
249 options: &ExchangeRateFnV2Options,
250 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>> {
251 if options.is_preview {
252 return Err(FendError::NoExchangeRatesAvailable.into());
253 }
254 #[allow(deprecated)]
255 self.get_exchange_rate_v1
256 .relative_to_base_currency(currency)
257 }
258}
259
260pub mod random {
261 pub trait RandomSource: Send + Sync {
262 fn get_random_u32(&mut self) -> u32;
263 }
264
265 pub(crate) struct RandomSourceFn(pub(crate) fn() -> u32);
266 impl RandomSource for RandomSourceFn {
267 fn get_random_u32(&mut self) -> u32 {
268 (self.0)()
269 }
270 }
271}
272
273#[non_exhaustive]
275#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
276pub enum DecimalSeparatorStyle {
277 #[default]
279 Dot,
280 Comma,
282}
283
284impl DecimalSeparatorStyle {
285 fn decimal_separator(self) -> char {
286 match self {
287 Self::Dot => '.',
288 Self::Comma => ',',
289 }
290 }
291
292 fn thousands_separator(self) -> char {
293 match self {
294 Self::Dot => ',',
295 Self::Comma => '.',
296 }
297 }
298}
299
300pub struct Context {
309 current_time: Option<CurrentTimeInfo>,
310 variables: HashMap<String, value::Value>,
311 fc_mode: FCMode,
312 random_u32: Option<Box<dyn random::RandomSource>>,
313 output_mode: OutputMode,
314 echo_result: bool, get_exchange_rate_v2: Option<Arc<dyn ExchangeRateFnV2 + Send + Sync>>,
316 custom_units: Vec<(String, String, String)>,
317 decimal_separator: DecimalSeparatorStyle,
318 is_preview: bool,
319}
320
321impl Clone for Context {
322 fn clone(&self) -> Self {
323 Self {
324 current_time: self.current_time.clone(),
325 variables: self.variables.clone(),
326 fc_mode: self.fc_mode.clone(),
327 random_u32: None,
328 output_mode: self.output_mode.clone(),
329 echo_result: self.echo_result,
330 get_exchange_rate_v2: self.get_exchange_rate_v2.clone(),
331 custom_units: self.custom_units.clone(),
332 decimal_separator: self.decimal_separator,
333 is_preview: self.is_preview,
334 }
335 }
336}
337
338impl fmt::Debug for Context {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.debug_struct("Context")
342 .field("current_time", &self.current_time)
343 .field("variables", &self.variables)
344 .field("fc_mode", &self.fc_mode)
345 .field("output_mode", &self.output_mode)
346 .field("echo_result", &self.echo_result)
347 .field("custom_units", &self.custom_units)
348 .field("decimal_separator", &self.decimal_separator)
349 .field("is_preview", &self.is_preview)
350 .finish_non_exhaustive()
351 }
352}
353
354impl Default for Context {
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360impl Context {
361 #[must_use]
363 pub fn new() -> Self {
364 Self {
365 current_time: None,
366 variables: HashMap::new(),
367 fc_mode: FCMode::CelsiusFahrenheit,
368 random_u32: None,
369 output_mode: OutputMode::SimpleText,
370 echo_result: true,
371 get_exchange_rate_v2: None,
372 custom_units: vec![],
373 decimal_separator: DecimalSeparatorStyle::default(),
374 is_preview: false,
375 }
376 }
377
378 pub fn set_current_time_v1(&mut self, _ms_since_1970: u64, _tz_offset_secs: i64) {
389 self.current_time = None;
394 }
395
396 pub fn use_coulomb_and_farad(&mut self) {
399 self.fc_mode = FCMode::CoulombFarad;
400 }
401
402 pub fn set_random_u32_fn(&mut self, random_u32: fn() -> u32) {
404 self.random_u32 = Some(Box::new(random::RandomSourceFn(random_u32)));
405 }
406
407 pub fn set_random_u32_trait(&mut self, random_u32: impl random::RandomSource + 'static) {
409 self.random_u32 = Some(Box::new(random_u32));
410 }
411
412 pub fn disable_rng(&mut self) {
414 self.random_u32 = None;
415 }
416
417 pub fn set_echo_result(&mut self, echo_result: bool) {
421 self.echo_result = echo_result;
422 }
423
424 pub fn set_output_mode_terminal(&mut self) {
427 self.output_mode = OutputMode::TerminalFixedWidth;
428 }
429
430 fn serialize_variables_internal(&self, write: &mut impl io::Write) -> FResult<()> {
431 self.variables.len().serialize(write)?;
432 for (k, v) in &self.variables {
433 k.as_str().serialize(write)?;
434 v.serialize(write)?;
435 }
436 Ok(())
437 }
438
439 pub fn serialize_variables(&self, write: &mut impl io::Write) -> Result<(), String> {
446 match self.serialize_variables_internal(write) {
447 Ok(()) => Ok(()),
448 Err(e) => Err(e.to_string()),
449 }
450 }
451
452 fn deserialize_variables_internal(&mut self, read: &mut impl io::Read) -> FResult<()> {
453 let len = usize::deserialize(read)?;
454 self.variables.clear();
455 self.variables.reserve(len);
456 for _ in 0..len {
457 let s = String::deserialize(read)?;
458 let v = value::Value::deserialize(read)?;
459 self.variables.insert(s, v);
460 }
461 Ok(())
462 }
463
464 pub fn deserialize_variables(&mut self, read: &mut impl io::Read) -> Result<(), String> {
471 match self.deserialize_variables_internal(read) {
472 Ok(()) => Ok(()),
473 Err(e) => Err(e.to_string()),
474 }
475 }
476
477 #[deprecated(note = "Use `set_exchange_rate_handler_v2` instead")]
479 #[allow(deprecated)]
480 pub fn set_exchange_rate_handler_v1<T: ExchangeRateFn + 'static + Send + Sync>(
481 &mut self,
482 get_exchange_rate: T,
483 ) {
484 self.get_exchange_rate_v2 = Some(Arc::new(ExchangeRateV1CompatWrapper {
485 get_exchange_rate_v1: Arc::new(get_exchange_rate),
486 }));
487 }
488
489 pub fn set_exchange_rate_handler_v2<T: ExchangeRateFnV2 + 'static + Send + Sync>(
491 &mut self,
492 get_exchange_rate: T,
493 ) {
494 self.get_exchange_rate_v2 = Some(Arc::new(get_exchange_rate));
495 }
496
497 pub fn define_custom_unit_v1(
498 &mut self,
499 singular: &str,
500 plural: &str,
501 definition: &str,
502 attribute: &CustomUnitAttribute,
503 ) {
504 let definition_prefix = match attribute {
505 CustomUnitAttribute::None => "",
506 CustomUnitAttribute::AllowLongPrefix => "l@",
507 CustomUnitAttribute::AllowShortPrefix => "s@",
508 CustomUnitAttribute::IsLongPrefix => "lp@",
509 CustomUnitAttribute::Alias => "=",
510 };
511 self.custom_units.push((
512 singular.to_string(),
513 plural.to_string(),
514 format!("{definition_prefix}{definition}"),
515 ));
516 }
517
518 pub fn set_decimal_separator_style(&mut self, style: DecimalSeparatorStyle) {
521 self.decimal_separator = style;
522 }
523}
524
525#[non_exhaustive]
527pub enum CustomUnitAttribute {
528 None,
530 AllowLongPrefix,
532 AllowShortPrefix,
534 IsLongPrefix,
536 Alias,
538}
539
540pub fn evaluate(input: &str, context: &mut Context) -> Result<FendResult, String> {
549 evaluate_with_interrupt(input, context, &interrupt::Never)
550}
551
552fn evaluate_with_interrupt_internal(
553 input: &str,
554 context: &mut Context,
555 int: &impl Interrupt,
556) -> Result<FendResult, String> {
557 if input.is_empty() {
558 return Ok(FendResult::empty());
560 }
561 let (result, attrs) = match eval::evaluate_to_spans(input, None, context, int) {
562 Ok(value) => value,
563 Err(e) => {
564 let mut error: &dyn Error = &e;
565 let mut s = error.to_string();
566 while let Some(inner) = error.source() {
567 write!(&mut s, ": {inner}").unwrap();
568 error = inner;
569 }
570 return Err(s);
571 }
572 };
573 let mut plain_result = String::new();
574 for s in &result {
575 plain_result.push_str(&s.string);
576 }
577 Ok(FendResult {
578 plain_result,
579 span_result: result,
580 attrs,
581 })
582}
583
584pub fn evaluate_with_interrupt(
593 input: &str,
594 context: &mut Context,
595 int: &impl Interrupt,
596) -> Result<FendResult, String> {
597 evaluate_with_interrupt_internal(input, context, int)
598}
599
600pub fn evaluate_preview_with_interrupt(
608 input: &str,
609 context: &Context,
610 int: &impl Interrupt,
611) -> FendResult {
612 let empty = FendResult::empty();
613 let mut context_clone = context.clone();
617 context_clone.random_u32 = None;
618 context_clone.is_preview = true;
619 let result = evaluate_with_interrupt_internal(input, &mut context_clone, int);
620 mem::drop(context_clone);
621 let Ok(result) = result else {
622 return empty;
623 };
624 let s = result.get_main_result();
625 if s.is_empty()
626 || result.output_is_empty()
627 || s.len() > 50
628 || s.trim() == input.trim()
629 || s.contains(|c| c < ' ')
630 {
631 return empty;
632 }
633 result
634}
635
636#[derive(Debug)]
637pub struct Completion {
638 display: String,
639 insert: String,
640}
641
642impl Completion {
643 #[must_use]
644 pub fn display(&self) -> &str {
645 &self.display
646 }
647
648 #[must_use]
649 pub fn insert(&self) -> &str {
650 &self.insert
651 }
652}
653
654static GREEK_LOWERCASE_LETTERS: [(&str, &str); 24] = [
655 ("alpha", "α"),
656 ("beta", "β"),
657 ("gamma", "γ"),
658 ("delta", "δ"),
659 ("epsilon", "ε"),
660 ("zeta", "ζ"),
661 ("eta", "η"),
662 ("theta", "θ"),
663 ("iota", "ι"),
664 ("kappa", "κ"),
665 ("lambda", "λ"),
666 ("mu", "μ"),
667 ("nu", "ν"),
668 ("xi", "ξ"),
669 ("omicron", "ο"),
670 ("pi", "π"),
671 ("rho", "ρ"),
672 ("sigma", "σ"),
673 ("tau", "τ"),
674 ("upsilon", "υ"),
675 ("phi", "φ"),
676 ("chi", "χ"),
677 ("psi", "ψ"),
678 ("omega", "ω"),
679];
680static GREEK_UPPERCASE_LETTERS: [(&str, &str); 24] = [
681 ("Alpha", "Α"),
682 ("Beta", "Β"),
683 ("Gamma", "Γ"),
684 ("Delta", "Δ"),
685 ("Epsilon", "Ε"),
686 ("Zeta", "Ζ"),
687 ("Eta", "Η"),
688 ("Theta", "Θ"),
689 ("Iota", "Ι"),
690 ("Kappa", "Κ"),
691 ("Lambda", "Λ"),
692 ("Mu", "Μ"),
693 ("Nu", "Ν"),
694 ("Xi", "Ξ"),
695 ("Omicron", "Ο"),
696 ("Pi", "Π"),
697 ("Rho", "Ρ"),
698 ("Sigma", "Σ"),
699 ("Tau", "Τ"),
700 ("Upsilon", "Υ"),
701 ("Phi", "Φ"),
702 ("Chi", "Χ"),
703 ("Psi", "Ψ"),
704 ("Omega", "Ω"),
705];
706
707#[must_use]
708pub fn get_completions_for_prefix(mut prefix: &str) -> (usize, Vec<Completion>) {
709 if let Some((prefix, letter)) = prefix.rsplit_once('\\')
710 && letter.starts_with(|c: char| c.is_ascii_alphabetic())
711 && letter.len() <= 7
712 {
713 return if letter.starts_with(|c: char| c.is_ascii_uppercase()) {
714 GREEK_UPPERCASE_LETTERS
715 } else {
716 GREEK_LOWERCASE_LETTERS
717 }
718 .iter()
719 .find(|l| l.0 == letter)
720 .map_or((0, vec![]), |l| {
721 (
722 prefix.len(),
723 vec![Completion {
724 display: prefix.to_string(),
725 insert: l.1.to_string(),
726 }],
727 )
728 });
729 }
730
731 let mut prepend = "";
732 let position = prefix.len();
733 if let Some((a, b)) = prefix.rsplit_once(' ') {
734 prepend = a;
735 prefix = b;
736 }
737
738 if prefix.is_empty() {
739 return (0, vec![]);
740 }
741 let mut res = units::get_completions_for_prefix(prefix);
742 for c in &mut res {
743 c.display.insert_str(0, prepend);
744 }
745 (position, res)
746}
747
748pub use inline_substitutions::substitute_inline_fend_expressions;
749
750const fn get_version_as_str() -> &'static str {
751 env!("CARGO_PKG_VERSION")
752}
753
754#[must_use]
756pub fn get_version() -> String {
757 get_version_as_str().to_string()
758}
759
760#[doc(hidden)]
762pub mod test_utils {
763 use crate::ExchangeRateFnV2;
764
765 #[doc(hidden)]
774 pub struct DummyCurrencyHandler;
775
776 impl ExchangeRateFnV2 for DummyCurrencyHandler {
777 fn relative_to_base_currency(
778 &self,
779 currency: &str,
780 _options: &crate::ExchangeRateFnV2Options,
781 ) -> Result<f64, Box<dyn std::error::Error + Send + Sync + 'static>> {
782 Ok(match currency {
783 "EUR" | "USD" => 1.0,
784 "GBP" => 0.9,
785 "NZD" => 1.5,
786 "HKD" => 8.0,
787 "AUD" => 1.3,
788 "PLN" => 0.2,
789 "JPY" => 149.9,
790 _ => panic!("unknown currency {currency}"),
791 })
792 }
793 }
794}