fend_core/
lib.rs

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
10//! This library implements most of the features of [fend](https://github.com/printfn/fend).
11//!
12//! ## Example
13//!
14//! ```rust
15//! extern crate fend_core;
16//!
17//! fn main() {
18//!     let mut context = fend_core::Context::new();
19//!     let result = fend_core::evaluate("1 + 1", &mut context).unwrap();
20//!     assert_eq!(result.get_main_result(), "2");
21//! }
22//! ```
23
24mod ast;
25mod date;
26mod error;
27mod eval;
28mod format;
29mod ident;
30mod inline_substitutions;
31mod interrupt;
32/// This module is not meant to be used by other crates. It may change or be removed at any point.
33#[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/// This contains the result of a computation.
57#[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	/// This retrieves the main result of the computation.
113	#[must_use]
114	pub fn get_main_result(&self) -> &str {
115		self.plain_result.as_str()
116	}
117
118	/// This retrieves the main result as a list of spans, which is useful
119	/// for colored output.
120	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	/// Returns whether the output should be hidden by default. This is set
134	/// if the output would just be the unit `()` type. It's useful to hide this
135	/// by default.
136	#[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	/// Returns whether or not the result should be outputted with a
150	/// trailing newline. This is controlled by the `@no_trailing_newline`
151	/// attribute.
152	#[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/// An exchange rate handler.
177#[deprecated(note = "Use `ExchangeRateFnV2` instead")]
178pub trait ExchangeRateFn {
179	/// Returns the value of a currency relative to the base currency.
180	/// The base currency depends on your implementation. fend-core can work
181	/// with any base currency as long as it is consistent.
182	///
183	/// # Errors
184	/// This function errors out if the currency was not found or the
185	/// conversion is impossible for any reason (HTTP request failed, etc.)
186	#[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
206/// Options passed to an exchange rate handler function
207pub struct ExchangeRateFnV2Options {
208	is_preview: bool,
209}
210
211impl ExchangeRateFnV2Options {
212	/// Whether or not this is a preview rather than a standard calculation.
213	#[must_use]
214	pub fn is_preview(&self) -> bool {
215		self.is_preview
216	}
217}
218
219/// An exchange rate handler.
220pub trait ExchangeRateFnV2 {
221	/// Returns the value of a currency relative to the base currency.
222	/// The base currency depends on your implementation. fend-core can work
223	/// with any base currency as long as it is consistent.
224	///
225	/// If `options.is_preview()` returns true, implementors are
226	/// encouraged to only return cached or pre-fetched results. Blocking the
227	/// thread for an extended period of time is not ideal if this calculation
228	/// is merely for a preview.
229	///
230	/// # Errors
231	/// This function errors out if the currency was not found or the
232	/// conversion is impossible for any reason (HTTP request failed, etc.)
233	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/// This controls decimal and thousands separators.
274#[non_exhaustive]
275#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
276pub enum DecimalSeparatorStyle {
277	/// Use `.` as the decimal separator and `,` as the thousands separator. This is common in English.
278	#[default]
279	Dot,
280	/// Use `,` as the decimal separator and `.` as the thousands separator. This is common in European languages.
281	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
300/// This struct contains fend's current context, including some settings
301/// as well as stored variables.
302///
303/// If you're writing an interpreter it's recommended to only
304/// instantiate this struct once so that variables and settings are
305/// preserved, but you can also manually serialise all variables
306/// and recreate the context for every calculation, depending on
307/// which is easier.
308pub 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, // whether to automatically print the result
315	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		// we can't derive Debug because of the get_exchange_rate and random_u32 fields
341		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	/// Create a new context instance.
362	#[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	/// This method currently has no effect!
379	///
380	/// Set the current time. This API will likely change in the future!
381	///
382	/// The first argument (`ms_since_1970`) must be the number of elapsed milliseconds
383	/// since January 1, 1970 at midnight UTC, ignoring leap seconds in the same way
384	/// as Unix time.
385	///
386	/// The second argument (`tz_offset_secs`) is the current time zone
387	/// offset to UTC, in seconds.
388	pub fn set_current_time_v1(&mut self, _ms_since_1970: u64, _tz_offset_secs: i64) {
389		// self.current_time = Some(CurrentTimeInfo {
390		//     elapsed_unix_time_ms: ms_since_1970,
391		//     timezone_offset_secs: tz_offset_secs,
392		// });
393		self.current_time = None;
394	}
395
396	/// Define the units `C` and `F` as coulomb and farad instead of degrees
397	/// celsius and degrees fahrenheit.
398	pub fn use_coulomb_and_farad(&mut self) {
399		self.fc_mode = FCMode::CoulombFarad;
400	}
401
402	/// Configure random number generator using a function pointer
403	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	/// Configure random number generator using a custom type
408	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	/// Clear the random number generator after setting it with via [`Self::set_random_u32_fn`]
413	pub fn disable_rng(&mut self) {
414		self.random_u32 = None;
415	}
416
417	/// Whether to automatically print the resulting value of this calculation.
418	/// This option is enabled by default. If disabled, only values printed
419	/// with the `print` or `println` functions will be shown in the output.
420	pub fn set_echo_result(&mut self, echo_result: bool) {
421		self.echo_result = echo_result;
422	}
423
424	/// Change the output mode to fixed-width terminal style. This enables ASCII
425	/// graphs in the output.
426	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	/// Serializes all variables defined in this context to a stream of bytes.
440	/// Note that the specific format is NOT stable, and can change with any
441	/// minor update.
442	///
443	/// # Errors
444	/// This function returns an error if the input cannot be serialized.
445	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	/// Deserializes the given variables, replacing all prior variables in
465	/// the given context.
466	///
467	/// # Errors
468	/// Returns an error if the input byte stream is invalid and cannot be
469	/// deserialized.
470	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	/// Set a handler function for loading exchange rates.
478	#[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	/// Set a handler function for loading exchange rates.
490	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	/// Sets the decimal separator style for this context. This can be used to
519	/// change the number format from e.g. `1,234.00` to `1.234,00`.
520	pub fn set_decimal_separator_style(&mut self, style: DecimalSeparatorStyle) {
521		self.decimal_separator = style;
522	}
523}
524
525/// These attributes make is possible to change the behaviour of custom units
526#[non_exhaustive]
527pub enum CustomUnitAttribute {
528	/// Don't allow using prefixes with this custom unit
529	None,
530	/// Support long prefixes (e.g. `milli-`, `giga-`) with this unit
531	AllowLongPrefix,
532	/// Support short prefixes (e.g. `k` for `kilo`) with this unit
533	AllowShortPrefix,
534	/// Allow using this unit as a long prefix with another unit
535	IsLongPrefix,
536	/// This unit definition is an alias and will always be replaced with its definition.
537	Alias,
538}
539
540/// This function evaluates a string using the given context. Any evaluation using this
541/// function cannot be interrupted.
542///
543/// For example, passing in the string `"1 + 1"` will return a result of `"2"`.
544///
545/// # Errors
546/// It returns an error if the given string is invalid.
547/// This may be due to parser or runtime errors.
548pub 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		// no or blank input: return no output
559		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
584/// This function evaluates a string using the given context and the provided
585/// Interrupt object.
586///
587/// For example, passing in the string `"1 + 1"` will return a result of `"2"`.
588///
589/// # Errors
590/// It returns an error if the given string is invalid.
591/// This may be due to parser or runtime errors.
592pub 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
600/// Evaluate the given string to use as a live preview.
601///
602/// Unlike the normal evaluation functions, `evaluate_preview_with_interrupt`
603/// does not mutate the passed-in context, and only returns results suitable
604/// for displaying as a live preview: overly long output, multi-line output,
605/// unit types etc. are all filtered out. RNG functions (e.g. `roll d6`) are
606/// also disabled.
607pub fn evaluate_preview_with_interrupt(
608	input: &str,
609	context: &Context,
610	int: &impl Interrupt,
611) -> FendResult {
612	let empty = FendResult::empty();
613	// unfortunately making a complete copy of the context is necessary
614	// because we want variables to still work in multi-statement inputs
615	// like `a = 2; 5a`.
616	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/// Returns the current version of `fend-core`.
755#[must_use]
756pub fn get_version() -> String {
757	get_version_as_str().to_string()
758}
759
760/// Used by unit and integration tests
761#[doc(hidden)]
762pub mod test_utils {
763	use crate::ExchangeRateFnV2;
764
765	/// A simple currency handler used in unit and integration tests. Not intended
766	/// to be used outside of `fend_core`.
767	///
768	/// # Panics
769	/// Panics on unknown currencies
770	///
771	/// # Errors
772	/// Panics on error, so it never needs to return Err(_)
773	#[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}