use super::{DirectCurrencyField, ResolvedCurrency, unit::ResolvedCurrencyUnit};
use crate::core::currency::currency_for_country;
#[derive(Clone, Debug, Default)]
pub(super) enum Hint<T> {
#[default]
Unknown,
Missing,
Invalid(String),
Present(T),
}
impl<T> Hint<T> {
pub(super) const fn is_unknown(&self) -> bool {
matches!(self, Self::Unknown)
}
pub(super) const fn is_missing(&self) -> bool {
matches!(self, Self::Missing)
}
pub(super) const fn present(&self) -> Option<&T> {
match self {
Self::Present(value) => Some(value),
Self::Unknown | Self::Missing | Self::Invalid(_) => None,
}
}
pub(super) fn invalid_code(&self) -> Option<&str> {
match self {
Self::Invalid(code) => Some(code),
Self::Unknown | Self::Missing | Self::Present(_) => None,
}
}
}
impl Hint<CurrencyHintValue> {
fn set_from_code(&mut self, code: Option<&str>, provenance: CurrencyHintProvenance) {
let Some(code) = code.map(str::trim).filter(|code| !code.is_empty()) else {
*self = Self::Missing;
return;
};
*self = ResolvedCurrencyUnit::from_code(code)
.map(|unit| CurrencyHintValue { unit, provenance })
.map_or_else(|| Self::Invalid(code.to_string()), Self::Present);
}
}
#[derive(Clone, Debug)]
pub(super) struct CurrencyHintValue {
unit: ResolvedCurrencyUnit,
provenance: CurrencyHintProvenance,
}
impl CurrencyHintValue {
pub(super) fn resolved_currency(&self) -> ResolvedCurrency {
match self.provenance {
CurrencyHintProvenance::QuoteV7 => {
ResolvedCurrency::quote_enrichment(self.unit.clone())
}
CurrencyHintProvenance::QuoteSummary => {
ResolvedCurrency::quote_summary_enrichment(self.unit.clone())
}
CurrencyHintProvenance::ChartMeta => {
ResolvedCurrency::direct_provider(self.unit.clone(), DirectCurrencyField::ChartMeta)
}
CurrencyHintProvenance::OptionsQuote => ResolvedCurrency::direct_provider(
self.unit.clone(),
DirectCurrencyField::OptionsQuote,
),
CurrencyHintProvenance::ProfileCountry => {
ResolvedCurrency::profile_country_heuristic(self.unit.clone())
}
}
}
}
#[derive(Clone, Copy, Debug)]
enum CurrencyHintProvenance {
QuoteV7,
QuoteSummary,
ChartMeta,
OptionsQuote,
ProfileCountry,
}
#[derive(Clone, Copy, Debug)]
pub(super) enum CurrencyHintField {
Quote,
Financial,
QuoteSummaryFinancial,
ProfileCountry,
}
impl CurrencyHintField {
const COUNT: usize = 4;
const fn index(self) -> usize {
match self {
Self::Quote => 0,
Self::Financial => 1,
Self::QuoteSummaryFinancial => 2,
Self::ProfileCountry => 3,
}
}
pub(super) const fn provider_path(self) -> &'static str {
match self {
Self::Quote => "currency",
Self::Financial => "financialCurrency",
Self::QuoteSummaryFinancial => "quoteSummary.financialCurrency",
Self::ProfileCountry => "profile.country",
}
}
}
#[derive(Clone, Debug, Default)]
pub struct CurrencyHints {
currencies: [Hint<CurrencyHintValue>; CurrencyHintField::COUNT],
pub(super) exchange: Option<String>,
pub(super) full_exchange_name: Option<String>,
pub(super) quote_type: Option<String>,
pub(super) country: Option<String>,
}
impl CurrencyHints {
pub fn from_quote(
quote_currency: Option<&str>,
financial_currency: Option<&str>,
exchange: Option<&str>,
full_exchange_name: Option<&str>,
quote_type: Option<&str>,
) -> Self {
let mut hints = Self::default();
hints.set_currency(
CurrencyHintField::Quote,
quote_currency,
CurrencyHintProvenance::QuoteV7,
);
hints.set_currency(
CurrencyHintField::Financial,
financial_currency,
CurrencyHintProvenance::QuoteV7,
);
hints.exchange = nonempty_owned(exchange);
hints.full_exchange_name = nonempty_owned(full_exchange_name);
hints.quote_type = nonempty_owned(quote_type);
hints
}
pub fn from_chart(
quote_currency: Option<&str>,
exchange: Option<&str>,
full_exchange_name: Option<&str>,
quote_type: Option<&str>,
) -> Self {
let mut hints = Self::default();
hints.set_currency(
CurrencyHintField::Quote,
quote_currency,
CurrencyHintProvenance::ChartMeta,
);
hints.exchange = nonempty_owned(exchange);
hints.full_exchange_name = nonempty_owned(full_exchange_name);
hints.quote_type = nonempty_owned(quote_type);
hints
}
pub fn from_options_quote(
quote_currency: Option<&str>,
exchange: Option<&str>,
full_exchange_name: Option<&str>,
quote_type: Option<&str>,
) -> Self {
let mut hints = Self::default();
if quote_currency
.map(str::trim)
.is_some_and(|code| !code.is_empty())
{
hints.set_currency(
CurrencyHintField::Quote,
quote_currency,
CurrencyHintProvenance::OptionsQuote,
);
}
hints.exchange = nonempty_owned(exchange);
hints.full_exchange_name = nonempty_owned(full_exchange_name);
hints.quote_type = nonempty_owned(quote_type);
hints
}
pub fn from_profile(
country: Option<&str>,
exchange: Option<&str>,
quote_type: Option<&str>,
) -> Self {
let mut hints = Self {
country: nonempty_owned(country),
exchange: nonempty_owned(exchange),
quote_type: nonempty_owned(quote_type),
..Self::default()
};
*hints.hint_mut(CurrencyHintField::ProfileCountry) = country
.and_then(currency_for_country)
.map(|currency| CurrencyHintValue {
unit: ResolvedCurrencyUnit::from_currency(currency),
provenance: CurrencyHintProvenance::ProfileCountry,
})
.map_or(Hint::Missing, Hint::Present);
hints
}
pub fn from_quote_summary_financial(financial_currency: Option<&str>) -> Self {
let mut hints = Self::default();
hints.set_currency(
CurrencyHintField::QuoteSummaryFinancial,
financial_currency,
CurrencyHintProvenance::QuoteSummary,
);
hints
}
pub(super) const fn hint(&self, field: CurrencyHintField) -> &Hint<CurrencyHintValue> {
&self.currencies[field.index()]
}
const fn hint_mut(&mut self, field: CurrencyHintField) -> &mut Hint<CurrencyHintValue> {
&mut self.currencies[field.index()]
}
fn set_currency(
&mut self,
field: CurrencyHintField,
code: Option<&str>,
provenance: CurrencyHintProvenance,
) {
self.hint_mut(field).set_from_code(code, provenance);
}
pub(super) const fn value(&self, field: CurrencyHintField) -> Option<&CurrencyHintValue> {
self.hint(field).present()
}
pub(super) fn invalid_code(&self, field: CurrencyHintField) -> Option<&str> {
self.hint(field).invalid_code()
}
pub(super) const fn is_unknown(&self, field: CurrencyHintField) -> bool {
self.hint(field).is_unknown()
}
pub(super) const fn is_missing(&self, field: CurrencyHintField) -> bool {
self.hint(field).is_missing()
}
pub(super) fn merge(&mut self, other: Self) {
for (slot, incoming) in self.currencies.iter_mut().zip(other.currencies) {
merge_hint(slot, incoming);
}
merge_option(&mut self.exchange, other.exchange);
merge_option(&mut self.full_exchange_name, other.full_exchange_name);
merge_option(&mut self.quote_type, other.quote_type);
merge_option(&mut self.country, other.country);
}
}
fn nonempty_owned(value: Option<&str>) -> Option<String> {
value
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn merge_option(slot: &mut Option<String>, incoming: Option<String>) {
if let Some(value) = incoming {
*slot = Some(value);
}
}
fn merge_hint<T>(slot: &mut Hint<T>, incoming: Hint<T>) {
match incoming {
Hint::Unknown => {}
Hint::Missing => {
if slot.is_unknown() {
*slot = Hint::Missing;
}
}
Hint::Invalid(value) => {
if !matches!(slot, Hint::Present(_)) {
*slot = Hint::Invalid(value);
}
}
Hint::Present(value) => *slot = Hint::Present(value),
}
}