diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..89c8d36 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,155 @@ +// Based on https://stackoverflow.com/a/65266882 +pub fn scientific(f: f64, precision: usize) -> String { + let mut ret = format!("{:.precision$E}", f, precision = precision); + let exp = ret.split_off(ret.find('E').unwrap_or(0)); + let (exp_sign, exp) = if let Some(stripped) = exp.strip_prefix("E-") { + ('-', stripped) + } else { + ('+', &exp[1..]) + }; + + let sign = if !ret.starts_with('-') { " " } else { "" }; + format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2) +} + +pub fn engineering(f: f64, precision: usize) -> String { + // Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string + // 1,000 => 1000E3 + let all = format!(" {:.precision$E}", f, precision = precision) + // Remove . since it can be moved + .replacen(".", "", 1) + // Add 00E before E here so the length is enough for slicing below + .replacen("E", "00E", 1); + // Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E + // 1000E3 => (1000, E3) + let (num_str, exp_str) = all.split_at(all.find('E').unwrap()); + // Extract the exponent as an isize. This should always be true because f64 max will be ~400 + // E3 => 3 as isize + let exp = exp_str[1..].parse::().unwrap(); + // Sign of the exponent. If string representation starts with E-, then negative + let display_exp_sign = if let Some(stripped) = exp_str.strip_prefix("E-") { + '-' + } else { + '+' + }; + + // The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above + // 100 => 0, 1000 => 3, .1 => 3 (but will show as -3) + let display_exp = (exp.div_euclid(3) * 3).abs(); + // Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility + let num_whole_digits = exp.rem_euclid(3) as usize + 1; + + // If this is a negative number, strip off the added space, otherwise keep the space (and next digit) + let num_str = if num_str.strip_prefix(" -").is_some() { + &num_str[1..] + } else { + num_str + }; + + // Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit) + // Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10 + let whole = &num_str[0..(num_whole_digits + 1)]; + // Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2 + let decimal = &num_str[(num_whole_digits + 1)..(precision + num_whole_digits + 1)]; + // Right align whole portion, always have decimal point + format!( + "{: >4}.{} E{}{:0>pad$}", + // display_sign, + whole, + decimal, + display_exp_sign, + display_exp, + pad = 2 + ) +} + +pub fn separated(f: f64, sep: char) -> String { + let mut ret = f.to_string(); + let start = if ret.starts_with('-') { 1 } else { 0 }; + let end = ret.find('.').unwrap_or_else(|| ret.len()); + for i in 0..((end - start - 1).div_euclid(3)) { + ret.insert(end - (i + 1) * 3, sep); + } + ret +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_fmt_scientific() { + for (f, p, s) in vec![ + // Basic + (1.0, 0, " 1 E+00"), + (-1.0, 0, "-1 E+00"), + (100.0, 0, " 1 E+02"), + (0.1, 0, " 1 E-01"), + (0.01, 0, " 1 E-02"), + (-0.1, 0, "-1 E-01"), + // i + (1.0, 0, " 1 E+00"), + // Precision + (-0.123456789, 3, "-1.235 E-01"), + (-0.123456789, 2, "-1.23 E-01"), + (-0.123456789, 2, "-1.23 E-01"), + (-1e99, 2, "-1.00 E+99"), + (-1e100, 2, "-1.00 E+100"), + // Rounding + (0.5, 2, " 5.00 E-01"), + (0.5, 1, " 5.0 E-01"), + (0.5, 0, " 5 E-01"), + (1.5, 2, " 1.50 E+00"), + (1.5, 1, " 1.5 E+00"), + (1.5, 0, " 2 E+00"), + ] { + assert_eq!(fmt_scientific(f, p), s); + } + } + + #[test] + fn test_fmt_separated() { + for (f, c, s) in vec![ + (100.0, ',', "100"), + (100.0, ',', "100"), + (-100.0, ',', "-100"), + (1_000.0, ',', "1,000"), + (-1_000.0, ',', "-1,000"), + (10_000.0, ',', "10,000"), + (-10_000.0, ',', "-10,000"), + (100_000.0, ',', "100,000"), + (-100_000.0, ',', "-100,000"), + (1_000_000.0, ',', "1,000,000"), + (-1_000_000.0, ',', "-1,000,000"), + (1_000_000.123456789, ',', "1,000,000.123456789"), + (-1_000_000.123456789, ',', "-1,000,000.123456789"), + (1_000_000.123456789, ' ', "1 000 000.123456789"), + (1_000_000.123456789, ' ', "1 000 000.123456789"), + ] { + assert_eq!(fmt_separated(f, c), s); + } + } + + #[test] + fn test_fmt_engineering() { + for (f, c, s) in vec![ + (100.0, 3, " 100.000 E+00"), + (100.0, 3, " 100.000 E+00"), + (-100.0, 3, "-100.000 E+00"), + (100.0, 0, " 100. E+00"), + (-100.0, 0, "-100. E+00"), + (0.1, 2, " 100.00 E-03"), + (0.01, 2, " 10.00 E-03"), + (0.001, 2, " 1.00 E-03"), + (0.0001, 2, " 100.00 E-06"), + // Rounding + (0.5, 2, " 500.00 E-03"), + (0.5, 1, " 500.0 E-03"), + (0.5, 0, " 500. E-03"), + (1.5, 2, " 1.50 E+00"), + (1.5, 1, " 1.5 E+00"), + (1.5, 0, " 2. E+00"), + ] { + assert_eq!(fmt_engineering(f, c), s); + } + } +} diff --git a/src/main.rs b/src/main.rs index 4e371e5..502ca6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod calc; mod util; +mod format; use calc::constants::{CalculatorDisplayMode, CalculatorState, RegisterState}; use calc::errors::CalculatorResult; @@ -161,13 +162,13 @@ fn main() -> Result<(), Box> { match app.calculator.get_display_mode() { CalculatorDisplayMode::Default => format!("{:>2}: {}", i, *m), CalculatorDisplayMode::Separated { separator } => { - format!("{:>2}: {}", i, fmt_separated(*m, *separator)) + format!("{:>2}: {}", i, format::separated(*m, *separator)) } CalculatorDisplayMode::Scientific { precision } => { - format!("{:>2}: {}", i, fmt_scientific(*m, *precision)) + format!("{:>2}: {}", i, format::scientific(*m, *precision)) } CalculatorDisplayMode::Engineering { precision } => { - format!("{:>2}: {}", i, fmt_engineering(*m, *precision)) + format!("{:>2}: {}", i, format::engineering(*m, *precision)) } CalculatorDisplayMode::Fixed { precision } => { format!("{:>2}: {:.precision$}", i, m, precision = precision) @@ -486,158 +487,3 @@ fn draw_clippy_rect(c: ClippyRectangle, f: &mut Frame String { - let mut ret = format!("{:.precision$E}", f, precision = precision); - let exp = ret.split_off(ret.find('E').unwrap_or(0)); - let (exp_sign, exp) = if let Some(stripped) = exp.strip_prefix("E-") { - ('-', stripped) - } else { - ('+', &exp[1..]) - }; - - let sign = if !ret.starts_with('-') { " " } else { "" }; - format!("{}{} E{}{:0>pad$}", sign, ret, exp_sign, exp, pad = 2) -} - -fn fmt_engineering(f: f64, precision: usize) -> String { - // Format the string so the first digit is always in the first column, and remove '.'. Requested precision + 2 to account for using 1, 2, or 3 digits for the whole portion of the string - // 1,000 => 1000E3 - let all = format!(" {:.precision$E}", f, precision = precision) - // Remove . since it can be moved - .replacen(".", "", 1) - // Add 00E before E here so the length is enough for slicing below - .replacen("E", "00E", 1); - // Extract mantissa and the string representation of the exponent. Unwrap should be safe as formatter will insert E - // 1000E3 => (1000, E3) - let (num_str, exp_str) = all.split_at(all.find('E').unwrap()); - // Extract the exponent as an isize. This should always be true because f64 max will be ~400 - // E3 => 3 as isize - let exp = exp_str[1..].parse::().unwrap(); - // Sign of the exponent. If string representation starts with E-, then negative - let display_exp_sign = if let Some(stripped) = exp_str.strip_prefix("E-") { - '-' - } else { - '+' - }; - - // The exponent to display. Always a multiple of 3 in engineering mode. Always positive because sign is added with display_exp_sign above - // 100 => 0, 1000 => 3, .1 => 3 (but will show as -3) - let display_exp = (exp.div_euclid(3) * 3).abs(); - // Number of whole digits. Always 1, 2, or 3 depending on exponent divisibility - let num_whole_digits = exp.rem_euclid(3) as usize + 1; - - // If this is a negative number, strip off the added space, otherwise keep the space (and next digit) - let num_str = if num_str.strip_prefix(" -").is_some() { - &num_str[1..] - } else { - num_str - }; - - // Whole portion of number. Slice is safe because the num_whole_digits is always 3 and the num_str will always have length >= 3 since precision in all=2 (+original whole digit) - // Original number is 1,000 => whole will be 1, if original is 0.01, whole will be 10 - let whole = &num_str[0..(num_whole_digits + 1)]; - // Decimal portion of the number. Sliced from the number of whole digits to the *requested* precision. Precision generated in all will be requested precision + 2 - let decimal = &num_str[(num_whole_digits + 1)..(precision + num_whole_digits + 1)]; - // Right align whole portion, always have decimal point - format!( - "{: >4}.{} E{}{:0>pad$}", - // display_sign, - whole, - decimal, - display_exp_sign, - display_exp, - pad = 2 - ) -} - -fn fmt_separated(f: f64, sep: char) -> String { - let mut ret = f.to_string(); - let start = if ret.starts_with('-') { 1 } else { 0 }; - let end = ret.find('.').unwrap_or_else(|| ret.len()); - for i in 0..((end - start - 1).div_euclid(3)) { - ret.insert(end - (i + 1) * 3, sep); - } - ret -} - -#[cfg(test)] -mod tests { - use super::*; - #[test] - fn test_fmt_scientific() { - for (f, p, s) in vec![ - // Basic - (1.0, 0, " 1 E+00"), - (-1.0, 0, "-1 E+00"), - (100.0, 0, " 1 E+02"), - (0.1, 0, " 1 E-01"), - (0.01, 0, " 1 E-02"), - (-0.1, 0, "-1 E-01"), - // i - (1.0, 0, " 1 E+00"), - // Precision - (-0.123456789, 3, "-1.235 E-01"), - (-0.123456789, 2, "-1.23 E-01"), - (-0.123456789, 2, "-1.23 E-01"), - (-1e99, 2, "-1.00 E+99"), - (-1e100, 2, "-1.00 E+100"), - // Rounding - (0.5, 2, " 5.00 E-01"), - (0.5, 1, " 5.0 E-01"), - (0.5, 0, " 5 E-01"), - (1.5, 2, " 1.50 E+00"), - (1.5, 1, " 1.5 E+00"), - (1.5, 0, " 2 E+00"), - ] { - assert_eq!(fmt_scientific(f, p), s); - } - } - - #[test] - fn test_fmt_separated() { - for (f, c, s) in vec![ - (100.0, ',', "100"), - (100.0, ',', "100"), - (-100.0, ',', "-100"), - (1_000.0, ',', "1,000"), - (-1_000.0, ',', "-1,000"), - (10_000.0, ',', "10,000"), - (-10_000.0, ',', "-10,000"), - (100_000.0, ',', "100,000"), - (-100_000.0, ',', "-100,000"), - (1_000_000.0, ',', "1,000,000"), - (-1_000_000.0, ',', "-1,000,000"), - (1_000_000.123456789, ',', "1,000,000.123456789"), - (-1_000_000.123456789, ',', "-1,000,000.123456789"), - (1_000_000.123456789, ' ', "1 000 000.123456789"), - (1_000_000.123456789, ' ', "1 000 000.123456789"), - ] { - assert_eq!(fmt_separated(f, c), s); - } - } - - #[test] - fn test_fmt_engineering() { - for (f, c, s) in vec![ - (100.0, 3, " 100.000 E+00"), - (100.0, 3, " 100.000 E+00"), - (-100.0, 3, "-100.000 E+00"), - (100.0, 0, " 100. E+00"), - (-100.0, 0, "-100. E+00"), - (0.1, 2, " 100.00 E-03"), - (0.01, 2, " 10.00 E-03"), - (0.001, 2, " 1.00 E-03"), - (0.0001, 2, " 100.00 E-06"), - // Rounding - (0.5, 2, " 500.00 E-03"), - (0.5, 1, " 500.0 E-03"), - (0.5, 0, " 500. E-03"), - (1.5, 2, " 1.50 E+00"), - (1.5, 1, " 1.5 E+00"), - (1.5, 0, " 2. E+00"), - ] { - assert_eq!(fmt_engineering(f, c), s); - } - } -}