1use crate::formats::{Chosen, Spec};
12use polars::prelude::*;
13use std::io::BufRead;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct HeaderRows {
19 pub name: Vec<usize>,
21 pub unit: Option<usize>,
23}
24
25impl HeaderRows {
26 pub fn last(&self) -> usize {
28 self.name
29 .iter()
30 .copied()
31 .chain(self.unit)
32 .max()
33 .unwrap_or(0)
34 }
35}
36
37pub use crate::formats::column_types::{Derived, DerivedKind};
38
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
42pub struct Delimited {
43 pub delimiter: Option<u8>,
44 pub comment_char: Option<String>,
45 pub skip_initial_space: Option<bool>,
46 pub header_rows: Option<HeaderRows>,
47 pub header_join: Option<String>,
48 pub metadata_line: Option<usize>,
49 pub null_values: Vec<String>,
50 pub skip_lines: Option<usize>,
51 pub columns: Vec<Derived>,
52 pub types: Vec<(String, crate::formats::column_types::ColumnType)>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
58pub struct Metadata {
59 pub raw: String,
61 pub title: Option<String>,
63 pub pairs: Vec<(String, String)>,
66}
67
68#[derive(Debug, Clone, Default, PartialEq, Eq)]
70pub struct HeadFacts {
71 pub units: Vec<(String, String)>,
73 pub metadata: Option<Metadata>,
74}
75
76#[derive(Debug, Clone)]
78pub struct DelimitedRead {
79 pub spec: Arc<Spec>,
80 pub by: Chosen,
81 pub also: Vec<String>,
83 pub units: Vec<(String, String)>,
85 pub metadata: Option<Metadata>,
86 pub facts_from: Option<String>,
88}
89
90impl DelimitedRead {
91 pub fn chosen(spec: Arc<Spec>, by: Chosen, also: Vec<String>) -> Self {
93 Self {
94 spec,
95 by,
96 also,
97 units: Vec::new(),
98 metadata: None,
99 facts_from: None,
100 }
101 }
102
103 pub fn delimited(&self) -> &Delimited {
104 self.spec
105 .delimited
106 .as_deref()
107 .expect("a delimited read has a delimited spec")
108 }
109
110 pub fn unit_of(&self, column: &str) -> Option<&str> {
112 self.units
113 .iter()
114 .find(|(name, _)| name == column)
115 .map(|(_, unit)| unit.as_str())
116 }
117
118 pub fn notes(&self) -> Vec<crate::notes::Note> {
121 let note = |summary: String, scope: String| crate::notes::Note {
122 summary,
123 scope,
124 read_as_text: None,
125 passed_over: None,
126 };
127 let from = self
128 .spec
129 .path
130 .as_ref()
131 .map_or_else(|| "the spec".to_string(), |p| p.display().to_string());
132 let mut notes = vec![note(
133 format!(
134 "read as {}, {}",
135 self.spec.name,
136 crate::formats::chosen_words(&self.spec, self.by)
137 ),
138 format!("from {from}"),
139 )];
140 if !self.also.is_empty() {
141 notes.push(note(
142 format!(
143 "{} also {} this file",
144 self.also.join(", "),
145 if self.also.len() == 1 {
146 "matches"
147 } else {
148 "match"
149 }
150 ),
151 format!("by {}", self.by.words()),
152 ));
153 }
154 notes
155 }
156}
157
158pub const MAX_HEAD_LINE: usize = 1000;
161
162impl Delimited {
163 pub fn apply(&self, options: &mut crate::OpenOptions) {
167 let typed = options.typed_dialect;
168 if let Some(d) = self.delimiter.filter(|_| !typed.delimiter) {
169 options.delimiter = Some(d);
170 }
171 if let Some(c) = self.comment_char.as_ref().filter(|_| !typed.comment_char) {
172 options.comment_char = Some(c.clone());
173 }
174 if let Some(s) = self
175 .skip_initial_space
176 .filter(|_| !typed.skip_initial_space)
177 {
178 options.skip_initial_space = s;
179 }
180 if let Some(rows) = self.header_rows.as_ref().filter(|_| !typed.header_rows) {
181 options.header_rows = rows.name.clone();
182 options.skip_lines = Some(options.skip_lines.unwrap_or(0).max(rows.last()));
184 }
185 if let Some(join) = &self.header_join {
186 options.header_join = join.clone();
187 }
188 if let Some(n) = self.skip_lines.filter(|_| !typed.skip_lines) {
189 options.skip_lines = Some(options.skip_lines.unwrap_or(0).max(n));
190 }
191 if !self.null_values.is_empty() {
192 let values = options.null_values.get_or_insert_with(Vec::new);
194 for value in &self.null_values {
195 if !values.contains(value) {
196 values.push(value.clone());
197 }
198 }
199 }
200 if options
201 .format
202 .is_none_or(|f| crate::FileFormat::separator(f).is_none())
203 {
204 options.format = Some(crate::FileFormat::Csv);
205 }
206 }
207
208 pub fn head_lines(&self) -> Vec<usize> {
210 let mut lines: Vec<usize> = self
211 .header_rows
212 .iter()
213 .flat_map(|rows| rows.name.iter().copied().chain(rows.unit))
214 .chain(self.metadata_line)
215 .collect();
216 lines.sort_unstable();
217 lines.dedup();
218 lines
219 }
220
221 pub fn facts(
224 &self,
225 source: impl BufRead,
226 separator: u8,
227 join: &str,
228 ) -> color_eyre::Result<HeadFacts> {
229 let wanted = self.head_lines();
230 if wanted.is_empty() {
231 return Ok(HeadFacts::default());
232 }
233 let lines = crate::formats::csv_dialect::named_lines(source, &wanted)?;
234 Ok(self.facts_of(&wanted, &lines, separator, join))
235 }
236
237 pub fn facts_of(
240 &self,
241 wanted: &[usize],
242 lines: &[Vec<u8>],
243 separator: u8,
244 join: &str,
245 ) -> HeadFacts {
246 let line = |n: usize| -> &[u8] {
247 wanted
248 .iter()
249 .position(|&w| w == n)
250 .map_or(&[][..], |i| lines[i].as_slice())
251 };
252 let comment = self.comment_char.as_deref();
253 let mut units = Vec::new();
254 if let Some(rows) = &self.header_rows
255 && let Some(unit) = rows.unit
256 {
257 let names = crate::formats::csv_dialect::names_of(
258 lines, wanted, &rows.name, join, separator, comment,
259 );
260 let raw: Vec<PlSmallStr> = (1..=names.len())
261 .map(|i| format!("column_{i}").into())
262 .collect();
263 let shown = crate::formats::csv_dialect::shown_names(&raw, Some(&names));
264 let unit_fields =
265 crate::formats::csv_dialect::header_fields(line(unit), unit, separator, comment);
266 for (name, unit) in shown.into_iter().zip(unit_fields) {
267 if !unit.is_empty() && !self.columns.iter().any(|d| d.name == name) {
270 units.push((name, unit));
271 }
272 }
273 }
274 let metadata = self.metadata_line.map(|n| {
275 let mut text = line(n);
276 if n == 1 {
277 text = text.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(text);
278 }
279 let text = String::from_utf8_lossy(text);
280 let text = text.trim_end_matches(['\n', '\r']);
281 let text = comment.and_then(|c| text.strip_prefix(c)).unwrap_or(text);
282 parse_metadata(text)
283 });
284 HeadFacts { units, metadata }
285 }
286
287 pub fn derive(&self, mut lf: LazyFrame) -> PolarsResult<LazyFrame> {
290 if self.columns.is_empty() {
291 return Ok(lf);
292 }
293 let schema = lf.collect_schema()?;
294 let mut order: Vec<PlSmallStr> = schema.iter_names().cloned().collect();
295 let mut exprs = Vec::with_capacity(self.columns.len());
296 for derived in &self.columns {
297 for from in &derived.from {
298 if !schema.contains(from) {
299 polars_bail!(
300 ColumnNotFound: "\"{}\" is made from \"{from}\", which the file has no column of",
301 derived.name
302 );
303 }
304 }
305 let name = PlSmallStr::from(derived.name.as_str());
306 if !order.contains(&name) {
307 let at = order
308 .iter()
309 .position(|c| c.as_str() == derived.from[0])
310 .unwrap_or(order.len());
311 order.insert(at, name.clone());
312 }
313 exprs.push(derived.expr().alias(name));
314 }
315 Ok(lf
316 .with_columns(exprs)
317 .select(order.into_iter().map(col).collect::<Vec<_>>()))
318 }
319}
320
321pub fn read_facts(
324 read: &DelimitedRead,
325 paths: &[std::path::PathBuf],
326 options: &crate::OpenOptions,
327) -> color_eyre::Result<DelimitedRead> {
328 let separator = options.separator_or(
329 options
330 .format
331 .and_then(crate::FileFormat::separator)
332 .unwrap_or(b','),
333 );
334 let facts_of = |file: &std::path::Path| -> color_eyre::Result<HeadFacts> {
335 let compression = options
336 .compression
337 .or_else(|| crate::CompressionFormat::from_extension(file));
338 let source = crate::formats::readers::csv::text_source(file, compression)
339 .map_err(|e| crate::error_display::in_file(file, e.into()))?;
340 read.delimited()
341 .facts(source, separator, &options.header_join)
342 .map_err(|e| crate::error_display::in_file(file, e))
343 };
344 let mut found = None;
347 for file in paths {
348 match facts_of(file) {
349 Err(e) if paths.len() > 1 && crate::formats::csv_dialect::is_blank_file(&e) => continue,
350 facts => {
351 found = Some((file, facts?));
352 break;
353 }
354 }
355 }
356 let Some((file, HeadFacts { units, metadata })) = found else {
357 return Ok(read.clone());
358 };
359 Ok(DelimitedRead {
360 units,
361 metadata,
362 facts_from: (paths.len() > 1).then(|| {
363 file.file_name().map_or_else(
364 || file.display().to_string(),
365 |n| n.to_string_lossy().into_owned(),
366 )
367 }),
368 ..read.clone()
369 })
370}
371
372pub fn check(
376 spec: &Arc<Spec>,
377 file: Option<&std::path::Path>,
378 rows: usize,
379 base: &crate::OpenOptions,
380) -> Result<String, String> {
381 let delimited = spec
382 .delimited
383 .as_deref()
384 .ok_or_else(|| format!("error: {} is not a delimited spec\n", spec.name))?;
385 let mut out = String::new();
386 out.push_str(&format!(" delimited: {}\n", delimited.summary()));
387 for derived in &delimited.columns {
388 out.push_str(&format!(
389 " {} = {} from {}\n",
390 derived.name,
391 derived.kind.name(),
392 derived.from.join(", ")
393 ));
394 }
395 for (name, ty) in &delimited.types {
396 let format = ty
397 .format
398 .as_ref()
399 .map_or_else(String::new, |f| format!(", format {f:?}"));
400 out.push_str(&format!(" {name}: {}{format}\n", ty.name()));
401 }
402 let typed = typed_summary(base);
403 if !typed.is_empty() {
404 out.push_str(&format!(" command line, over the spec: {typed}\n"));
405 }
406 let Some(file) = file else {
407 return Ok(out);
408 };
409 let fail = |out: &str, e: &color_eyre::Report| {
410 let said = crate::error_display::user_message_from_report(e, Some(file));
411 format!("{out}error: {said}\n")
412 };
413 let mut options = crate::OpenOptions {
414 format: crate::FileFormat::from_path(file),
415 ..base.clone()
416 };
417 delimited.apply(&mut options);
418 let chosen = DelimitedRead::chosen(spec.clone(), Chosen::SpecFile, Vec::new());
419 options.delimited = Some(Arc::new(chosen.clone()));
420 let read = read_facts(&chosen, &[file.to_path_buf()], &options).map_err(|e| fail(&out, &e))?;
421 if let Some(metadata) = &read.metadata {
422 if metadata.pairs.is_empty() {
423 out.push_str(&format!(
424 "metadata (not key=value pairs): {}\n",
425 metadata.raw
426 ));
427 } else {
428 let pairs: Vec<String> = metadata
429 .pairs
430 .iter()
431 .map(|(k, v)| format!("{k} = {v}"))
432 .collect();
433 let title = metadata
434 .title
435 .as_ref()
436 .map_or_else(String::new, |t| format!("{t}: "));
437 out.push_str(&format!("metadata: {title}{}\n", pairs.join(", ")));
438 }
439 }
440 if !read.units.is_empty() {
441 let units: Vec<String> = read
442 .units
443 .iter()
444 .map(|(column, unit)| format!("{column} = {unit}"))
445 .collect();
446 out.push_str(&format!("units: {}\n", units.join(", ")));
447 }
448 let separator = options.separator_or(b',');
449 let read = crate::formats::readers::csv::read_delimited(
450 file,
451 separator,
452 &options,
453 &Default::default(),
454 )
455 .map_err(|e| fail(&out, &crate::error_display::in_file(file, e)))?;
456 let df = crate::formats::readers::polars::resolved(read.lf)
457 .map_err(|e| fail(&out, &crate::error_display::in_file(file, e)))?
458 .limit(rows as IdxSize)
459 .collect()
460 .map_err(|e| fail(&out, &crate::error_display::in_file(file, e.into())))?;
461 out.push_str(&crate::formats::text_table(&df));
462 Ok(out)
463}
464
465fn typed_summary(options: &crate::OpenOptions) -> String {
467 let typed = options.typed_dialect;
468 let mut said = Vec::new();
469 if let Some(d) = options.delimiter.filter(|_| typed.delimiter) {
470 said.push(format!("delimiter {:?}", d as char));
471 }
472 if let Some(c) = options.comment_char.as_ref().filter(|_| typed.comment_char) {
473 said.push(format!("comment {c:?}"));
474 }
475 if typed.skip_initial_space {
476 said.push(format!("skip initial space {}", options.skip_initial_space));
477 }
478 if typed.header_rows {
479 let rows: Vec<String> = options.header_rows.iter().map(usize::to_string).collect();
480 said.push(format!("header rows {}", rows.join(",")));
481 }
482 if let Some(n) = options.skip_lines.filter(|_| typed.skip_lines) {
483 said.push(format!("skip lines {n}"));
484 }
485 said.join(", ")
486}
487
488impl Delimited {
489 pub fn summary(&self) -> String {
491 let lines = |rows: &[usize]| {
492 let rows: Vec<String> = rows.iter().map(usize::to_string).collect();
493 rows.join(" + ")
494 };
495 let mut said = Vec::new();
496 if let Some(d) = self.delimiter {
497 said.push(format!("delimiter {:?}", d as char));
498 }
499 if let Some(rows) = &self.header_rows {
500 said.push(format!("names on line {}", lines(&rows.name)));
501 if let Some(unit) = rows.unit {
502 said.push(format!("units on line {unit}"));
503 }
504 }
505 if let Some(n) = self.metadata_line {
506 said.push(format!("metadata on line {n}"));
507 }
508 if let Some(c) = &self.comment_char {
509 said.push(format!("comments start {c:?}"));
510 }
511 if self.skip_initial_space == Some(true) {
512 said.push("skip initial space".to_string());
513 }
514 if let Some(n) = self.skip_lines {
515 said.push(format!("skip {n} lines"));
516 }
517 if !self.null_values.is_empty() {
518 said.push(format!("null {}", self.null_values.join(", ")));
519 }
520 if said.is_empty() {
521 "CSV with a header line".to_string()
522 } else {
523 said.join(", ")
524 }
525 }
526}
527
528pub fn parse_metadata(line: &str) -> Metadata {
532 let raw = line.trim().to_string();
533 let mut items = Vec::new();
534 let mut item = String::new();
535 let mut quoted = false;
536 for c in line.chars() {
537 match c {
538 '"' => {
539 quoted = !quoted;
540 item.push(c);
541 }
542 ',' if !quoted => items.push(std::mem::take(&mut item)),
543 c => item.push(c),
544 }
545 }
546 items.push(item);
547 let unparsed = |raw: String| Metadata {
548 raw,
549 title: None,
550 pairs: Vec::new(),
551 };
552 if quoted {
553 return unparsed(raw);
554 }
555 let mut title = None;
556 let mut pairs = Vec::new();
557 for (i, item) in items.iter().map(|s| s.trim()).enumerate() {
558 if item.is_empty() {
559 continue;
560 }
561 match item.split_once('=') {
562 Some((key, value)) => {
563 let key = key.trim();
564 if key.is_empty() || key.contains('"') {
565 return unparsed(raw);
566 }
567 pairs.push((key.to_string(), unquote(value.trim())));
568 }
569 None if i == 0 && !item.contains('"') => title = Some(item.to_string()),
570 None => return unparsed(raw),
571 }
572 }
573 if pairs.is_empty() {
574 return unparsed(raw);
575 }
576 Metadata { raw, title, pairs }
577}
578
579fn unquote(value: &str) -> String {
581 match value.strip_prefix('"').and_then(|v| v.strip_suffix('"')) {
582 Some(inner) => inner.replace("\"\"", "\""),
583 None => value.to_string(),
584 }
585}
586
587#[cfg(test)]
588mod tests;