smart-calculator 1.0.3

Natural-language calculator for math, units, time zones, dates, fiat, and crypto conversions
Documentation
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::HashMap, error::Error, sync::Arc};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum ResultType {
    Math,
    Unit,
    Currency,
    Crypto,
    Time,
    Date,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum AnswerType {
    Number(f64),
    Text(String),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CalculatorResult {
    pub res_type: ResultType,
    pub input: String,
    pub result: AnswerType,
    pub formatted: String,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub metadata: Option<HashMap<String, Value>>,
}

#[async_trait]
pub trait RateProvider: Send + Sync {
    async fn get_fiat_rate(
        &self,
        base: &str,
        target: &str,
    ) -> std::result::Result<f64, Box<dyn Error>>;

    async fn get_crypto_rate(
        &self,
        base: &str,
        target: &str,
    ) -> std::result::Result<f64, Box<dyn Error>>;
}

#[derive(Clone, Default)]
pub struct Config {
    rate_provider: Option<Arc<dyn RateProvider + Send + Sync>>,
    timezone: Option<String>,
    locale: Option<String>,
    precision: Option<u8>,
}

impl Config {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_rate_provider(
        mut self,
        rate_provider: Arc<dyn RateProvider + Send + Sync>,
    ) -> Self {
        self.rate_provider = Some(rate_provider);
        self
    }

    pub fn with_timezone(mut self, timezone: impl Into<String>) -> Self {
        self.timezone = Some(timezone.into());
        self
    }

    pub fn with_locale(mut self, locale: impl Into<String>) -> Self {
        self.locale = Some(locale.into());
        self
    }

    pub fn with_precision(mut self, precision: u8) -> Self {
        self.precision = Some(precision.clamp(1, 21));
        self
    }

    pub fn rate_provider(&self) -> Option<Arc<dyn RateProvider + Send + Sync>> {
        self.rate_provider.clone()
    }

    pub fn timezone(&self) -> &Option<String> {
        &self.timezone
    }

    pub fn locale(&self) -> &Option<String> {
        &self.locale
    }

    pub fn precision(&self) -> Option<u8> {
        self.precision
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Intent {
    Time {
        query: String,
        #[serde(skip_serializing_if = "Option::is_none")]
        from: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        to: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        time: Option<String>,
    },

    Date {
        query: String,
    },

    Currency {
        amount: f64,
        from: String,
        to: String,
    },

    Crypto {
        amount: f64,
        from: String,
        to: String,
    },

    Unit {
        amount: f64,
        from: String,
        to: String,
        category: String,
    },

    Math {
        expression: String,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RelativeDirection {
    Next,
    Last,
    This,
}

impl RelativeDirection {
    pub fn from_str(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "next" | "coming" | "upcoming" => Some(Self::Next),
            "last" => Some(Self::Last),
            "this" => Some(Self::This),
            _ => None,
        }
    }
}