///|
/// Trim keys and values so records from different input sources share one
/// normalization policy before validation or serialization.
pub fn normalize_rows(
rows : Array[Map[String, String]],
) -> Array[Map[String, String]] {
rows.map(fn(row) {
let normalized = Map([])
for key, value in row {
normalized[key.trim().to_owned()] = value.trim().to_owned()
}
normalized
})
}
///|
/// Project records to a stable set of columns, filling missing cells with "".
pub fn project_rows(
rows : Array[Map[String, String]],
columns : Array[String],
) -> Array[Map[String, String]] {
rows.map(fn(row) {
let projected = Map([])
for column in columns {
projected[column] = match row.get(column) {
Some(value) => value
None => ""
}
}
projected
})
}
///|
/// Return all column names in sorted order without duplicates.
pub fn row_columns(rows : Array[Map[String, String]]) -> Array[String] {
let seen = Map([])
for row in rows {
for key in row.keys() {
seen[key] = ()
}
}
let columns = seen.keys().to_array()
columns.sort()
columns
}
///|
/// Serialize rows as RFC 4180-compatible single-line CSV records.
pub fn render_csv(
rows : Array[Map[String, String]],
columns : Array[String],
) -> String {
let lines = [columns.map(escape_csv).join(",")]
for row in rows {
lines.push(
columns
.map(fn(column) {
escape_csv(
match row.get(column) {
Some(value) => value
None => ""
},
)
})
.join(","),
)
}
lines.join("\n") + "\n"
}
///|
/// Serialize rows as JSONL with every cell represented as a JSON string.
pub fn render_jsonl(rows : Array[Map[String, String]]) -> String {
let lines = rows.map(fn(row) {
let object = Map([])
for key, value in row {
object[key] = Json::string(value)
}
Json::object(object).stringify()
})
if lines.is_empty() {
""
} else {
lines.join("\n") + "\n"
}
}
///|
fn escape_csv(value : String) -> String {
let mut escaped = ""
for ch in value {
if ch == '"' {
escaped = escaped + "\"\""
} else {
escaped = escaped + "\{ch}"
}
}
if value.contains(",") || value.contains("\"") || value.contains("\n") {
"\"\{escaped}\""
} else {
escaped
}
}