///|
/// Return a trimmed identifier, preserving case and semantic content.
pub fn normalize_identifier(value : String) -> String {
value.trim().to_owned()
}
///|
/// Return a trimmed feature record without changing numeric values.
pub fn normalize_feature(feature : Feature) -> Feature {
{
id: normalize_identifier(feature.id),
kind: feature.kind,
nominal: feature.nominal,
lower: feature.lower,
upper: feature.upper,
datum: feature.datum,
}
}
///|
/// Return a trimmed device record without changing capability values.
pub fn normalize_device(device : Device) -> Device {
{
id: normalize_identifier(device.id),
accuracy: device.accuracy,
reach: device.reach,
supported_kinds: device.supported_kinds,
}
}
///|
/// Return a trimmed observation record without changing measured value.
pub fn normalize_observation(observation : Observation) -> Observation {
{
feature_id: normalize_identifier(observation.feature_id),
value: observation.value,
sample_count: observation.sample_count,
}
}
///|
/// Normalize all identifiers in a catalog while preserving array order.
pub fn normalize_catalog(catalog : Catalog) -> Catalog {
let features = []
for feature in catalog.features {
features.push(normalize_feature(feature))
}
let devices = []
for device in catalog.devices {
devices.push(normalize_device(device))
}
{ features, devices }
}
///|
/// Return whether two features differ in any field.
pub fn normalization_changed(before : Feature, after : Feature) -> Bool {
before.id != after.id ||
before.kind != after.kind ||
before.nominal != after.nominal ||
before.lower != after.lower ||
before.upper != after.upper ||
before.datum != after.datum
}
///|
/// Return whether feature identifiers are unique and non-blank.
pub fn ids_are_unique(features : Array[Feature]) -> Bool {
let seen = []
let mut result = true
for feature in features {
if feature.id.trim().length() == 0 {
result = false
}
for id in seen {
if id == feature.id {
result = false
}
}
seen.push(feature.id)
}
result
}
///|
/// Return the canonical CSV line for a feature.
pub fn canonical_feature_line(feature : Feature) -> String {
feature.id +
"," +
feature_kind_name(feature.kind) +
"," +
feature.nominal.to_string() +
"," +
feature.lower.to_string() +
"," +
feature.upper.to_string() +
"," +
feature.datum.to_string()
}
///|
/// Return the canonical CSV line for an observation.
pub fn canonical_observation_line(observation : Observation) -> String {
observation.feature_id +
"," +
observation.value.to_string() +
"," +
observation.sample_count.to_string()
}
///|
/// Return a stable part identifier suitable for report headers.
pub fn canonical_part_id(part_id : String) -> String {
normalize_identifier(part_id)
}
///|
/// Normalize a list of observations while preserving source order.
pub fn normalize_observations(
observations : Array[Observation],
) -> Array[Observation] {
let result = []
for observation in observations {
result.push(normalize_observation(observation))
}
result
}
///|
/// Count features whose identifiers change during normalization.
pub fn normalization_change_count(features : Array[Feature]) -> Int {
let mut result = 0
for feature in features {
if normalization_changed(feature, normalize_feature(feature)) {
result += 1
}
}
result
}
///|
/// Count devices whose identifiers change during normalization.
pub fn device_normalization_change_count(devices : Array[Device]) -> Int {
let mut result = 0
for device in devices {
let normalized = normalize_device(device)
if normalized.id != device.id {
result += 1
}
}
result
}
///|
/// Return whether all identifiers are already canonical.
pub fn is_canonical_catalog(catalog : Catalog) -> Bool {
normalization_change_count(catalog.features) == 0 &&
device_normalization_change_count(catalog.devices) == 0
}