///|
/// Inferred validation metadata for one column.
pub(all) struct CsvInferredColumnRule {
name : String
kind : CsvColumnType
required : Bool
total : Int
empty : Int
unique : Int
examples : Array[String]
} derive(Eq, Debug)
///|
/// Infer schema metadata for every column in a header-aware table.
pub fn infer_schema(table : CsvTable) -> Array[CsvInferredColumnRule] {
let profiles = profile_table(table)
let rules : Array[CsvInferredColumnRule] = Array::new()
for i in 0.. 0 && profile.empty == 0,
total: profile.total,
empty: profile.empty,
unique: profile.unique,
examples: schema_infer_examples_at(table, i, 3),
})
}
rules
}
///|
/// Infer ordinary validation rules from observed data.
pub fn infer_validation_rules(table : CsvTable) -> Array[CsvColumnRule] {
let inferred = infer_schema(table)
let rules : Array[CsvColumnRule] = Array::new()
for rule in inferred {
rules.push({ name: rule.name, kind: rule.kind, required: rule.required })
}
rules
}
///|
/// Validate a table against its inferred schema.
pub fn validate_inferred_schema(table : CsvTable) -> Array[CsvValidationError] {
validate_table(table, infer_validation_rules(table))
}
///|
/// Infer schema from CSV text using automatic dialect detection.
pub fn infer_schema_auto(input : String) -> Array[CsvInferredColumnRule] {
infer_schema(parse_table_auto(input))
}
///|
/// Render inferred schema as a Markdown table.
pub fn schema_inference_to_markdown(
rules : Array[CsvInferredColumnRule],
) -> String {
let out = StringBuilder()
out.write_string("| column | type | required | empty | unique | examples |\n")
out.write_string("| --- | --- | --- | ---: | ---: | --- |\n")
for rule in rules {
out.write_string("| ")
schema_infer_write_markdown_cell(out, rule.name)
out.write_string(" | ")
out.write_string(schema_infer_column_type_name(rule.kind))
out.write_string(" | ")
out.write_string(if rule.required { "yes" } else { "no" })
out.write_string(" | \{rule.empty}/\{rule.total} | \{rule.unique} | ")
schema_infer_write_markdown_cell(
out,
schema_infer_join(rule.examples, ", "),
)
out.write_string(" |\n")
}
out.to_string()
}
///|
/// Render inferred schema as compact text for terminal output.
pub fn schema_inference_to_text(rules : Array[CsvInferredColumnRule]) -> String {
if rules.length() == 0 {
return "no columns"
}
let out = StringBuilder()
for i in 0.. 0 {
out.write_char('\n')
}
let rule = rules[i]
out.write_string(rule.name)
out.write_string(": ")
out.write_string(schema_infer_column_type_name(rule.kind))
out.write_string(if rule.required { ", required" } else { ", optional" })
out.write_string(", empty=\{rule.empty}/\{rule.total}")
out.write_string(", unique=\{rule.unique}")
if rule.examples.length() > 0 {
out.write_string(", examples=")
out.write_string(schema_infer_join(rule.examples, "|"))
}
}
out.to_string()
}
///|
/// Render inferred schema for a table as Markdown.
pub fn table_schema_markdown(table : CsvTable) -> String {
schema_inference_to_markdown(infer_schema(table))
}
///|
/// Render inferred schema for a table as compact text.
pub fn table_schema_text(table : CsvTable) -> String {
schema_inference_to_text(infer_schema(table))
}
///|
/// Return true if the inferred schema marks a column as required.
pub fn schema_inferred_required(
rules : Array[CsvInferredColumnRule],
column : String,
) -> Bool {
match schema_infer_find(rules, column) {
Some(rule) => rule.required
None => false
}
}
///|
/// Return the inferred scalar type for a column.
pub fn schema_inferred_type(
rules : Array[CsvInferredColumnRule],
column : String,
) -> CsvColumnType? {
match schema_infer_find(rules, column) {
Some(rule) => Some(rule.kind)
None => None
}
}
///|
/// Return first-seen non-empty examples for one column.
pub fn column_examples(
table : CsvTable,
column : String,
limit : Int,
) -> Array[String] {
schema_infer_examples(table, column, limit)
}
///|
fn schema_infer_kind(kind : CsvInferredType) -> CsvColumnType {
match kind {
EmptyOnly => Text
IntegerColumn => Integer
FloatColumn => Float
BooleanColumn => Boolean
TextColumn => Text
}
}
///|
fn schema_infer_examples(
table : CsvTable,
column : String,
limit : Int,
) -> Array[String] {
let examples : Array[String] = Array::new()
if limit <= 0 {
return examples
}
match schema_infer_column_index(table.headers, column) {
Some(index) =>
for row in table.rows {
let value = schema_infer_cell_at(row, index)
if !value.is_empty() && !schema_infer_contains(examples, value) {
examples.push(value)
if examples.length() >= limit {
return examples
}
}
}
None => ()
}
examples
}
///|
fn schema_infer_examples_at(
table : CsvTable,
index : Int,
limit : Int,
) -> Array[String] {
let examples : Array[String] = Array::new()
if limit <= 0 {
return examples
}
for row in table.rows {
let value = schema_infer_cell_at(row, index)
if !value.is_empty() && !schema_infer_contains(examples, value) {
examples.push(value)
if examples.length() >= limit {
return examples
}
}
}
examples
}
///|
fn schema_infer_find(
rules : Array[CsvInferredColumnRule],
column : String,
) -> CsvInferredColumnRule? {
for rule in rules {
if rule.name == column {
return Some(rule)
}
}
None
}
///|
fn schema_infer_column_type_name(kind : CsvColumnType) -> String {
match kind {
Text => "text"
Integer => "integer"
Float => "float"
Boolean => "boolean"
}
}
///|
fn schema_infer_column_index(headers : Array[String], column : String) -> Int? {
for i in 0.. String {
if index >= 0 && index < row.length() {
row[index]
} else {
""
}
}
///|
fn schema_infer_contains(values : Array[String], value : String) -> Bool {
for item in values {
if item == value {
return true
}
}
false
}
///|
fn schema_infer_join(values : Array[String], separator : String) -> String {
let out = StringBuilder()
for i in 0.. 0 {
out.write_string(separator)
}
out.write_string(values[i])
}
out.to_string()
}
///|
fn schema_infer_write_markdown_cell(
out : StringBuilder,
value : String,
) -> Unit {
for ch in value.iter() {
if ch == '|' {
out.write_string("\\|")
} else if ch == '\n' || ch == '\r' {
out.write_char(' ')
} else {
out.write_char(ch)
}
}
}