Skip to main content

mlt_core/convert/
infer.rs

1//! Column kind inference shared by the importers of dynamically typed
2//! properties (MVT, `GeoJSON`), which need one fixed [`PropKind`] per column.
3
4use std::collections::HashMap;
5
6use crate::tile::PropKind;
7
8/// Column kind inferred so far. `Unknown` is a column seen only as null, which
9/// resolves to [`PropKind::Str`].
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum InferredKind {
12    Unknown,
13    Bool,
14    I64,
15    U64,
16    F32,
17    F64,
18    Str,
19}
20
21impl InferredKind {
22    /// Merge with another kind: `I64`+`U64` widen to `I64`, floats widen to
23    /// `F64`, integers mixed with floats widen to `F64`, and any other conflict
24    /// falls back to `Str`.
25    #[must_use]
26    pub fn merge(self, other: Self) -> Self {
27        if self == Self::Unknown {
28            return other;
29        }
30        if other == Self::Unknown || self == other {
31            return self;
32        }
33        #[expect(clippy::match_same_arms)]
34        match (self, other) {
35            // Signed and unsigned integers share a signed column.
36            (Self::I64 | Self::U64, Self::I64 | Self::U64) => Self::I64,
37            // Every f32 is exactly representable as f64.
38            (Self::F32 | Self::F64, Self::F32 | Self::F64) => Self::F64,
39            // Integers mixed with floats are most likely whole-number values of a float column
40            // that the encoder wrote as integers, so the column stays a float.
41            (Self::I64 | Self::U64, Self::F32 | Self::F64)
42            | (Self::F32 | Self::F64, Self::I64 | Self::U64) => Self::F64,
43            _ => Self::Str,
44        }
45    }
46
47    #[must_use]
48    pub fn prop_kind(self) -> PropKind {
49        match self {
50            Self::Unknown | Self::Str => PropKind::Str,
51            Self::Bool => PropKind::Bool,
52            Self::I64 => PropKind::I64,
53            Self::U64 => PropKind::U64,
54            Self::F32 => PropKind::F32,
55            Self::F64 => PropKind::F64,
56        }
57    }
58}
59
60/// Column names in first-seen order, each with the kind merged from every value seen.
61#[derive(Default)]
62pub(crate) struct ColumnInference<'a> {
63    index: HashMap<&'a str, usize>,
64    names: Vec<String>,
65    kinds: Vec<InferredKind>,
66}
67
68impl<'a> ColumnInference<'a> {
69    /// Record a value of `kind` in column `name`, returning the column's index.
70    pub fn observe(&mut self, name: &'a str, kind: InferredKind) -> usize {
71        let idx = self.column(name);
72        self.merge(idx, kind);
73        idx
74    }
75
76    /// The index of column `name`, adding it on first sight.
77    pub fn column(&mut self, name: &'a str) -> usize {
78        *self.index.entry(name).or_insert_with(|| {
79            self.names.push(name.to_string());
80            self.kinds.push(InferredKind::Unknown);
81            self.names.len() - 1
82        })
83    }
84
85    /// Record a value of `kind` in the column at `idx`.
86    pub fn merge(&mut self, idx: usize, kind: InferredKind) {
87        // One bounds check rather than one per index expression.
88        let slot = &mut self.kinds[idx];
89        *slot = slot.merge(kind);
90    }
91
92    /// The column names and their resolved kinds, in first-seen order.
93    pub fn finish(self) -> (Vec<String>, Vec<PropKind>) {
94        let kinds = self
95            .kinds
96            .into_iter()
97            .map(InferredKind::prop_kind)
98            .collect();
99        (self.names, kinds)
100    }
101}