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