1use polars_buffer::Buffer;
2use polars_core::prelude::*;
3#[cfg(feature = "polars-time")]
4use polars_time::chunkedarray::string::infer as date_infer;
5#[cfg(feature = "polars-time")]
6use polars_time::prelude::string::Pattern;
7use polars_utils::format_pl_smallstr;
8
9use super::splitfields::SplitFields;
10use super::{CsvParseOptions, NullValues};
11use crate::utils::{BOOLEAN_RE, FLOAT_RE, FLOAT_RE_DECIMAL, INTEGER_RE};
12
13#[allow(clippy::too_many_arguments)]
17pub(super) fn infer_file_schema_impl(
18 header_line: &Option<Buffer<u8>>,
19 content_lines: &[Buffer<u8>],
20 infer_all_as_str: bool,
21 parse_options: &CsvParseOptions,
22 column_names_overwrite: Option<&[PlSmallStr]>,
23 schema_overwrite: Option<&Schema>,
24) -> PolarsResult<Schema> {
25 let mut headers = if let Some(header_line) = header_line {
26 infer_headers(header_line, parse_options)?
27 } else {
28 Vec::with_capacity(8)
29 };
30
31 let extend_header_with_unknown_column = header_line.is_none();
32
33 let mut column_types = vec![PlIndexSet::<DataType>::with_capacity(4); headers.len()];
34 let mut nulls = vec![false; headers.len()];
35
36 for content_line in content_lines {
37 infer_types_from_line(
38 content_line,
39 infer_all_as_str,
40 &mut headers,
41 extend_header_with_unknown_column,
42 parse_options,
43 &mut column_types,
44 &mut nulls,
45 );
46 }
47
48 if let Some(column_names_overwrite) = column_names_overwrite {
49 polars_ensure!(
51 column_names_overwrite.len() <= headers.len(),
52 ShapeMismatch:
53 "The length of the new names list should be equal to or less than the original column length",
54 );
55 for (i, name) in column_names_overwrite.iter().cloned().enumerate() {
56 if i < headers.len() {
57 headers[i] = name
58 } else {
59 headers.push(name)
60 }
61
62 if i >= column_types.len() {
63 column_types.push(PlIndexSet::from_iter(Some(DataType::Null)))
64 }
65 }
66 }
67
68 Ok(build_schema(&headers, &column_types, schema_overwrite))
69}
70
71fn infer_headers(
72 mut header_line: &[u8],
73 parse_options: &CsvParseOptions,
74) -> PolarsResult<Vec<PlSmallStr>> {
75 let len = header_line.len();
76
77 if header_line.last().copied() == Some(b'\r') {
78 header_line = &header_line[..len - 1];
79 }
80
81 let byterecord = SplitFields::new(
82 header_line,
83 parse_options.separator,
84 parse_options.quote_char,
85 parse_options.eol_char,
86 );
87
88 let headers = byterecord
89 .map(|(slice, needs_escaping)| {
90 let slice_escaped = if needs_escaping && (slice.len() >= 2) {
91 &slice[1..(slice.len() - 1)]
92 } else {
93 slice
94 };
95 String::from_utf8_lossy(slice_escaped)
96 })
97 .collect::<Vec<_>>();
98
99 let mut deduplicated_headers = PlIndexSet::with_capacity(headers.len());
100 let mut header_names = PlHashMap::with_capacity(headers.len());
101
102 for name in &headers {
103 let count = header_names.entry(name.as_ref()).or_insert(0usize);
104 let duplicated = *count != 0;
105 let deduplicated_name = if duplicated {
106 format_pl_smallstr!("{}_duplicated_{}", name, *count - 1)
107 } else {
108 PlSmallStr::from_str(name)
109 };
110
111 if !deduplicated_headers.insert(deduplicated_name.clone()) {
112 let (deduplicated_from, nth_duplicated) = if duplicated {
113 (name.as_ref(), 1 + *count)
114 } else {
115 let i = deduplicated_name.rfind("_duplicated_").unwrap();
116 (
117 &deduplicated_name[..i],
118 2 + deduplicated_name[i + 12..].parse::<usize>().unwrap(),
119 )
120 };
121
122 polars_bail!(
123 Duplicate:
124 "de-duplication of occurrence #{nth_duplicated} of column name '{deduplicated_from}' \
125 failed; the name '{deduplicated_name}' also exists in the file."
126 )
127 }
128
129 *count += 1;
130 }
131
132 Ok(Vec::from_iter(deduplicated_headers))
133}
134
135fn infer_types_from_line(
136 mut line: &[u8],
137 infer_all_as_str: bool,
138 headers: &mut Vec<PlSmallStr>,
139 extend_header_with_unknown_column: bool,
140 parse_options: &CsvParseOptions,
141 column_types: &mut Vec<PlIndexSet<DataType>>,
142 nulls: &mut Vec<bool>,
143) {
144 let line_len = line.len();
145 if line.last().copied() == Some(b'\r') {
146 line = &line[..line_len - 1];
147 }
148
149 let record = SplitFields::new(
150 line,
151 parse_options.separator,
152 parse_options.quote_char,
153 parse_options.eol_char,
154 );
155
156 for (i, (slice, needs_escaping)) in record.enumerate() {
157 if i >= headers.len() {
158 if extend_header_with_unknown_column {
159 headers.push(column_name(i));
160 column_types.push(Default::default());
161 nulls.push(false);
162 } else {
163 break;
164 }
165 }
166
167 if infer_all_as_str {
168 column_types[i].insert(DataType::String);
169 continue;
170 }
171
172 if slice.is_empty() {
173 nulls[i] = true;
174 } else {
175 let slice_escaped = if needs_escaping && (slice.len() >= 2) {
176 &slice[1..(slice.len() - 1)]
177 } else {
178 slice
179 };
180 let s = String::from_utf8_lossy(slice_escaped);
181 let dtype = match &parse_options.null_values {
182 None => Some(infer_field_schema(
183 &s,
184 parse_options.try_parse_dates,
185 parse_options.decimal_comma,
186 )),
187 Some(NullValues::AllColumns(names)) => {
188 if !names.iter().any(|nv| nv == s.as_ref()) {
189 Some(infer_field_schema(
190 &s,
191 parse_options.try_parse_dates,
192 parse_options.decimal_comma,
193 ))
194 } else {
195 None
196 }
197 },
198 Some(NullValues::AllColumnsSingle(name)) => {
199 if s.as_ref() != name.as_str() {
200 Some(infer_field_schema(
201 &s,
202 parse_options.try_parse_dates,
203 parse_options.decimal_comma,
204 ))
205 } else {
206 None
207 }
208 },
209 Some(NullValues::Named(names)) => {
210 let current_name = &headers[i];
211 let null_name = &names.iter().find(|name| name.0 == current_name);
212
213 if let Some(null_name) = null_name {
214 if null_name.1.as_str() != s.as_ref() {
215 Some(infer_field_schema(
216 &s,
217 parse_options.try_parse_dates,
218 parse_options.decimal_comma,
219 ))
220 } else {
221 None
222 }
223 } else {
224 Some(infer_field_schema(
225 &s,
226 parse_options.try_parse_dates,
227 parse_options.decimal_comma,
228 ))
229 }
230 },
231 };
232 if let Some(dtype) = dtype {
233 column_types[i].insert(dtype);
234 }
235 }
236 }
237}
238
239fn build_schema(
240 headers: &[PlSmallStr],
241 column_types: &[PlIndexSet<DataType>],
242 schema_overwrite: Option<&Schema>,
243) -> Schema {
244 assert!(headers.len() == column_types.len());
245
246 let get_schema_overwrite = |field_name| {
247 if let Some(schema_overwrite) = schema_overwrite {
248 if let Some((_, name, dtype)) = schema_overwrite.get_full(field_name) {
251 return Some((name.clone(), dtype.clone()));
252 }
253 }
254
255 None
256 };
257
258 Schema::from_iter(
259 headers
260 .iter()
261 .zip(column_types)
262 .map(|(field_name, type_possibilities)| {
263 let (name, dtype) = get_schema_overwrite(field_name).unwrap_or_else(|| {
264 (
265 field_name.clone(),
266 finish_infer_field_schema(type_possibilities),
267 )
268 });
269
270 Field::new(name, dtype)
271 }),
272 )
273}
274
275pub fn finish_infer_field_schema(possibilities: &PlIndexSet<DataType>) -> DataType {
276 match possibilities.len() {
279 1 => possibilities.iter().next().unwrap().clone(),
280 2 if possibilities.contains(&DataType::Int64)
281 && possibilities.contains(&DataType::Float64) =>
282 {
283 DataType::Float64
285 },
286 #[cfg(feature = "dtype-i128")]
287 2 if possibilities.contains(&DataType::Int64)
288 && possibilities.contains(&DataType::Int128) =>
289 {
290 DataType::Int128
292 },
293 #[cfg(feature = "dtype-i128")]
294 2 if possibilities.contains(&DataType::Int128)
295 && possibilities.contains(&DataType::Float64) =>
296 {
297 DataType::Float64
299 },
300 _ => DataType::String,
302 }
303}
304
305pub fn infer_field_schema(string: &str, try_parse_dates: bool, decimal_comma: bool) -> DataType {
307 let bytes = string.as_bytes();
310 if bytes.len() >= 2 && *bytes.first().unwrap() == b'"' && *bytes.last().unwrap() == b'"' {
311 if try_parse_dates {
312 #[cfg(feature = "polars-time")]
313 {
314 match date_infer::infer_pattern_single(&string[1..string.len() - 1]) {
315 Some(pattern_with_offset) => match pattern_with_offset {
316 Pattern::DatetimeYMD | Pattern::DatetimeDMY => {
317 DataType::Datetime(TimeUnit::Microseconds, None)
318 },
319 Pattern::DateYMD | Pattern::DateDMY => DataType::Date,
320 Pattern::DatetimeYMDZ => {
321 DataType::Datetime(TimeUnit::Microseconds, Some(TimeZone::UTC))
322 },
323 Pattern::Time => DataType::Time,
324 },
325 None => DataType::String,
326 }
327 }
328 #[cfg(not(feature = "polars-time"))]
329 {
330 panic!("activate one of {{'dtype-date', 'dtype-datetime', dtype-time'}} features")
331 }
332 } else {
333 DataType::String
334 }
335 }
336 else if BOOLEAN_RE.is_match(string) {
338 DataType::Boolean
339 } else if !decimal_comma && FLOAT_RE.is_match(string)
340 || decimal_comma && FLOAT_RE_DECIMAL.is_match(string)
341 {
342 DataType::Float64
343 } else if INTEGER_RE.is_match(string) {
344 if string.parse::<i64>().is_ok() {
345 DataType::Int64
346 } else {
347 #[cfg(feature = "dtype-i128")]
348 {
349 DataType::Int128
350 }
351 #[cfg(not(feature = "dtype-i128"))]
352 {
353 DataType::Int64
354 }
355 }
356 } else if try_parse_dates {
357 #[cfg(feature = "polars-time")]
358 {
359 match date_infer::infer_pattern_single(string) {
360 Some(pattern_with_offset) => match pattern_with_offset {
361 Pattern::DatetimeYMD | Pattern::DatetimeDMY => {
362 DataType::Datetime(TimeUnit::Microseconds, None)
363 },
364 Pattern::DateYMD | Pattern::DateDMY => DataType::Date,
365 Pattern::DatetimeYMDZ => {
366 DataType::Datetime(TimeUnit::Microseconds, Some(TimeZone::UTC))
367 },
368 Pattern::Time => DataType::Time,
369 },
370 None => DataType::String,
371 }
372 }
373 #[cfg(not(feature = "polars-time"))]
374 {
375 panic!("activate one of {{'dtype-date', 'dtype-datetime', dtype-time'}} features")
376 }
377 } else {
378 DataType::String
379 }
380}
381
382fn column_name(i: usize) -> PlSmallStr {
383 format_pl_smallstr!("column_{}", i + 1)
384}
385
386#[cfg(test)]
387mod tests {
388 use super::*;
389
390 #[test]
391 fn test_infer_field_schema_i64_overflow() {
392 assert_eq!(
394 infer_field_schema("9223372036854775807", false, false),
395 DataType::Int64,
396 );
397
398 let large = "12345678901234567890";
401 #[cfg(feature = "dtype-i128")]
402 assert_eq!(infer_field_schema(large, false, false), DataType::Int128,);
403 #[cfg(not(feature = "dtype-i128"))]
404 assert_eq!(infer_field_schema(large, false, false), DataType::Int64,);
405 }
406
407 #[test]
408 #[cfg(feature = "dtype-i128")]
409 fn test_finish_infer_field_schema_i64_and_i128() {
410 let mut possibilities = PlIndexSet::new();
411 possibilities.insert(DataType::Int64);
412 possibilities.insert(DataType::Int128);
413 assert_eq!(finish_infer_field_schema(&possibilities), DataType::Int128);
414 }
415}