use super::{
AfFormat, Keystroke, KeystrokeResult, Thrown, decimal_mark_for_style,
digit_separator_for_style, is_style_with_comma_decimal_mark, is_style_with_digit_separator,
merge_change, valid_style_or_zero,
};
use crate::error::{AfColor, AlertMessage, Error};
use crate::parse::{c_atof, is_decimal_digit, is_number, normalize_decimal_mark, trim_spaces};
const DOUBLE_CORRECT: f64 = 0.000_000_000_000_001;
const DOUBLE_DIGITS10: i32 = 15;
pub(crate) fn format_fixed(value: f64, prec: usize) -> String {
if !value.is_finite() {
return "0".to_string();
}
format!("{value:.prec$}")
}
pub(crate) fn calculate_string(d_value: f64, i_dec: i32) -> (String, bool, usize) {
let negative = d_value < 0.0;
let mag = if negative { -d_value } else { d_value };
let prec = usize::try_from(i_dec.clamp(0, DOUBLE_DIGITS10)).unwrap_or(0);
let formatted = format_fixed(mag, prec);
let int_end = formatted.find('.').unwrap_or(formatted.len());
(formatted, negative, int_end)
}
pub(crate) fn insert_thousands(s: &mut String, int_end: usize, stop: usize, sep: char) {
let mut at = int_end;
while at > stop.saturating_add(3) {
at = at.saturating_sub(3);
if at > s.len() {
break;
}
s.insert(at, sep);
}
}
#[must_use]
pub fn af_number_format(
value: &str,
n_dec: i32,
sep_style: i32,
neg_style: i32,
currency: &str,
currency_prepend: bool,
) -> AfFormat {
let trimmed = trim_spaces(value);
if trimmed.is_empty() {
return AfFormat::unchanged();
}
let i_dec = i32::try_from(n_dec.unsigned_abs()).unwrap_or(i32::MAX);
let i_sep = valid_style_or_zero(sep_style);
let i_neg = valid_style_or_zero(neg_style);
let normalized = normalize_decimal_mark(trimmed);
let mut d_value = c_atof(&normalized);
if i_dec > 0 {
d_value += DOUBLE_CORRECT;
}
let i_dec_clamped = i_dec.clamp(0, DOUBLE_DIGITS10);
let (mut str_value, b_negative, i_dec2) = calculate_string(d_value, i_dec_clamped);
if i_dec2 < str_value.len() {
if is_style_with_comma_decimal_mark(i_sep) {
str_value = str_value.replace('.', ",");
}
if i_dec2 == 0 {
str_value.insert(0, '0');
}
}
if is_style_with_digit_separator(i_sep) {
let sep = digit_separator_for_style(i_sep);
insert_thousands(&mut str_value, i_dec2, 0, sep);
}
let mut out = if currency_prepend {
let mut s = String::from(currency);
s.push_str(&str_value);
s
} else {
let mut s = str_value;
s.push_str(currency);
s
};
let wants_red_style = i_neg == 1 || i_neg == 3;
if b_negative {
if i_neg == 0 {
out.insert(0, '-');
} else if i_neg == 2 || i_neg == 3 {
out.insert(0, '(');
out.push(')');
}
}
let formatted = AfFormat::formatted(out);
if wants_red_style {
let color = if b_negative {
AfColor::RED
} else {
AfColor::BLACK
};
formatted.with_color(color)
} else {
formatted
}
}
pub fn af_number_keystroke(
event: &Keystroke,
_n_dec: i32,
sep_style: i32,
) -> Result<KeystrokeResult, Thrown> {
const CALLER: &str = "AFNumber_Keystroke";
if event.will_commit {
let sw_temp = trim_spaces(&event.value);
if sw_temp.is_empty() {
return Ok(KeystrokeResult::accept());
}
let normalized = normalize_decimal_mark(sw_temp);
if !is_number(&normalized) {
return Err(Thrown::alerting(
Error::InvalidInput,
CALLER,
AlertMessage::InvalidInput,
));
}
return Ok(KeystrokeResult::accept());
}
let selected: String = match usize::try_from(event.sel_start) {
Ok(start) => {
let end = usize::try_from(event.sel_end.max(event.sel_start)).unwrap_or(start);
event
.value
.chars()
.skip(start)
.take(end.saturating_sub(start))
.collect()
}
Err(_) => String::new(),
};
let mut has_sign = event.value.contains('-') && !selected.contains('-');
if has_sign && !selected.is_empty() && event.sel_start == 0 {
return Ok(KeystrokeResult::reject());
}
let i_sep = valid_style_or_zero(sep_style);
let c_sep = decimal_mark_for_style(i_sep);
let mut has_sep = event.value.contains(c_sep);
for (index, ch) in event.change.chars().enumerate() {
if ch == c_sep {
if has_sep {
return Ok(KeystrokeResult::reject());
}
has_sep = true;
continue;
}
if ch == '-' {
if has_sign || index != 0 || event.sel_start != 0 {
return Ok(KeystrokeResult::reject());
}
has_sign = true;
continue;
}
if !is_decimal_digit(ch) {
return Ok(KeystrokeResult::reject());
}
}
let merged = merge_change(&event.value, &event.change, event.sel_start, event.sel_end);
Ok(KeystrokeResult::accept_value(merged))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::af::AfOutcome;
fn fmt(value: &str, n_dec: i32, sep: i32, neg: i32, currency: &str, prepend: bool) -> String {
match af_number_format(value, n_dec, sep, neg, currency, prepend).outcome {
AfOutcome::Formatted(s) => s,
AfOutcome::Unchanged => value.to_string(),
AfOutcome::Rejected => unreachable!("AFNumber_Format never rejects"),
}
}
#[test]
fn empty_after_trim_leaves_the_value_alone() {
for value in ["", " "] {
let out = af_number_format(value, 2, 0, 0, "", false);
assert_eq!(out.outcome, AfOutcome::Unchanged, "{value:?}");
assert!(out.effects.is_empty());
}
}
#[test]
fn transcript_lines() {
assert_eq!(fmt("blooey", 0, 1, 0, "", false), "0");
assert_eq!(fmt("12", 0, 1, 0, "", false), "12");
}
#[test]
fn separator_styles() {
assert_eq!(fmt("1234.5", 2, 0, 0, "", false), "1,234.50");
assert_eq!(fmt("1234.5", 2, 1, 0, "", false), "1234.50");
assert_eq!(fmt("1234.5", 2, 2, 0, "", false), "1.234,50");
assert_eq!(fmt("1234.5", 2, 3, 0, "", false), "1234,50");
assert_eq!(fmt("1234567.5", 2, 0, 0, "", false), "1,234,567.50");
}
#[test]
fn out_of_range_separator_style_clamps_to_zero() {
for style in [4, 5, -1, i32::MIN, i32::MAX] {
assert_eq!(
fmt("1234", 0, style, 0, "", false),
"1,234",
"style {style}"
);
}
}
#[test]
fn negative_styles_split_the_sign_between_text_and_colour() {
assert_eq!(fmt("-12.5", 2, 0, 0, "", false), "-12.50");
assert_eq!(fmt("-12.5", 2, 0, 1, "", false), "12.50");
assert_eq!(fmt("-12.5", 2, 0, 2, "", false), "(12.50)");
assert_eq!(fmt("-12.5", 2, 0, 3, "", false), "(12.50)");
assert_eq!(
af_number_format("-12.5", 2, 0, 1, "", false)
.effects
.text_color,
Some(AfColor::RED)
);
assert_eq!(
af_number_format("-12.5", 2, 0, 3, "", false)
.effects
.text_color,
Some(AfColor::RED)
);
for style in [0, 2] {
assert_eq!(
af_number_format("-12.5", 2, 0, style, "", false)
.effects
.text_color,
None,
"style {style}"
);
}
}
#[test]
fn red_styles_ask_for_black_when_the_value_is_not_negative() {
for style in [1, 3] {
assert_eq!(
af_number_format("12.5", 2, 0, style, "", false)
.effects
.text_color,
Some(AfColor::BLACK),
"style {style}"
);
}
}
#[test]
fn currency_is_inside_the_parentheses() {
assert_eq!(fmt("12", 2, 0, 0, "$", true), "$12.00");
assert_eq!(fmt("12", 2, 0, 0, " EUR", false), "12.00 EUR");
assert_eq!(fmt("-12", 0, 0, 0, "$", true), "-$12");
assert_eq!(fmt("-12.5", 2, 0, 2, "$", true), "($12.50)");
assert_eq!(fmt("-12.5", 2, 0, 3, "$", true), "($12.50)");
}
#[test]
fn commas_become_periods_before_parsing() {
assert_eq!(fmt("1,5", 1, 0, 0, "", false), "1.5");
assert_eq!(fmt("1,234.5", 2, 0, 0, "", false), "1.23");
}
#[test]
fn sub_unit_values_keep_a_leading_zero() {
assert_eq!(fmt(".5", 1, 0, 0, "", false), "0.5");
assert_eq!(fmt(".5", 2, 0, 0, "", false), "0.50");
assert_eq!(fmt(".123456", 4, 0, 0, "", false), "0.1235");
assert_eq!(fmt(".5", 1, 2, 0, "", false), "0,5");
}
#[test]
fn commit_of_a_non_number_notifies_and_throws() {
let thrown = af_number_keystroke(&Keystroke::commit("abc"), 1, 2).unwrap_err();
assert_eq!(thrown.error, Error::InvalidInput);
assert_eq!(thrown.error.to_string(), "The input value is invalid.");
assert_eq!(
thrown.effects.alerts.first().map(ToString::to_string),
Some("AFNumber_Keystroke[icon=3,type=0]: The input value is invalid.".to_string())
);
}
#[test]
fn commit_of_a_number_or_of_nothing_is_accepted() {
for value in ["123", "", " ", "560,024", "-1.23e+23"] {
let out = af_number_keystroke(&Keystroke::commit(value), 1, 2);
assert!(out.is_ok(), "{value:?}");
}
}
#[test]
fn keystroke_rejections_are_silent() {
let cases: &[(&str, &str, i32)] = &[
(".2", ".", 2), ("12", "a", 2), ("12", "-", 1), ("-12", "-", 0), ("12", "1-", 2), ];
for &(value, change, caret) in cases {
let out = af_number_keystroke(&Keystroke::insert(value, change, caret), 1, 0).unwrap();
assert!(out.is_reject(), "{value:?} + {change:?}");
assert!(out.effects.is_empty(), "{value:?} + {change:?}");
}
}
#[test]
fn accepted_keystrokes_hand_back_the_merged_value() {
for &(value, change, caret, expected) in &[
("12", "3", 2, "123"),
("12", "-", 0, "-12"),
("1", ".", 1, "1."),
] {
let out = af_number_keystroke(&Keystroke::insert(value, change, caret), 1, 0).unwrap();
assert_eq!(out.value(), Some(expected), "{value:?} + {change:?}");
}
}
#[test]
fn the_separator_style_decides_which_mark_is_the_decimal_one() {
let out = af_number_keystroke(&Keystroke::insert("1,2", ",", 3), 1, 2).unwrap();
assert!(out.is_reject());
let out = af_number_keystroke(&Keystroke::insert("1,2", ".", 3), 1, 2).unwrap();
assert!(out.is_reject());
}
#[test]
fn a_selected_minus_no_longer_blocks_the_front() {
let out = af_number_keystroke(&Keystroke::replace("-12", "9", 0, 1), 1, 0).unwrap();
assert_eq!(out.value(), Some("912"));
}
}