Skip to content

Commit

Permalink
Add validation for numeric fields.
Browse files Browse the repository at this point in the history
Add validation for value in euro, number of shares and percentage
of ownership.

We validate the format should be a number with 2 decimals at most.
  • Loading branch information
vaijira committed Dec 14, 2024
1 parent 4b951bc commit 2744283
Show file tree
Hide file tree
Showing 3 changed files with 161 additions and 20 deletions.
1 change: 1 addition & 0 deletions src/data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub type Aeat720Records = Vec<Aeat720Record>;
pub const DEFAULT_YEAR: usize = 2024;
pub const SPAIN_COUNTRY_CODE: &str = "ES";

Check warning on line 12 in src/data.rs

View workflow job for this annotation

GitHub Actions / Format & Clippy (1.80.1)

constant `SPAIN_COUNTRY_CODE` is never used

Check warning on line 12 in src/data.rs

View workflow job for this annotation

GitHub Actions / Unit Tests on 1.80.1

constant `SPAIN_COUNTRY_CODE` is never used

Check warning on line 12 in src/data.rs

View workflow job for this annotation

GitHub Actions / Unit Tests on 1.80.1

constant `SPAIN_COUNTRY_CODE` is never used
pub const DEFAULT_LOCALE: &Locale = &Locale::es;
pub const DEFAULT_NUMBER_OF_DECIMALS: u16 = 2;

#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub enum BrokerOperation {
Expand Down
139 changes: 119 additions & 20 deletions src/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,33 @@ use futures_signals::{
signal::{Mutable, Signal, SignalExt},
signal_vec::{MutableVec, SignalVecExt},
};
use rust_decimal::Decimal;
use web_sys::{HtmlElement, HtmlInputElement};

use crate::{
css::{TABLE_CAPTION, TABLE_HEADER, TABLE_ROW, TABLE_STYLE},
data::{Aeat720Record, BrokerInformation, DEFAULT_LOCALE},
utils::{decimal::decimal_to_str_locale, icons::render_svg_trash_icon, usize_to_date},
data::{Aeat720Record, BrokerInformation, DEFAULT_LOCALE, DEFAULT_NUMBER_OF_DECIMALS},
utils::{
decimal::{decimal_to_str_locale, valid_str_number_with_decimals},
icons::render_svg_trash_icon,
usize_to_date,
},
};

const NAME_NOT_VALID_ERR_MSG: &str = "Nombre no válido";
const ISIN_NOT_VALID_ERR_MSG: &str = "ISIN no válido";
const VALUE_NOT_VALID_ERR_MSG: &str = "Valor (€) no válido";
const QUANTITY_NOT_VALID_ERR_MSG: &str = "Nº acciones no válido";
const PERCENT_NOT_VALID_ERR_MSG: &str = "Porcentaje no válido";

#[derive(Debug, Clone)]
struct Aeat720RecordInfo {
record: Aeat720Record,
name_err_msg: Mutable<Option<&'static str>>,
isin_err_msg: Mutable<Option<&'static str>>,
value_err_msg: Mutable<Option<&'static str>>,
quantity_err_msg: Mutable<Option<&'static str>>,
percent_err_msg: Mutable<Option<&'static str>>,
}
pub struct Table {
headers: Vec<&'static str>,
Expand Down Expand Up @@ -59,6 +70,9 @@ impl Table {
record,
name_err_msg: Mutable::new(None),
isin_err_msg: Mutable::new(None),
value_err_msg: Mutable::new(None),
quantity_err_msg: Mutable::new(None),
percent_err_msg: Mutable::new(None),
}));
}
}
Expand Down Expand Up @@ -169,8 +183,7 @@ impl Table {
record.signal_ref(clone!(record => move |r| {
Some(
html!("td", {
.child(
html!("input" => HtmlInputElement, {
.child(html!("input" => HtmlInputElement, {
.style("display", "block")
.attr("type", "text")
.attr("size", "12")
Expand Down Expand Up @@ -199,15 +212,12 @@ impl Table {
record.lock_mut().record.company.isin = isin;
}))
})
})
)
.child(
html!("span", {
}))
.child(html!("span", {
.style("color", "red")
.style("font-size", "small")
.text_signal(record.lock_ref().isin_err_msg.signal_ref(|t| t.unwrap_or("")))
})
)
}))
})
)
}))
Expand Down Expand Up @@ -265,46 +275,135 @@ impl Table {
}

fn value_cell(record: &Mutable<Aeat720RecordInfo>) -> impl Signal<Item = Option<Dom>> {
record.signal_ref(move |r| {
record.signal_ref(clone!(record => move |r| {
Some(html!("td", {
.child(html!("input", {
.child(html!("input" => HtmlInputElement, {
.style("text-align", "right")
.attr("type", "text")
.attr("size", "9")
.attr("maxlength", "15")
.attr("value", &decimal_to_str_locale(&r.record.value_in_euro, DEFAULT_LOCALE))
.with_node!(element => {
.event(clone!(record => move |_: events::Input| {
if valid_str_number_with_decimals(&element.value(), DEFAULT_NUMBER_OF_DECIMALS, DEFAULT_LOCALE) {
*record.lock_mut().value_err_msg.lock_mut() = None;
} else {
*record.lock_mut().value_err_msg.lock_mut() = Some(VALUE_NOT_VALID_ERR_MSG);
}
}))
})
.with_node!(element => {
.event(clone!(record => move |_: events::Change| {
let money_str = element.value();
if valid_str_number_with_decimals(&money_str, DEFAULT_NUMBER_OF_DECIMALS, DEFAULT_LOCALE) {
if let Ok(money) = money_str.parse::<Decimal>() {
*record.lock_mut().value_err_msg.lock_mut() = None;
record.lock_mut().record.value_in_euro = money;
return
}
}
*record.lock_mut().value_err_msg.lock_mut() = Some(VALUE_NOT_VALID_ERR_MSG);
record.lock_mut().record.value_in_euro = Decimal::ZERO;
let _ = element.focus();
}))
})
}))
.child(html!("span", {
.style("color", "red")
.style("font-size", "small")
.text_signal(record.lock_ref().value_err_msg.signal_ref(|t| t.unwrap_or("")))
}))
}))
})
}))
}

fn quantity_cell(record: &Mutable<Aeat720RecordInfo>) -> impl Signal<Item = Option<Dom>> {
record.signal_ref(move |r| {
record.signal_ref(clone!(record => move |r| {
Some(html!("td", {
.child(html!("input", {
.child(html!("input" => HtmlInputElement, {
.style("text-align", "right")
.attr("type", "text")
.attr("size", "6")
.attr("maxlength", "15")
.attr("value", &decimal_to_str_locale(&r.record.quantity, DEFAULT_LOCALE))
.with_node!(element => {
.event(clone!(record => move |_: events::Input| {
if valid_str_number_with_decimals(&element.value(), DEFAULT_NUMBER_OF_DECIMALS, DEFAULT_LOCALE) {
*record.lock_mut().quantity_err_msg.lock_mut() = None;
} else {
*record.lock_mut().quantity_err_msg.lock_mut() = Some(QUANTITY_NOT_VALID_ERR_MSG);
}
}))
})
.with_node!(element => {
.event(clone!(record => move |_: events::Change| {
let quantity_str = element.value();
if valid_str_number_with_decimals(&quantity_str, DEFAULT_NUMBER_OF_DECIMALS, DEFAULT_LOCALE) {
if let Ok(quantity) = quantity_str.parse::<Decimal>() {
*record.lock_mut().quantity_err_msg.lock_mut() = None;
record.lock_mut().record.quantity = quantity;
return
}
}
*record.lock_mut().quantity_err_msg.lock_mut() = Some(QUANTITY_NOT_VALID_ERR_MSG);
record.lock_mut().record.quantity = Decimal::ONE_HUNDRED;
let _ = element.focus();
}))
})
}))
.child(html!("span", {
.style("color", "red")
.style("font-size", "small")
.text_signal(record.lock_ref().quantity_err_msg.signal_ref(|t| t.unwrap_or("")))
}))
}))
})
}))
}

fn percentage_cell(record: &Mutable<Aeat720RecordInfo>) -> impl Signal<Item = Option<Dom>> {
record.signal_ref(move |r| {
record.signal_ref(clone!(record => move |r| {
Some(html!("td", {
.child(html!("input", {
.child(html!("input" => HtmlInputElement, {
.style("text-align", "right")
.attr("type", "text")
.attr("size", "4")
.attr("maxlength", "6")
.attr("value", &r.record.percentage.to_string())
.with_node!(element => {
.event(clone!(record => move |_: events::Input| {
if valid_str_number_with_decimals(&element.value(), DEFAULT_NUMBER_OF_DECIMALS, DEFAULT_LOCALE) {
*record.lock_mut().percent_err_msg.lock_mut() = None;
} else {
*record.lock_mut().percent_err_msg.lock_mut() = Some(PERCENT_NOT_VALID_ERR_MSG);
}
}))
})
.with_node!(element => {
.event(clone!(record => move |_: events::Change| {
let percentage_str = element.value().replace(DEFAULT_LOCALE.decimal(), ".");
if valid_str_number_with_decimals(&percentage_str, DEFAULT_NUMBER_OF_DECIMALS, DEFAULT_LOCALE) {
if let Ok(percentage) = percentage_str.parse::<Decimal>() {
if percentage.gt(&Decimal::ZERO) && percentage.le(&Decimal::ONE_HUNDRED) {
*record.lock_mut().percent_err_msg.lock_mut() = None;
record.lock_mut().record.percentage = percentage;
return;
}
}
}
*record.lock_mut().percent_err_msg.lock_mut() = Some(PERCENT_NOT_VALID_ERR_MSG);
record.lock_mut().record.percentage = Decimal::ONE_HUNDRED;
let _ = element.focus();
}))
})
}))
.text(" % ")
.child(html!("span", {
.style("color", "red")
.style("font-size", "small")
.text_signal(record.lock_ref().percent_err_msg.signal_ref(|t| t.unwrap_or("")))
}))
.text("%")
}))
})
}))
}

fn actions_cell(
Expand Down
41 changes: 41 additions & 0 deletions src/utils/decimal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,29 @@ pub fn decimal_to_str_locale(number: &Decimal, locale: &Locale) -> String {
result
}

pub fn valid_str_number_with_decimals(number: &str, decimal_number: u16, locale: &Locale) -> bool {
let mut state = 0; // 0 integer part, 1 decimal part
let mut decimals = 0;
for c in number.chars() {
if c.is_numeric() {
if state == 1 {
decimals += 1;
if decimals > decimal_number {
return false;
}
}
} else if state == 0 {
if locale.decimal().starts_with(c) || c == '.' {
state = 1;
} else {
return false;
}
}
}

true
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -27,4 +50,22 @@ mod tests {
let x = Decimal::new(2314, 2);
assert_eq!("23,14", decimal_to_str_locale(&x, &Locale::es));
}

#[test]
fn test_valid_str_number_with_decimals() {
assert!(valid_str_number_with_decimals("23,14", 2, &Locale::es));
assert!(valid_str_number_with_decimals("2333.14", 2, &Locale::es));
assert_eq!(
false,
valid_str_number_with_decimals("323,143", 2, &Locale::es)
);
assert_eq!(

Check warning on line 62 in src/utils/decimal.rs

View workflow job for this annotation

GitHub Actions / Format & Clippy (1.80.1)

used `assert_eq!` with a literal bool
false,
valid_str_number_with_decimals("423h14", 2, &Locale::es)
);
assert_eq!(
false,
valid_str_number_with_decimals("5a23.14", 2, &Locale::es)
);
}
}

1 comment on commit 2744283

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.