chore: reformat
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,5 @@
|
||||
use std::collections::HashSet;
|
||||
use serde_json::Value;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum FieldKind {
|
||||
@ -51,73 +51,76 @@ pub fn analyze_records(records: &[Value]) -> Vec<FieldProposal> {
|
||||
}
|
||||
}
|
||||
|
||||
fields.into_iter().map(|field| {
|
||||
let values: Vec<&Value> = records.iter()
|
||||
.filter_map(|r| r.get(&field))
|
||||
.collect();
|
||||
fields
|
||||
.into_iter()
|
||||
.map(|field| {
|
||||
let values: Vec<&Value> = records.iter().filter_map(|r| r.get(&field)).collect();
|
||||
|
||||
let all_numeric = values.iter().all(|v| v.is_number());
|
||||
let all_string = values.iter().all(|v| v.is_string());
|
||||
let all_numeric = values.iter().all(|v| v.is_number());
|
||||
let all_string = values.iter().all(|v| v.is_string());
|
||||
|
||||
if all_numeric {
|
||||
return FieldProposal {
|
||||
field,
|
||||
kind: FieldKind::Measure,
|
||||
distinct_values: vec![],
|
||||
accepted: true,
|
||||
};
|
||||
}
|
||||
|
||||
if all_string {
|
||||
let distinct: HashSet<&str> = values.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.collect();
|
||||
let distinct_vec: Vec<String> = distinct.into_iter().map(String::from).collect();
|
||||
let n = distinct_vec.len();
|
||||
let _total = values.len();
|
||||
|
||||
// Check if looks like date
|
||||
let looks_like_date = distinct_vec.iter().any(|s| {
|
||||
s.contains('-') && s.len() >= 8
|
||||
|| s.starts_with("Q") && s.len() == 2
|
||||
|| ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]
|
||||
.iter().any(|m| s.starts_with(m))
|
||||
});
|
||||
|
||||
if looks_like_date {
|
||||
if all_numeric {
|
||||
return FieldProposal {
|
||||
field,
|
||||
kind: FieldKind::TimeCategory,
|
||||
distinct_values: distinct_vec,
|
||||
kind: FieldKind::Measure,
|
||||
distinct_values: vec![],
|
||||
accepted: true,
|
||||
};
|
||||
}
|
||||
|
||||
if n <= CATEGORY_THRESHOLD {
|
||||
if all_string {
|
||||
let distinct: HashSet<&str> = values.iter().filter_map(|v| v.as_str()).collect();
|
||||
let distinct_vec: Vec<String> = distinct.into_iter().map(String::from).collect();
|
||||
let n = distinct_vec.len();
|
||||
let _total = values.len();
|
||||
|
||||
// Check if looks like date
|
||||
let looks_like_date = distinct_vec.iter().any(|s| {
|
||||
s.contains('-') && s.len() >= 8
|
||||
|| s.starts_with("Q") && s.len() == 2
|
||||
|| [
|
||||
"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct",
|
||||
"Nov", "Dec",
|
||||
]
|
||||
.iter()
|
||||
.any(|m| s.starts_with(m))
|
||||
});
|
||||
|
||||
if looks_like_date {
|
||||
return FieldProposal {
|
||||
field,
|
||||
kind: FieldKind::TimeCategory,
|
||||
distinct_values: distinct_vec,
|
||||
accepted: true,
|
||||
};
|
||||
}
|
||||
|
||||
if n <= CATEGORY_THRESHOLD {
|
||||
return FieldProposal {
|
||||
field,
|
||||
kind: FieldKind::Category,
|
||||
distinct_values: distinct_vec,
|
||||
accepted: true,
|
||||
};
|
||||
}
|
||||
|
||||
return FieldProposal {
|
||||
field,
|
||||
kind: FieldKind::Category,
|
||||
kind: FieldKind::Label,
|
||||
distinct_values: distinct_vec,
|
||||
accepted: true,
|
||||
accepted: false,
|
||||
};
|
||||
}
|
||||
|
||||
return FieldProposal {
|
||||
// Mixed or other: treat as label
|
||||
FieldProposal {
|
||||
field,
|
||||
kind: FieldKind::Label,
|
||||
distinct_values: distinct_vec,
|
||||
distinct_values: vec![],
|
||||
accepted: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Mixed or other: treat as label
|
||||
FieldProposal {
|
||||
field,
|
||||
kind: FieldKind::Label,
|
||||
distinct_values: vec![],
|
||||
accepted: false,
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Extract nested array from JSON by dot-path
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
pub mod wizard;
|
||||
pub mod analyzer;
|
||||
|
||||
pub mod wizard;
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
use serde_json::Value;
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde_json::Value;
|
||||
|
||||
use super::analyzer::{FieldKind, FieldProposal, analyze_records, extract_array_at_path, find_array_paths};
|
||||
use crate::model::Model;
|
||||
use super::analyzer::{
|
||||
analyze_records, extract_array_at_path, find_array_paths, FieldKind, FieldProposal,
|
||||
};
|
||||
use crate::model::cell::{CellKey, CellValue};
|
||||
use crate::model::Model;
|
||||
|
||||
// ── Pipeline (no UI state) ────────────────────────────────────────────────────
|
||||
|
||||
@ -75,10 +77,16 @@ impl ImportPipeline {
|
||||
|
||||
/// Build a Model from the current proposals. Pure — no side effects.
|
||||
pub fn build_model(&self) -> Result<Model> {
|
||||
let categories: Vec<&FieldProposal> = self.proposals.iter()
|
||||
.filter(|p| p.accepted && matches!(p.kind, FieldKind::Category | FieldKind::TimeCategory))
|
||||
let categories: Vec<&FieldProposal> = self
|
||||
.proposals
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.accepted && matches!(p.kind, FieldKind::Category | FieldKind::TimeCategory)
|
||||
})
|
||||
.collect();
|
||||
let measures: Vec<&FieldProposal> = self.proposals.iter()
|
||||
let measures: Vec<&FieldProposal> = self
|
||||
.proposals
|
||||
.iter()
|
||||
.filter(|p| p.accepted && p.kind == FieldKind::Measure)
|
||||
.collect();
|
||||
|
||||
@ -112,7 +120,8 @@ impl ImportPipeline {
|
||||
let mut valid = true;
|
||||
|
||||
for cat_proposal in &categories {
|
||||
let val = map.get(&cat_proposal.field)
|
||||
let val = map
|
||||
.get(&cat_proposal.field)
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string())
|
||||
.or_else(|| map.get(&cat_proposal.field).map(|v| v.to_string()));
|
||||
@ -128,7 +137,9 @@ impl ImportPipeline {
|
||||
}
|
||||
}
|
||||
|
||||
if !valid { continue; }
|
||||
if !valid {
|
||||
continue;
|
||||
}
|
||||
|
||||
for measure in &measures {
|
||||
if let Some(val) = map.get(&measure.field).and_then(|v| v.as_f64()) {
|
||||
@ -180,7 +191,12 @@ impl ImportWizard {
|
||||
WizardStep::ReviewProposals
|
||||
};
|
||||
|
||||
Self { pipeline, step, cursor: 0, message: None }
|
||||
Self {
|
||||
pipeline,
|
||||
step,
|
||||
cursor: 0,
|
||||
message: None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Step transitions ──────────────────────────────────────────────────────
|
||||
@ -219,7 +235,9 @@ impl ImportWizard {
|
||||
WizardStep::ReviewProposals => self.pipeline.proposals.len(),
|
||||
_ => 0,
|
||||
};
|
||||
if len == 0 { return; }
|
||||
if len == 0 {
|
||||
return;
|
||||
}
|
||||
if delta > 0 {
|
||||
self.cursor = (self.cursor + 1).min(len - 1);
|
||||
} else if self.cursor > 0 {
|
||||
@ -240,18 +258,22 @@ impl ImportWizard {
|
||||
if self.cursor < self.pipeline.proposals.len() {
|
||||
let p = &mut self.pipeline.proposals[self.cursor];
|
||||
p.kind = match p.kind {
|
||||
FieldKind::Category => FieldKind::Measure,
|
||||
FieldKind::Measure => FieldKind::TimeCategory,
|
||||
FieldKind::Category => FieldKind::Measure,
|
||||
FieldKind::Measure => FieldKind::TimeCategory,
|
||||
FieldKind::TimeCategory => FieldKind::Label,
|
||||
FieldKind::Label => FieldKind::Category,
|
||||
FieldKind::Label => FieldKind::Category,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Model name input ──────────────────────────────────────────────────────
|
||||
|
||||
pub fn push_name_char(&mut self, c: char) { self.pipeline.model_name.push(c); }
|
||||
pub fn pop_name_char(&mut self) { self.pipeline.model_name.pop(); }
|
||||
pub fn push_name_char(&mut self, c: char) {
|
||||
self.pipeline.model_name.push(c);
|
||||
}
|
||||
pub fn pop_name_char(&mut self) {
|
||||
self.pipeline.model_name.pop();
|
||||
}
|
||||
|
||||
// ── Delegate build to pipeline ────────────────────────────────────────────
|
||||
|
||||
@ -262,9 +284,9 @@ impl ImportWizard {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use super::ImportPipeline;
|
||||
use crate::import::analyzer::FieldKind;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn flat_array_auto_selected() {
|
||||
@ -337,7 +359,9 @@ mod tests {
|
||||
fn build_model_fails_with_no_accepted_categories() {
|
||||
let raw = json!([{"revenue": 100.0, "cost": 50.0}]);
|
||||
let mut p = ImportPipeline::new(raw);
|
||||
for prop in &mut p.proposals { prop.accepted = false; }
|
||||
for prop in &mut p.proposals {
|
||||
prop.accepted = false;
|
||||
}
|
||||
assert!(p.build_model().is_err());
|
||||
}
|
||||
|
||||
@ -364,14 +388,20 @@ mod tests {
|
||||
use crate::model::cell::CellKey;
|
||||
let k_east = CellKey::new(vec![
|
||||
("Measure".to_string(), "revenue".to_string()),
|
||||
("region".to_string(), "East".to_string()),
|
||||
("region".to_string(), "East".to_string()),
|
||||
]);
|
||||
let k_west = CellKey::new(vec![
|
||||
("Measure".to_string(), "revenue".to_string()),
|
||||
("region".to_string(), "West".to_string()),
|
||||
("region".to_string(), "West".to_string()),
|
||||
]);
|
||||
assert_eq!(model.get_cell(&k_east).and_then(|v| v.as_f64()), Some(100.0));
|
||||
assert_eq!(model.get_cell(&k_west).and_then(|v| v.as_f64()), Some(200.0));
|
||||
assert_eq!(
|
||||
model.get_cell(&k_east).and_then(|v| v.as_f64()),
|
||||
Some(100.0)
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_cell(&k_west).and_then(|v| v.as_f64()),
|
||||
Some(200.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user