1use std::sync::Arc;
4
5use crate::base::date::{days_in_month_in, serial_from_ymd};
6use crate::base::parse::{Parsed, parse_input};
7use crate::base::{CellRef, DateSystem, MAX_COLS, MAX_ROWS, Value, col_to_letters};
8use crate::model::{Cell, NumFmt, Sheet, StyleId, Workbook};
9
10use crate::xlsx::{CsvOptions, IoError, ReadReport};
11
12pub const MAX_CSV_BYTES: usize = 512 << 20;
14
15const CP1252: [u32; 32] = [
17 0x20AC, 0x81, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0160, 0x2039, 0x0152, 0x8D, 0x017D, 0x8F, 0x90, 0x2018, 0x2019,
18 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x02DC, 0x2122, 0x0161, 0x203A, 0x0153, 0x9D, 0x017E, 0x0178,
19];
20
21pub(crate) const CP1256: [u32; 128] = [
23 0x20AC, 0x067E, 0x201A, 0x0192, 0x201E, 0x2026, 0x2020, 0x2021, 0x02C6, 0x2030, 0x0679, 0x2039, 0x0152, 0x0686, 0x0698, 0x0688, 0x06AF, 0x2018,
24 0x2019, 0x201C, 0x201D, 0x2022, 0x2013, 0x2014, 0x06A9, 0x2122, 0x0691, 0x203A, 0x0153, 0x200C, 0x200D, 0x06BA, 0x00A0, 0x060C, 0x00A2, 0x00A3,
25 0x00A4, 0x00A5, 0x00A6, 0x00A7, 0x00A8, 0x00A9, 0x06BE, 0x00AB, 0x00AC, 0x00AD, 0x00AE, 0x00AF, 0x00B0, 0x00B1, 0x00B2, 0x00B3, 0x00B4, 0x00B5,
26 0x00B6, 0x00B7, 0x00B8, 0x00B9, 0x061B, 0x00BB, 0x00BC, 0x00BD, 0x00BE, 0x061F, 0x06C1, 0x0621, 0x0622, 0x0623, 0x0624, 0x0625, 0x0626, 0x0627,
27 0x0628, 0x0629, 0x062A, 0x062B, 0x062C, 0x062D, 0x062E, 0x062F, 0x0630, 0x0631, 0x0632, 0x0633, 0x0634, 0x0635, 0x0636, 0x00D7, 0x0637, 0x0638,
28 0x0639, 0x063A, 0x0640, 0x0641, 0x0642, 0x0643, 0x00E0, 0x0644, 0x00E2, 0x0645, 0x0646, 0x0647, 0x0648, 0x00E7, 0x00E8, 0x00E9, 0x00EA, 0x00EB,
29 0x0649, 0x064A, 0x00EE, 0x00EF, 0x064B, 0x064C, 0x064D, 0x064E, 0x00F4, 0x064F, 0x0650, 0x00F7, 0x0651, 0x00F9, 0x0652, 0x00FB, 0x00FC, 0x200E,
30 0x200F, 0x06D2,
31];
32
33#[derive(Default)]
35struct Cp1256Evidence {
36 differing: usize,
39 arabic: usize,
41 arabic_touching_latin: usize,
43 shared_letters_in_runs: usize,
46}
47
48impl Cp1256Evidence {
49 fn of(bytes: &[u8]) -> Self {
50 let mut e = Self::default();
51 for (i, &b) in bytes.iter().enumerate() {
52 if b < 0x80 {
53 continue;
54 }
55 let before = i.checked_sub(1).and_then(|j| bytes.get(j)).copied();
56 let after = bytes.get(i + 1).copied();
57 let next_to = |pred: fn(u8) -> bool| before.is_some_and(pred) || after.is_some_and(pred);
58 if same_in_cp1252_and_cp1256(b) {
59 if is_shared_latin_letter(b) && next_to(|x| x >= 0x80) {
60 e.shared_letters_in_runs += 1;
61 }
62 continue;
63 }
64 if is_invisible_mark(b) || is_ordinal_after_digit(b, before) {
65 continue;
66 }
67 e.differing += 1;
68 if is_cp1256_arabic(b) {
69 e.arabic += 1;
70 if next_to(|x| x.is_ascii_alphabetic()) {
71 e.arabic_touching_latin += 1;
72 }
73 }
74 }
75 e
76 }
77
78 fn is_arabic(&self) -> bool {
81 self.arabic >= 2
82 && self.arabic * 10 >= self.differing * 9
83 && self.arabic_touching_latin * 10 <= self.arabic * 3
84 && self.shared_letters_in_runs * 20 <= self.arabic
85 }
86}
87
88fn same_in_cp1252_and_cp1256(b: u8) -> bool {
89 matches!(
90 b,
91 0x80 | 0x82..=0x89 | 0x8B..=0x8C | 0x91..=0x97 | 0x99 | 0x9B..=0x9C | 0xA0 | 0xA2..=0xA9 | 0xAB..=0xB9 | 0xBB..=0xBE | 0xD7 | 0xE0 | 0xE2 | 0xE7..=0xEB
92 | 0xEE..=0xEF | 0xF4 | 0xF7 | 0xF9 | 0xFB..=0xFC
93 )
94}
95
96fn is_invisible_mark(b: u8) -> bool {
99 matches!(b, 0x9D | 0x9E | 0xFD | 0xFE)
100}
101
102fn is_ordinal_after_digit(b: u8, before: Option<u8>) -> bool {
105 matches!(b, 0xAA | 0xBA) && before.is_some_and(|x| x.is_ascii_digit())
106}
107
108fn is_shared_latin_letter(b: u8) -> bool {
109 matches!(b, 0xE0 | 0xE2 | 0xE7..=0xEB | 0xEE..=0xEF | 0xF4 | 0xF9 | 0xFB..=0xFC)
110}
111
112fn is_cp1256_arabic(b: u8) -> bool {
113 matches!(
114 b,
115 0x81 | 0x8A | 0x8D..=0x90 | 0x98 | 0x9A | 0x9F | 0xA1 | 0xAA | 0xBA | 0xBF..=0xD6 | 0xD8..=0xDB | 0xDD..=0xDF | 0xE1 | 0xE3..=0xE6 | 0xEC..=0xED | 0xFF
116 )
117}
118
119pub fn decode_text(bytes: &[u8]) -> String {
123 if let Some(rest) = bytes.strip_prefix(&[0xEF, 0xBB, 0xBF]) {
124 return String::from_utf8_lossy(rest).into_owned();
125 }
126 let utf16 = |b: &[u8], le: bool| -> String {
127 let units = b.as_chunks::<2>().0.iter().map(|c| if le { u16::from_le_bytes(*c) } else { u16::from_be_bytes(*c) });
128 char::decode_utf16(units).map(|r| r.unwrap_or('\u{FFFD}')).collect()
129 };
130 if let Some(rest) = bytes.strip_prefix(&[0xFF, 0xFE]) {
131 return utf16(rest, true);
132 }
133 if let Some(rest) = bytes.strip_prefix(&[0xFE, 0xFF]) {
134 return utf16(rest, false);
135 }
136 match std::str::from_utf8(bytes) {
137 Ok(s) => s.to_string(),
138 Err(_) => {
139 let use_windows_1256 = Cp1256Evidence::of(bytes).is_arabic();
140 bytes
141 .iter()
142 .map(|&b| match b {
143 0x80..=0xFF if use_windows_1256 => {
144 char::from_u32(CP1256.get((b - 0x80) as usize).copied().unwrap_or(0xFFFD)).unwrap_or('\u{FFFD}')
145 }
146 0x80..=0x9F => char::from_u32(CP1252.get((b - 0x80) as usize).copied().unwrap_or(0xFFFD)).unwrap_or('\u{FFFD}'),
147 _ => b as char,
148 })
149 .collect()
150 }
151 }
152}
153
154pub fn sniff_delimiter(text: &str, quote: char) -> u8 {
157 let mut best = (b',', 0usize, 0usize);
158 for d in [',', ';', '\t'] {
159 let mut counts = vec![];
160 let mut n = 0usize;
161 let mut in_q = false;
162 let mut blank = true;
163 for ch in text.chars().take(64 * 1024) {
164 if !in_q && ch == '\n' {
165 if !blank {
167 counts.push(n);
168 }
169 n = 0;
170 blank = true;
171 if counts.len() >= 20 {
172 break;
173 }
174 continue;
175 }
176 if ch != '\r' {
177 blank = false;
178 }
179 if ch == quote {
180 in_q = !in_q;
181 } else if !in_q && ch == d {
182 n += 1;
183 }
184 }
185 if !blank {
186 counts.push(n);
187 }
188 let min = counts.iter().copied().min().unwrap_or(0);
189 let total: usize = counts.iter().sum();
190 if min > 0 && (min, total) > (best.1, best.2) {
191 best = (d as u8, min, total);
192 }
193 }
194 best.0
195}
196
197pub(crate) fn parse_records(text: &str, delim: char, quote: char, mut on_record: impl FnMut(Vec<String>) -> bool) {
200 let mut field = String::new();
201 let mut record: Vec<String> = vec![];
202 let mut in_q = false;
203 let mut chars = text.chars().peekable();
204 let mut field_started_quoted = false;
205 while let Some(c) = chars.next() {
206 if in_q {
207 if c == quote {
208 if chars.peek() == Some("e) {
209 field.push(quote);
210 chars.next();
211 } else {
212 in_q = false;
213 }
214 } else {
215 field.push(c);
216 }
217 continue;
218 }
219 if c == quote && field.is_empty() && !field_started_quoted {
220 in_q = true;
221 field_started_quoted = true;
222 } else if c == delim {
223 let f = std::mem::take(&mut field);
224 if record.len() < MAX_COLS as usize {
225 record.push(f);
226 }
227 field_started_quoted = false;
228 } else if c == '\r' || c == '\n' {
229 if c == '\r' && chars.peek() == Some(&'\n') {
230 chars.next();
231 }
232 let f = std::mem::take(&mut field);
233 if record.len() < MAX_COLS as usize {
234 record.push(f);
235 }
236 field_started_quoted = false;
237 if !on_record(std::mem::take(&mut record)) {
238 return;
239 }
240 } else {
241 field.push(c);
242 }
243 }
244 if !field.is_empty() || !record.is_empty() || field_started_quoted {
245 if record.len() < MAX_COLS as usize {
246 record.push(field);
247 }
248 on_record(record);
249 }
250}
251
252const DATE_SAMPLE_ROWS: usize = 200;
254const DATE_SAMPLE_BYTES: usize = 1 << 20;
255
256#[derive(Clone, Copy, Debug, PartialEq, Eq)]
258enum DateRule {
259 NoDates,
261 MonthFirst,
263 DayFirst,
265 Ambiguous,
267 Conflict,
269}
270
271impl DateRule {
272 fn of(day_first: usize, month_first: usize, ambiguous: usize) -> Self {
273 match (day_first > 0, month_first > 0, ambiguous > 0) {
274 (true, true, _) => Self::Conflict,
275 (true, false, _) => Self::DayFirst,
276 (false, true, _) => Self::MonthFirst,
277 (false, false, true) => Self::Ambiguous,
278 (false, false, false) => Self::NoDates,
279 }
280 }
281}
282
283#[derive(Clone, Default)]
285struct DateEvidence {
286 day_first: usize,
287 month_first: usize,
288 ambiguous: usize,
289 day_first_example: Option<String>,
290 month_first_example: Option<String>,
291 ambiguous_example: Option<String>,
292}
293
294struct NumericDate {
296 day_first: Option<f64>,
297 month_first: Option<f64>,
298 time: Option<f64>,
299}
300
301impl NumericDate {
302 fn read(&self, field: &str, rule: DateRule) -> Parsed {
304 let serial = match rule {
305 DateRule::DayFirst => self.day_first,
306 DateRule::MonthFirst | DateRule::Ambiguous => self.month_first,
307 DateRule::Conflict | DateRule::NoDates => None,
308 };
309 match serial {
310 Some(serial) => Parsed {
311 value: Value::number(serial + self.time.unwrap_or(0.0)),
312 format: Some(if self.time.is_some() { "m/d/yyyy h:mm" } else { "m/d/yyyy" }),
313 },
314 None => Parsed { value: Value::text(field), format: None },
315 }
316 }
317}
318
319fn numeric_date(field: &str, sys: DateSystem) -> Option<NumericDate> {
323 let text = field.trim();
324 let (date, time) = split_time(text, sys);
325 let date = date.trim_end_matches(',');
327 let mut parts = date.split(['/', '-']);
328 let (a, b, c) = (parts.next()?, parts.next()?, parts.next()?);
329 if parts.next().is_some() {
330 return None;
331 }
332 let (a, b, c) = (a.trim(), b.trim(), c.trim());
333 if a.len() == 4 || c.len() == 3 {
336 return None;
337 }
338 let a = parse_digits(a)?;
339 let b = parse_digits(b)?;
340 let year = expand_year(c, parse_digits(c)?);
341 let day_first = valid_date(sys, year, b, a);
342 let month_first = valid_date(sys, year, a, b);
343 if day_first.is_none() && month_first.is_none() {
344 return None;
345 }
346 Some(NumericDate { day_first, month_first, time })
347}
348
349fn split_time(text: &str, sys: DateSystem) -> (&str, Option<f64>) {
352 for at in [text.find(' '), text.rfind(' ')].into_iter().flatten() {
353 let (date, time) = text.split_at(at);
354 let time = time.trim();
355 if time.len() > 40 {
357 continue;
358 }
359 if let Some(fraction) = time_of_day(time, sys) {
360 return (date.trim(), Some(fraction));
361 }
362 }
363 (text, None)
364}
365
366fn time_of_day(text: &str, sys: DateSystem) -> Option<f64> {
367 match parse_input(text, sys) {
368 Parsed { value: Value::Number(fraction), format: Some(code) } if code.contains('h') => Some(fraction),
369 _ => None,
370 }
371}
372
373fn parse_digits(s: &str) -> Option<i64> {
374 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
375 return None;
376 }
377 s.parse().ok()
378}
379
380fn expand_year(text: &str, year: i64) -> i64 {
382 if text.len() <= 2 { if year < 30 { 2000 + year } else { 1900 + year } } else { year }
383}
384
385fn valid_date(sys: DateSystem, year: i64, month: i64, day: i64) -> Option<f64> {
387 if !(1..=12).contains(&month) || day < 1 || year < 1900 {
388 return None;
389 }
390 let year = i32::try_from(year).ok()?;
391 let month = u32::try_from(month).ok()?;
392 if day > i64::from(days_in_month_in(sys, year, month)) {
393 return None;
394 }
395 serial_from_ymd(sys, i64::from(year), i64::from(month), day)
396}
397
398fn sample_records(text: &str, delim: char, quote: char) -> Vec<Vec<String>> {
400 let mut cut = text.len().min(DATE_SAMPLE_BYTES);
401 while !text.is_char_boundary(cut) {
402 cut -= 1;
403 }
404 let truncated = cut < text.len();
405 let sample = text.get(..cut).unwrap_or_default();
406 let mut rows = Vec::new();
407 parse_records(sample, delim, quote, |record| {
408 rows.push(record);
409 rows.len() < DATE_SAMPLE_ROWS
410 });
411 if truncated {
413 rows.pop();
414 }
415 rows
416}
417
418fn infer_date_rules(rows: &[Vec<String>], sys: DateSystem) -> Vec<(DateRule, DateEvidence)> {
420 let width = rows.iter().map(Vec::len).max().unwrap_or(0).min(MAX_COLS as usize);
421 let mut columns = vec![DateEvidence::default(); width];
422 for row in rows {
423 for (col, field) in row.iter().enumerate() {
424 let Some(column) = columns.get_mut(col) else { continue };
425 let Some(date) = numeric_date(field, sys) else { continue };
426 let example = || field.trim().chars().take(40).collect::<String>();
427 match (date.day_first.is_some(), date.month_first.is_some()) {
428 (true, true) => {
429 column.ambiguous += 1;
430 column.ambiguous_example.get_or_insert_with(example);
431 }
432 (true, false) => {
433 column.day_first += 1;
434 column.day_first_example.get_or_insert_with(example);
435 }
436 (false, true) => {
437 column.month_first += 1;
438 column.month_first_example.get_or_insert_with(example);
439 }
440 (false, false) => {}
441 }
442 }
443 }
444 columns.into_iter().map(|c| (DateRule::of(c.day_first, c.month_first, c.ambiguous), c)).collect()
445}
446
447fn date_warnings(columns: &[(DateRule, DateEvidence)], report: &mut ReadReport) {
450 for (col, (rule, evidence)) in columns.iter().enumerate() {
451 match rule {
452 DateRule::Ambiguous => report.warn(format!(
453 "column {}: every date is ambiguous, so it was read month first ({})",
454 col_to_letters(col as u32),
455 evidence.ambiguous_example.as_deref().unwrap_or("a date")
456 )),
457 DateRule::Conflict => report.warn(format!(
458 "column {}: it holds both day-first and month-first dates ({} and {}), so its dates stay text",
459 col_to_letters(col as u32),
460 evidence.day_first_example.as_deref().unwrap_or("a day-first date"),
461 evidence.month_first_example.as_deref().unwrap_or("a month-first date")
462 )),
463 DateRule::NoDates | DateRule::MonthFirst | DateRule::DayFirst => {}
464 }
465 }
466}
467
468fn read_field(field: &str, sys: DateSystem, rule: DateRule) -> Parsed {
470 match numeric_date(field, sys) {
471 Some(date) if rule != DateRule::NoDates => date.read(field, rule),
472 _ => parse_input(field, sys),
473 }
474}
475
476pub fn read_csv(bytes: &[u8], opts: &CsvOptions) -> Result<Workbook, IoError> {
477 read_csv_with_report(bytes, opts).map(|(wb, _)| wb)
478}
479
480pub fn read_csv_with_report(bytes: &[u8], opts: &CsvOptions) -> Result<(Workbook, ReadReport), IoError> {
483 let text = decode_text(bytes);
484 let quote = if opts.quote == 0 { '"' } else { opts.quote as char };
485 let delim = if opts.delimiter == 0 { sniff_delimiter(&text, quote) } else { opts.delimiter } as char;
486 if delim == quote {
487 return Err(IoError::Format("the delimiter and quote character must differ".into()));
488 }
489 let date_columns = if opts.parse_values { infer_date_rules(&sample_records(&text, delim, quote), opts.date_system) } else { vec![] };
490 let mut report = ReadReport::default();
491 date_warnings(&date_columns, &mut report);
492 let mut wb = Workbook::new();
493 wb.date_system = opts.date_system;
494 wb.csv_delimiter = u8::try_from(delim).ok();
495 let mut sheet = Sheet::new("Sheet1");
496 let mut styles = std::mem::take(&mut wb.styles);
497 let mut fmt_cache: Vec<(&'static str, StyleId)> = vec![];
498 let mut row: u32 = 0;
499 parse_records(&text, delim, quote, |rec| {
500 if row >= MAX_ROWS {
501 return false;
502 }
503 for (col, f) in rec.into_iter().enumerate() {
505 if col as u32 >= MAX_COLS {
506 break;
507 }
508 if f.is_empty() {
509 continue;
510 }
511 let pos = CellRef::new(row, col as u32);
512 let cell = if opts.parse_values {
513 let rule = date_columns.get(col).map_or(DateRule::NoDates, |(rule, _)| *rule);
514 let p = read_field(&f, opts.date_system, rule);
515 let style = match p.format {
516 Some(code) => match fmt_cache.iter().find(|(c, _)| *c == code) {
517 Some((_, id)) => *id,
518 None => {
519 let id = styles.derive(StyleId::DEFAULT, |s| s.num_fmt = NumFmt::new(code));
520 fmt_cache.push((code, id));
521 id
522 }
523 },
524 None => StyleId::DEFAULT,
525 };
526 Cell { value: p.value, formula: None, style, py: None }
527 } else {
528 Cell::value(Value::Text(Arc::from(f)))
529 };
530 sheet.cells.set(pos, cell);
531 }
532 row += 1;
533 true
534 });
535 wb.styles = styles;
536 wb.sheets = vec![Arc::new(sheet)];
537 Ok((wb, report))
538}
539
540pub fn write_csv(sheet: &Sheet, delimiter: u8, cell_text: impl FnMut(CellRef) -> String) -> Result<Vec<u8>, IoError> {
551 write_csv_limited(sheet, delimiter, MAX_CSV_BYTES, cell_text)
552}
553
554pub fn write_csv_limited(sheet: &Sheet, delimiter: u8, limit: usize, mut cell_text: impl FnMut(CellRef) -> String) -> Result<Vec<u8>, IoError> {
556 let delim = if delimiter == 0 { ',' } else { delimiter as char };
557 let mut out = String::new();
558 let Some(r) = sheet.used_range() else { return Ok(Vec::new()) };
559 let too_large =
560 |what: String| IoError::TooLarge(format!("the CSV would be {what}, over the limit of {} MiB; export a smaller range", limit >> 20));
561 let (rows, cols) = (u64::from(r.end.row) + 1, u64::from(r.end.col) + 1);
564 let per_row = (cols - 1).saturating_mul(delim.len_utf8() as u64).saturating_add(2);
565 let least = rows.saturating_mul(per_row);
566 if least > limit as u64 {
567 return Err(too_large(format!("at least {} MiB ({rows} rows by {cols} columns)", least >> 20)));
568 }
569 for row in 0..=r.end.row {
570 for col in 0..=r.end.col {
571 if col > 0 {
572 out.push(delim);
573 }
574 let t = cell_text(CellRef::new(row, col));
575 let quoted = t.contains(delim) || t.contains('"') || t.contains('\n') || t.contains('\r');
576 let field_len = if quoted { t.len() + t.matches('"').count() + 2 } else { t.len() };
579 if out.len().saturating_add(field_len) > limit {
580 return Err(too_large(format!("more than {} MiB (stopped at row {})", limit >> 20, row + 1)));
581 }
582 if quoted {
583 out.push('"');
584 for ch in t.chars() {
585 if ch == '"' {
586 out.push('"');
587 }
588 out.push(ch);
589 }
590 out.push('"');
591 } else {
592 out.push_str(&t);
593 }
594 }
595 out.push_str("\r\n");
596 if out.len() > limit {
597 return Err(too_large(format!("more than {} MiB (stopped at row {})", limit >> 20, row + 1)));
598 }
599 }
600 Ok(out.into_bytes())
601}