use std::{fmt, marker::PhantomData, str::FromStr};
#[cfg(feature = "rusqlite")]
use rusqlite::{
ToSql,
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef}
};
use crate::{Controller, StrVal, err::Error};
#[derive(Clone, Debug)]
pub enum Lim {
Rel(f32),
Abs(u64)
}
impl Default for Lim {
fn default() -> Self {
Self::Rel(1.0)
}
}
#[derive(Clone, Debug)]
pub struct AnyLim;
impl Controller for AnyLim {
type Type = Lim;
fn def() -> String {
String::from("100%")
}
fn validate(val: &Self::Type) -> Result<(), Error> {
match val {
Lim::Rel(f) => {
let range = 0.0..=100.0;
if range.contains(f) {
Ok(())
} else {
Err(Error::OutOfBounds(format!("{range:?}")))
}
}
Lim::Abs(_a) => Ok(())
}
}
}
#[derive(Debug, Clone)]
pub struct RelAbsLim<C = AnyLim> {
sval: String,
val: Lim,
_marker: PhantomData<C>
}
impl<C> Default for RelAbsLim<C> {
fn default() -> Self {
Self {
sval: "100%".into(),
val: Lim::default(),
_marker: PhantomData
}
}
}
impl<C> StrVal for RelAbsLim<C>
where
C: Controller<Type = Lim>
{
type Type = Lim;
fn set(&mut self, sval: &str) -> Result<Self::Type, Error> {
let dv = sval.parse::<Self>()?;
C::validate(&dv.val)?;
self.sval = sval.to_string();
self.val = dv.val.clone();
Ok(dv.val)
}
fn get(&self) -> Self::Type {
self.val.clone()
}
fn val_str(&self) -> Option<String> {
match self.val {
Lim::Rel(f) => Some(f.to_string()),
Lim::Abs(v) => Some(v.to_string())
}
}
fn get_str_vals(&self) -> (String, Option<String>) {
(self.sval.clone(), None)
}
}
impl<C> AsRef<str> for RelAbsLim<C> {
fn as_ref(&self) -> &str {
&self.sval
}
}
impl<C> fmt::Display for RelAbsLim<C> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.sval)
}
}
impl<C> FromStr for RelAbsLim<C>
where
C: Controller<Type = Lim>
{
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(s) = s.strip_suffix("%") {
let f = s.parse::<f32>().unwrap();
let f = f / 100.0;
let val = Lim::Rel(f);
C::validate(&val)?;
Ok(Self {
sval: s.to_string(),
val,
_marker: PhantomData
})
} else {
let val = parse_size::Config::default()
.with_binary()
.parse_size(s)
.map_err(|e| Error::Invalid(e.to_string()))?;
let val = Lim::Abs(val);
C::validate(&val)?;
Ok(Self {
sval: s.to_string(),
val,
_marker: PhantomData
})
}
}
}
#[cfg(feature = "rusqlite")]
#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite")))]
impl<C> ToSql for RelAbsLim<C> {
fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
Ok(ToSqlOutput::from(self.as_ref()))
}
}
#[cfg(feature = "rusqlite")]
#[cfg_attr(docsrs, doc(cfg(feature = "rusqlite")))]
impl<C> FromSql for RelAbsLim<C>
where
C: Controller<Type = Lim>
{
#[inline]
fn column_result(value: ValueRef<'_>) -> FromSqlResult<Self> {
let s = String::column_result(value)?;
s.parse::<Self>()
.map_err(|e| FromSqlError::Other(Box::new(e)))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[allow(clippy::float_cmp)]
fn percent() {
let v = "100%".parse::<RelAbsLim>().unwrap();
let Lim::Rel(f) = v.get() else {
panic!("Unexpectedly not Lim::Rel");
};
assert_eq!(f, 1.0);
}
#[test]
fn abs() {
let v = "1000".parse::<RelAbsLim>().unwrap();
let Lim::Abs(a) = v.get() else {
panic!("Unexpectedly not Lim::Abs");
};
assert_eq!(a, 1000);
}
#[test]
fn abs2() {
let v = "64K".parse::<RelAbsLim>().unwrap();
let Lim::Abs(a) = v.get() else {
panic!("Unexpectedly not Lim::Abs");
};
assert_eq!(a, 65536);
}
}