1use super::*;
4
5impl Field {
6 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 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
37pub 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
45pub 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
99pub(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
106pub(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
118pub(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
131pub(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
140pub(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
195pub(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
213pub 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}