Skip to main content

datui_lib/formats/
types.rs

1//! Type names, field names and the other small helpers the parser and the spec share.
2
3use super::*;
4
5impl Field {
6    /// A named field of `ty`, as stored.
7    pub fn plain(name: &str, ty: Type, endian: Option<Endian>) -> Self {
8        Self {
9            name: Some(name.to_string()),
10            ty,
11            size: None,
12            endian,
13            meaning: Meaning::Plain,
14            null: None,
15            count: None,
16            flatten: false,
17            file: None,
18            at: None,
19            encoding: Encoding::Utf8,
20            delta: Delta::None,
21            bits: Vec::new(),
22            group: Vec::new(),
23            string_at: None,
24            lookup: None,
25            description: None,
26            unit: None,
27        }
28    }
29
30    /// Whether the reader of fixed records reads it as it is: one value or an Array
31    /// of them, of a size known before a record is read, and nothing derived.
32    pub fn is_fixed_width(&self) -> bool {
33        self.ty.width().is_some() || matches!(self.ty, Type::Str | Type::Bytes | Type::Pad)
34    }
35}
36
37/// Every field of the records: the common ones, then each variant's.
38pub fn all_fields(records: &Records) -> impl Iterator<Item = &Field> {
39    records
40        .fields
41        .iter()
42        .chain(records.variants.iter().flat_map(|v| &v.fields))
43}
44
45/// The parts `{name}` and `{name:%Y%m%d}` of a `[files]` path, in order.
46pub fn path_parts(pattern: &str) -> Result<Vec<PathPart>, String> {
47    let mut parts: Vec<PathPart> = Vec::new();
48    let mut rest = pattern;
49    if pattern.trim().is_empty()
50        || Path::new(pattern).is_absolute()
51        || pattern.split('/').any(|c| c == ".." || c.is_empty())
52    {
53        return Err(
54            "expected a path relative to the directory, such as {date:%Y%m%d}/trades.bin".into(),
55        );
56    }
57    while let Some(open) = rest.find('{') {
58        let after = &rest[open + 1..];
59        let close = after.find('}').ok_or("a `{` without its `}`")?;
60        let inside = &after[..close];
61        let (name, date) = match inside.split_once(':') {
62            Some((name, format)) => (name, Some(format.to_string())),
63            None => (inside, None),
64        };
65        if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
66            return Err(format!(
67                "`{{{inside}}}`: a part is named with letters, digits and `_`"
68            ));
69        }
70        if inside.contains('/') {
71            return Err(format!(
72                "`{{{inside}}}`: a part stays inside one directory's name"
73            ));
74        }
75        if let Some(format) = &date
76            && (format.is_empty()
77                || chrono::format::StrftimeItems::new(format)
78                    .any(|i| matches!(i, chrono::format::Item::Error)))
79        {
80            return Err(format!(
81                "`{{{inside}}}`: `{format}` is not a date format such as %Y%m%d"
82            ));
83        }
84        if parts.iter().any(|p| p.name == name) {
85            return Err(format!("`{name}` is named twice"));
86        }
87        parts.push(PathPart {
88            name: name.to_string(),
89            date,
90        });
91        rest = &after[close + 1..];
92    }
93    if rest.contains('}') {
94        return Err("a `}` without its `{`".into());
95    }
96    Ok(parts)
97}
98
99/// The columns a field shows as: none for `pad`, `name_0`... when flattened.
100pub(crate) fn output_names(field: &Field) -> Vec<String> {
101    let mut names = own_names(field);
102    names.extend(field.bits.iter().map(|b| b.name.clone()));
103    names
104}
105
106/// The columns of a field's own value, without its bits: `name`, or `name_0`... when
107/// flattened.
108pub(crate) fn own_names(field: &Field) -> Vec<String> {
109    let Some(name) = &field.name else {
110        return Vec::new();
111    };
112    match (&field.count, field.flatten) {
113        (Some(Amount::Given(n)), true) => (0..*n).map(|i| format!("{name}_{i}")).collect(),
114        _ => vec![name.clone()],
115    }
116}
117
118/// The smallest and largest value an integer (or bool) type holds.
119pub(crate) fn integer_range(ty: Type) -> Option<(i128, i128)> {
120    let bits = match ty {
121        Type::Unsigned(n) | Type::Signed(n) => u32::from(n) * 8,
122        Type::Bool => 8,
123        _ => return None,
124    };
125    Some(match ty {
126        Type::Signed(_) => (-(1i128 << (bits - 1)), (1i128 << (bits - 1)) - 1),
127        _ => (0, (1i128 << bits) - 1),
128    })
129}
130
131/// Whether `name` is one file's name, with no directory in it.
132pub(crate) fn is_file_name(name: &str) -> bool {
133    let mut parts = Path::new(name).components();
134    matches!(
135        (parts.next(), parts.next()),
136        (Some(std::path::Component::Normal(_)), None)
137    ) && !name.contains(['/', '\\'])
138}
139
140/// A type name and the byte order its suffix asks for.
141pub(crate) fn parse_type(text: &str) -> Option<(Type, Option<Endian>)> {
142    match text {
143        "str" => return Some((Type::Str, None)),
144        "strz" => return Some((Type::Strz, None)),
145        "vu" => return Some((Type::VarU, None)),
146        "vs" => return Some((Type::VarS, None)),
147        "bf2" => return Some((Type::BFloat16, None)),
148        "bf2le" => return Some((Type::BFloat16, Some(Endian::Little))),
149        "bf2be" => return Some((Type::BFloat16, Some(Endian::Big))),
150        "bytes" => return Some((Type::Bytes, None)),
151        "pad" => return Some((Type::Pad, None)),
152        "bool" => return Some((Type::Bool, None)),
153        _ => {}
154    }
155    let (base, endian) = if let Some(base) = text.strip_suffix("le") {
156        (base, Some(Endian::Little))
157    } else if let Some(base) = text.strip_suffix("be") {
158        (base, Some(Endian::Big))
159    } else {
160        (text, None)
161    };
162    let mut chars = base.chars();
163    let kind = chars.next()?;
164    let digits = chars.as_str();
165    if digits.len() != 1 {
166        return None;
167    }
168    let width: u8 = digits.parse().ok()?;
169    let ty = match (kind, width) {
170        ('u', 1..=8) => Type::Unsigned(width),
171        ('s', 1..=8) => Type::Signed(width),
172        ('f', 2 | 4 | 8) => Type::Float(width),
173        _ => return None,
174    };
175    Some((ty, endian))
176}
177
178pub(crate) fn type_name(ty: Type) -> String {
179    match ty {
180        Type::Unsigned(n) => format!("u{n}"),
181        Type::Signed(n) => format!("s{n}"),
182        Type::Float(n) => format!("f{n}"),
183        Type::Bool => "bool".into(),
184        Type::BFloat16 => "bf2".into(),
185        Type::Str => "str".into(),
186        Type::Strz => "strz".into(),
187        Type::Bytes => "bytes".into(),
188        Type::Pad => "pad".into(),
189        Type::VarU => "vu".into(),
190        Type::VarS => "vs".into(),
191        Type::Group => "group".into(),
192    }
193}
194
195/// Nanoseconds from the Unix epoch to `text`: a date, or an RFC 3339 date-time (a
196/// date-time without an offset is UTC).
197pub(crate) fn parse_epoch(text: &str) -> Option<i64> {
198    use chrono::{DateTime, NaiveDate, NaiveDateTime};
199    let at = if let Ok(dt) = DateTime::parse_from_rfc3339(text) {
200        dt.naive_utc()
201    } else if let Ok(dt) = NaiveDateTime::parse_from_str(text, "%Y-%m-%dT%H:%M:%S%.f") {
202        dt
203    } else if let Ok(dt) = NaiveDateTime::parse_from_str(text, "%Y-%m-%d %H:%M:%S%.f") {
204        dt
205    } else {
206        NaiveDate::parse_from_str(text, "%Y-%m-%d")
207            .ok()?
208            .and_hms_opt(0, 0, 0)?
209    };
210    at.and_utc().timestamp_nanos_opt()
211}
212
213/// What a spec name may be: namespaced, as `acme.l2feed` is, so it cannot be taken for
214/// a built-in format.
215pub fn is_spec_name(name: &str) -> bool {
216    name.contains('.')
217        && !name.starts_with('.')
218        && !name.ends_with('.')
219        && name
220            .chars()
221            .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '_' | '-'))
222}