use anyhow::Result;
use crate::input::{ParsedFeature, TimeFormat};
use stt_core::timestamp::{scale_timestamp_to_ms, TimestampUnit};
pub(crate) enum RowOutcome {
Feature(Box<ParsedFeature>),
GeomSkip,
}
#[derive(Default)]
pub(crate) struct VertexCoercions {
pub timestamps: usize,
pub values: usize,
}
impl VertexCoercions {
pub(crate) fn warn(&self, source: &str) {
if self.timestamps > 0 {
tracing::warn!(
"{} per-vertex {source} timestamp element(s) were NULL/unmappable and defaulted \
to 0 (epoch); check vertex_timestamps for gaps",
self.timestamps
);
}
if self.values > 0 {
tracing::warn!(
"{} per-vertex {source} value element(s) were NULL/unmappable and defaulted to NaN; \
check vertex_values / vertex_value_matrix for gaps",
self.values
);
}
}
}
pub(crate) fn warn_dropped_columns<'a, I>(
props: I,
seen_props: &std::collections::HashSet<String>,
total_rows: usize,
source: &str,
unmappable_examples: &str,
) where
I: IntoIterator<Item = (&'a str, Option<String>)>,
{
let dropped: Vec<String> = props
.into_iter()
.filter(|(name, _)| !seen_props.contains(*name))
.map(|(name, ty)| match ty {
Some(ty) => format!("{name} ({ty})"),
None => name.to_string(),
})
.collect();
if !dropped.is_empty() {
tracing::warn!(
"{} {source} source column(s) carried no value in any of {total_rows} rows and were \
dropped from tiles (unmappable column type — e.g. {unmappable_examples} — or \
entirely NULL): {}",
dropped.len(),
dropped.join(", ")
);
}
}
pub(crate) fn apply_int_time_format(v: i64, time_format: TimeFormat, row: usize) -> Result<i64> {
match time_format {
TimeFormat::UnixSec => scale_timestamp_to_ms(v, TimestampUnit::Second).map_err(|_| {
anyhow::anyhow!(
"row {row}: second-precision timestamp {v} overflows the millisecond range"
)
}),
TimeFormat::UnixMs | TimeFormat::Iso8601 => Ok(v),
}
}
pub(crate) fn json_number_or_null(v: f64) -> serde_json::Value {
serde_json::Number::from_f64(v)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn int_time_format_mapping() {
assert_eq!(apply_int_time_format(5, TimeFormat::UnixSec, 0).unwrap(), 5000);
assert_eq!(apply_int_time_format(5, TimeFormat::UnixMs, 0).unwrap(), 5);
assert_eq!(apply_int_time_format(5, TimeFormat::Iso8601, 0).unwrap(), 5);
}
#[test]
fn int_time_format_second_overflow_errors_with_row_context() {
let err = apply_int_time_format(i64::MAX, TimeFormat::UnixSec, 7).unwrap_err();
let msg = format!("{err:#}");
assert!(msg.contains("row 7"), "{msg}");
assert!(msg.contains("overflows"), "{msg}");
}
#[test]
fn nan_float_becomes_json_null() {
assert_eq!(json_number_or_null(f64::NAN), serde_json::Value::Null);
assert_eq!(json_number_or_null(1.5), serde_json::json!(1.5));
}
}