///|
fn check_text_size(text : String) -> Unit raise FcaError {
if text.length() > 33554432 {
raise Budget("text input exceeds 32 Mi UTF-16 code units")
}
}
///|
fn decimal_count(text : String, maximum : Int32) -> Int32 raise FcaError {
let chars = text.to_array()
if chars.is_empty() || chars.length() > 5 {
raise Invalid("expected a bounded unsigned decimal count")
}
let mut result = 0
for c in chars {
if c < '0' || c > '9' {
raise Invalid("count contains a nondecimal character")
}
result = result * 10 + c.to_int() - '0'.to_int()
if result > maximum {
raise Budget("declared dimension exceeds the supported maximum")
}
}
result
}
///|
/// Strict Burmeister binary CXT: B, blank, counts, blank, labels, X/. incidence.
/// LF and CRLF accepted; one optional terminal line ending, no trailing records.
pub fn parse_cxt(text : String) -> Context raise FcaError {
check_text_size(text)
let normalized = text.replace_all(old="\r\n", new="\n")
if normalized.to_array().contains('\r') {
raise Invalid("CXT contains a bare carriage return")
}
let lines = normalized.split("\n").map(fn(s) { s.to_owned() }).to_array()
if normalized.length() > 0 && lines[lines.length() - 1] == "" {
ignore(lines.pop())
}
if lines.length() < 5 || lines[0] != "B" || lines[1] != "" || lines[4] != "" {
raise Invalid("invalid binary CXT header")
}
let objects = decimal_count(lines[2], 4096)
let attributes = decimal_count(lines[3], 128)
if lines.length() != 5 + objects + attributes + objects {
raise Invalid("CXT record count mismatch or trailing content")
}
let object_names = Array::makei(objects, fn(i) { lines[5 + i] })
let attribute_names = Array::makei(attributes, fn(i) {
lines[5 + objects + i]
})
let rows : Array[Array[Bool]] = []
for o = 0; o < objects; o = o + 1 {
let chars = lines[5 + objects + attributes + o].to_array()
if chars.length() != attributes {
raise Invalid("CXT incidence row width mismatch")
}
let row : Array[Bool] = []
for c in chars {
match c {
'X' => row.push(true)
'.' => row.push(false)
_ => raise Invalid("CXT incidence cells must be X or .")
}
}
rows.push(row)
}
context(object_names, attribute_names, rows)
}
///|
/// Reject multiline labels because CXT has no quoting mechanism; use CSV/JSON instead.
pub fn Context::to_cxt(self : Context) -> String raise FcaError {
let lines = [
"B",
"",
self.object_count().to_string(),
self.attribute_count().to_string(),
"",
]
for name in self.object_names {
if name.to_array().contains('\n') || name.to_array().contains('\r') {
raise Invalid("CXT cannot represent multiline labels")
}
lines.push(name)
}
for name in self.attribute_names {
if name.to_array().contains('\n') || name.to_array().contains('\r') {
raise Invalid("CXT cannot represent multiline labels")
}
lines.push(name)
}
for row in self.incidence {
lines.push(row.map(fn(value) { if value { "X" } else { "." } }).join(""))
}
lines.join("\n") + "\n"
}