///|
/// Parsed feature rows plus all row-level diagnostics.
pub(all) struct ParsedFeatures {
features : Array[Feature]
diagnostics : Array[Diagnostic]
}
///|
/// Parsed observation rows plus all row-level diagnostics.
pub(all) struct ParsedObservations {
observations : Array[Observation]
diagnostics : Array[Diagnostic]
}
///|
fn parse_double_or_none(text : String) -> Double? {
Some(@string.parse_double(text)) catch {
_ => None
}
}
///|
fn parse_int_or_none(text : String) -> Int? {
Some(@string.parse_int(text)) catch {
_ => None
}
}
///|
fn csv_fields(line : String) -> Array[String] {
let fields = []
for field in line.trim().split(",") {
fields.push(field.to_owned())
}
if fields.length() >= 7 && fields[0].trim().has_prefix("\"") {
let first = fields[0].trim().to_owned() + "," + fields[1].trim().to_owned()
let result = [first]
for field in fields[2:] {
result.push(field)
}
result
} else {
fields
}
}
///|
fn csv_unquote(value : String) -> String {
let text = value.trim().to_owned()
let chars = text.to_array()
if chars.length() >= 2 && chars[0] == '"' && chars[chars.length() - 1] == '"' {
let inner = []
for i in 1..<(chars.length() - 1) {
inner.push(chars[i])
}
String::from_array(inner)
} else {
text
}
}
///|
fn feature_kind_or_none(text : String) -> FeatureKind? {
match text.trim() {
"hole" => Some(FeatureKind::Hole)
"shaft" => Some(FeatureKind::Shaft)
"plane" => Some(FeatureKind::Plane)
"slot" => Some(FeatureKind::Slot)
"circle" => Some(FeatureKind::Circle)
"cylinder" => Some(FeatureKind::Cylinder)
"datum" => Some(FeatureKind::Datum)
_ => None
}
}
///|
fn parse_bool_or_none(text : String) -> Bool? {
match text.trim() {
"true" => Some(true)
"false" => Some(false)
_ => None
}
}
///|
/// Parse feature CSV without dropping malformed rows.
pub fn parse_feature_csv_strict(input : String) -> ParsedFeatures {
let features = []
let diagnostics = []
for row, line in input.split("\n") {
let fields = csv_fields(line.to_owned())
if line.trim().length() == 0 || fields[0].trim() == "id" {
continue
}
if fields.length() < 6 {
diagnostics.push(
error_diagnostic(
code="csv.row.short",
message="feature row needs six fields",
index=row,
),
)
continue
}
let kind = feature_kind_or_none(fields[1])
if kind is None {
diagnostics.push(
error_diagnostic(
code="csv.kind.unknown",
message="feature kind is not recognized",
index=row,
),
)
continue
}
let nominal = parse_double_or_none(fields[2].trim().to_owned())
let lower = parse_double_or_none(fields[3].trim().to_owned())
let upper = parse_double_or_none(fields[4].trim().to_owned())
let datum = parse_bool_or_none(fields[5])
let numeric_ok = nominal is Some(_) && lower is Some(_) && upper is Some(_)
if !numeric_ok {
diagnostics.push(
error_diagnostic(
code="csv.number.invalid",
message="feature numeric field is invalid",
index=row,
),
)
}
if datum is None {
diagnostics.push(
error_diagnostic(
code="csv.boolean.invalid",
message="feature datum must be true or false",
index=row,
),
)
}
match (nominal, lower, upper, datum) {
(Some(nominal), Some(lower), Some(upper), Some(datum)) =>
features.push({
id: csv_unquote(fields[0]),
kind: kind.unwrap(),
nominal,
lower,
upper,
datum,
})
_ => ()
}
}
{ features, diagnostics }
}
///|
/// Parse observation CSV without dropping malformed rows.
pub fn parse_observation_csv_strict(input : String) -> ParsedObservations {
let observations = []
let diagnostics = []
for row, line in input.split("\n") {
let fields = csv_fields(line.to_owned())
if line.trim().length() == 0 || fields[0].trim() == "feature_id" {
continue
}
if fields.length() < 3 {
diagnostics.push(
error_diagnostic(
code="csv.observation.row.short",
message="observation row needs three fields",
index=row,
),
)
continue
}
let value = parse_double_or_none(fields[1].trim().to_owned())
let sample_count = parse_int_or_none(fields[2].trim().to_owned())
if value is None {
diagnostics.push(
error_diagnostic(
code="csv.observation.value.invalid",
message="observation value is invalid",
index=row,
),
)
}
if sample_count is None || !sample_count_is_valid(sample_count.unwrap()) {
diagnostics.push(
error_diagnostic(
code="csv.observation.samples.invalid",
message="observation sample count must be positive",
index=row,
),
)
}
match (value, sample_count) {
(Some(value), Some(sample_count)) if sample_count_is_valid(sample_count) =>
observations.push({
feature_id: fields[0].trim().to_owned(),
value,
sample_count,
})
_ => ()
}
}
{ observations, diagnostics }
}
///|
/// Collect strict feature CSV diagnostics.
pub fn csv_diagnostics(input : String) -> Array[Diagnostic] {
parse_feature_csv_strict(input).diagnostics
}