diff --git a/src/helper/number_format.rs b/src/helper/number_format.rs index 185d5b17..120415ed 100644 --- a/src/helper/number_format.rs +++ b/src/helper/number_format.rs @@ -78,18 +78,25 @@ pub fn to_formatted_string, P: AsRef>(value: S, format: P) -> _ => {} } - // Convert any other escaped characters to quoted strings, e.g. (\T to "T") + let Ok(parsed_val) = value.parse::() else { + return value.to_string(); + }; + // Inspect the selected raw section before escaped characters are rewritten. + // This allows placeholder-free sections to retain their literal meaning. + let raw_sections: Vec<&str> = split(&SECTION_REGEX, &format).collect(); + let (_, raw_split_format, _) = split_format(raw_sections, &parsed_val); + if let Some(literal) = literal_only_section(&raw_split_format) { + return literal.trim().to_string(); + } + + // Convert any other escaped characters to quoted strings, e.g. (\T to "T") let mut format = ESCAPE_REGEX.replace_all(&format, r#""$0""#); // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal) let sections: Vec<&str> = split(&SECTION_REGEX, &format).collect(); - let Ok(parsed_val) = value.parse::() else { - return value.to_string(); - }; - let (_, split_format, split_value) = split_format(sections, &parsed_val); format = Cow::Owned(split_format); value = Cow::Owned(split_value); @@ -132,6 +139,54 @@ pub fn to_formatted_string, P: AsRef>(value: S, format: P) -> value.trim().to_string() } +/// Decode a section that contains literals but no value placeholder. +/// +/// Excel uses these sections for displays such as a dash in place of zero: +/// `#,##0;[Red](#,##0);\-\ \ `. Quoted text and backslash escapes are +/// literal. `_x` becomes one space, while `*x` contributes one fill character +/// because this string formatter has no cell width. Value-dependent formats +/// stay on the existing date/number paths. +fn literal_only_section(format: &str) -> Option { + if DATE_TIME_REGEX.is_match(format).unwrap_or(false) { + return None; + } + + let mut result = String::with_capacity(format.len()); + let mut chars = format.chars(); + let mut in_quotes = false; + + while let Some(ch) = chars.next() { + if in_quotes { + if ch == '"' { + in_quotes = false; + } else { + result.push(ch); + } + continue; + } + + match ch { + '"' => in_quotes = true, + '\\' | '*' => result.push(chars.next()?), + '_' => { + chars.next()?; + result.push(' '); + } + '[' => { + for control in chars.by_ref() { + if control == ']' { + break; + } + } + } + '0' | '#' | '?' | '@' | '%' => return None, + _ => result.push(ch), + } + } + + (!in_quotes).then_some(result) +} + fn split_format(sections: Vec<&str>, value: &f64) -> (String, String, String) { let mut converted_sections: Vec = Vec::new(); @@ -435,3 +490,11 @@ fn test_to_formatted_string_quoted_number_section_still_normalises() { // A quoted numeric literal keeps its existing behaviour. assert_eq!(to_formatted_string("5", r#""123""#), "123"); } + +#[test] +fn test_to_formatted_string_zero_section_with_only_escaped_literals() { + let format = r"#,##0_);[Red]\(#,##0\);\-\ \ "; + + assert_eq!("-", to_formatted_string("0", format)); + assert_eq!("", to_formatted_string("0", "0;-0;")); +} diff --git a/src/helper/number_format/number_formater.rs b/src/helper/number_format/number_formater.rs index afecd2b3..1c59dd2f 100644 --- a/src/helper/number_format/number_formater.rs +++ b/src/helper/number_format/number_formater.rs @@ -236,7 +236,11 @@ pub(crate) fn round_decimal_string(value: &str, decimals: usize) -> String { let all: String = kept.iter().map(|d| char::from(b'0' + d)).collect(); let split_at = all.len() - decimals; let int_digits = all[..split_at].trim_start_matches('0'); - let int_digits = if int_digits.is_empty() { "0" } else { int_digits }; + let int_digits = if int_digits.is_empty() { + "0" + } else { + int_digits + }; let mut result = format!("{sign}{int_digits}"); if decimals > 0 { result.push('.'); diff --git a/src/structs/cell_format.rs b/src/structs/cell_format.rs index 9797b410..77870ed7 100644 --- a/src/structs/cell_format.rs +++ b/src/structs/cell_format.rs @@ -352,7 +352,9 @@ mod tests { // rewrote each xfId as 0. #[test] fn reads_the_xf_id() { - let obj = read_xf(r#""#); + let obj = read_xf( + r#""#, + ); assert_eq!(*obj.get_format_id(), 6); assert_eq!(*obj.get_number_format_id(), 9); }