use crate::core::validators::ValidationError;
use super::FieldType;
pub fn validate_decimal_field(
value: &str,
max_digits: usize,
decimal_places: usize,
) -> Result<(), ValidationError> {
let normalized = value.trim().trim_start_matches(['+', '-']);
let parts: Vec<_> = normalized.split('.').collect();
let is_valid_decimal = match parts.as_slice() {
[whole] => !whole.is_empty() && whole.chars().all(|ch| ch.is_ascii_digit()),
[whole, fractional] => {
!whole.is_empty()
&& whole.chars().all(|ch| ch.is_ascii_digit())
&& fractional.chars().all(|ch| ch.is_ascii_digit())
}
_ => false,
};
if !is_valid_decimal {
return Err(ValidationError::new(
"Enter a valid decimal number.",
"invalid",
));
}
let whole_digits = parts[0].chars().count();
let fractional_digits = parts
.get(1)
.map_or(0, |fractional| fractional.chars().count());
let total_digits = whole_digits + fractional_digits;
if fractional_digits > decimal_places {
return Err(ValidationError::new(
format!("Ensure that there are no more than {decimal_places} decimal places."),
"max_decimal_places",
));
}
if total_digits > max_digits {
return Err(ValidationError::new(
format!("Ensure that there are no more than {max_digits} digits in total."),
"max_digits",
));
}
Ok(())
}
fn parse_float(value: &str) -> Result<f64, ValidationError> {
value.trim().parse::<f64>().map_err(|_| {
ValidationError::new("Enter a valid number.", "invalid").with_param("value", value)
})
}
#[must_use]
pub fn db_type(field: &FieldType, vendor: &str) -> Option<String> {
match field {
FieldType::Float => Some(match vendor {
"postgres" => "double precision".to_string(),
"sqlite" => "real".to_string(),
"mysql" => "double".to_string(),
_ => "double precision".to_string(),
}),
FieldType::Decimal {
max_digits,
decimal_places,
} => Some(format!("numeric({max_digits},{decimal_places})")),
_ => None,
}
}
pub fn get_prep_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
match field {
FieldType::Float => Ok(parse_float(value)?.to_string()),
FieldType::Decimal {
max_digits,
decimal_places,
} => {
validate_decimal_field(value, *max_digits as usize, *decimal_places as usize)?;
Ok(value.trim().to_string())
}
_ => Err(ValidationError::new(
"Field type is not handled by float preparation.",
"invalid",
)),
}
}
pub fn from_db_value(field: &FieldType, value: &str) -> Result<String, ValidationError> {
match field {
FieldType::Float => Ok(parse_float(value)?.to_string()),
FieldType::Decimal {
max_digits,
decimal_places,
} => {
validate_decimal_field(value, *max_digits as usize, *decimal_places as usize)?;
Ok(value.trim().to_string())
}
_ => Err(ValidationError::new(
"Field type is not handled by float conversion.",
"invalid",
)),
}
}
#[must_use]
pub fn formfield(field: &FieldType) -> Option<&'static str> {
match field {
FieldType::Float => Some("FloatField"),
FieldType::Decimal { .. } => Some("DecimalField"),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::{FieldType, db_type, formfield, from_db_value, get_prep_value};
#[test]
fn db_type_for_postgres_uses_double_precision_for_float() {
assert_eq!(
db_type(&FieldType::Float, "postgres").as_deref(),
Some("double precision")
);
}
#[test]
fn get_prep_value_normalizes_float_input() {
let prepared = get_prep_value(&FieldType::Float, "3.5")
.expect("float preparation should parse valid numbers");
assert_eq!(prepared, "3.5");
}
#[test]
fn from_db_value_parses_decimal_round_trip() {
let field = FieldType::Decimal {
max_digits: 5,
decimal_places: 2,
};
let parsed = from_db_value(&field, "12.34").expect("decimal values should parse");
assert_eq!(parsed, "12.34");
}
#[test]
fn formfield_returns_correct_type() {
let field = FieldType::Decimal {
max_digits: 6,
decimal_places: 2,
};
assert_eq!(formfield(&field), Some("DecimalField"));
}
}