Merge pull request #636 from MarekSpodBiedry:issue-519

This commit is contained in:
Brandon Thomas 2026-10-12 23:21:58 -04:00
commit 1fa38e583e
3 changed files with 97 additions and 1 deletions

View File

@ -336,10 +336,46 @@ fn num_data(tag: &str, f: Option<&str>, wb: &Workbook, si: usize) -> String {
s
}
/// Splits the inside of an array constant on `,` and `;` outside double quotes; inside quotes `""` is a literal quote.
/// Unquoted items are trimmed; quoted ones keep the text between the quotes and drop the spaces around them.
fn split_array_items(inner: &str) -> Vec<String> {
let mut items = Vec::new();
let mut cur = String::new();
let mut quoted = false;
let mut in_quote = false;
let mut chars = inner.chars().peekable();
let finish = |cur: &mut String, quoted: &mut bool| {
let text = std::mem::take(cur);
let item = if *quoted { text } else { text.trim().to_string() };
*quoted = false;
item
};
while let Some(c) = chars.next() {
match c {
'"' if in_quote && chars.peek() == Some(&'"') => {
cur.push('"');
chars.next();
}
'"' => {
if !quoted && cur.trim().is_empty() {
cur.clear();
}
in_quote = !in_quote;
quoted = true;
}
',' | ';' if !in_quote => items.push(finish(&mut cur, &mut quoted)),
c if quoted && !in_quote && c.is_whitespace() => {}
_ => cur.push(c),
}
}
items.push(finish(&mut cur, &mut quoted));
items
}
fn str_data(tag: &str, f: &str, wb: &Workbook, si: usize) -> String {
let f = f.trim();
if f.starts_with('{') {
let vals: Vec<String> = f.trim_matches(|c| c == '{' || c == '}').split([',', ';']).map(|v| v.trim().trim_matches('"').to_string()).collect();
let vals = split_array_items(f.trim_matches(|c| c == '{' || c == '}'));
let mut s = format!("<c:{tag}><c:strLit><c:ptCount val=\"{}\"/>", vals.len());
for (i, v) in vals.iter().enumerate() {
let _ = write!(s, "<c:pt idx=\"{i}\"><c:v>{}</c:v></c:pt>", esc(v));

View File

@ -0,0 +1,59 @@
//! Literal chart categories that contain a comma, a semicolon or a quote stay one category on save.
use gridcraft_model::{Anchor, Chart, ChartKind, LegendPos, Series, Sheet, Workbook};
use std::sync::Arc;
use crate::{read_xlsx, write_xlsx};
#[test]
fn literal_category_with_separators_survives_save() {
// (typed array constant, what reading the saved file gives back)
let cats = [
(r#"{"a,b"}"#, r#"{"a,b"}"#),
(r#"{"a;b"}"#, r#"{"a;b"}"#),
(r#"{"red","blue"}"#, r#"{"red","blue"}"#),
(r#"{"say ""hi"", ok"}"#, r#"{"say ""hi"", ok"}"#),
(r#"{"a,b","c;d","e"}"#, r#"{"a,b","c;d","e"}"#),
(r#"{ "Jan" , "Feb, Mar" ; Apr }"#, r#"{"Jan","Feb, Mar","Apr"}"#),
];
let mut wb = Workbook::default();
let mut s = Sheet::new("Data");
let series: Vec<Series> = cats
.iter()
.map(|(c, _)| Series {
name: Some("North, South".into()),
categories: Some((*c).into()),
values: "{1,2,3}".into(),
bubble_sizes: None,
color: None,
secondary: false,
kind: None,
smooth: false,
trendlines: Vec::new(),
})
.collect();
s.charts.push(Chart {
id: 1,
kind: ChartKind::ColumnClustered,
anchor: Anchor { cell: gridcraft_core::CellRef::new(0, 3), width: 480.0, height: 300.0, ..Anchor::default() },
title: None,
series,
legend: LegendPos::Bottom,
data_labels: false,
gridlines: true,
style: 2,
x_title: None,
y_title: None,
source: None,
by_rows: false,
pivot: None,
});
wb.sheets = vec![Arc::new(s)];
let bytes = write_xlsx(&wb).unwrap();
let (back, _rep) = read_xlsx(&bytes).unwrap();
let saved = &back.sheets[0].charts[0].series;
for (i, (_, want)) in cats.iter().enumerate() {
assert_eq!(saved[i].categories.as_deref(), Some(*want));
assert_eq!(saved[i].name.as_deref(), Some("North, South"));
}
}

View File

@ -4,6 +4,7 @@ mod array_export;
mod csv_tests;
mod encrypted_tests;
mod fixtures;
mod literal_category_roundtrip;
mod malformed;
mod ods_tests;
mod passthrough_tests;