///|
/// Errors distinguish invalid data from an explicitly exhausted resource budget.
pub(all) suberror FcaError {
Invalid(String)
Budget(String)
} derive(Eq, Debug)
///|
/// Owned binary incidence context. All input arrays and exposed arrays are copied.
pub struct Context {
priv object_names : Array[String]
priv attribute_names : Array[String]
priv incidence : Array[Array[Bool]]
} derive(Eq, Debug)
///|
fn validate_labels(
labels : Array[String],
kind : String,
) -> Unit raise FcaError {
for i = 0; i < labels.length(); i = i + 1 {
if labels[i].length() == 0 || labels[i].length() > 4096 {
raise Invalid("\{kind}: label length must be 1..4096")
}
for j = 0; j < i; j = j + 1 {
if labels[i] == labels[j] {
raise Invalid("\{kind}: duplicate label at index \{i}")
}
}
}
}
///|
/// Up to 4096 objects and 128 attributes; empty dimensions have standard FCA semantics.
pub fn context(
objects : Array[String],
attributes : Array[String],
rows : Array[Array[Bool]],
) -> Context raise FcaError {
if objects.length() > 4096 || attributes.length() > 128 {
raise Budget("context dimensions exceed 4096 objects or 128 attributes")
}
validate_labels(objects, "objects")
validate_labels(attributes, "attributes")
if rows.length() != objects.length() {
raise Invalid("row count does not match object count")
}
let copied : Array[Array[Bool]] = []
for row in rows {
if row.length() != attributes.length() {
raise Invalid("row width does not match attribute count")
}
copied.push(row.copy())
}
{
object_names: objects.copy(),
attribute_names: attributes.copy(),
incidence: copied,
}
}
///|
pub fn Context::object_count(self : Context) -> Int32 {
self.object_names.length()
}
///|
pub fn Context::attribute_count(self : Context) -> Int32 {
self.attribute_names.length()
}
///|
pub fn Context::objects(self : Context) -> Array[String] {
self.object_names.copy()
}
///|
pub fn Context::attributes(self : Context) -> Array[String] {
self.attribute_names.copy()
}
///|
pub fn Context::rows(self : Context) -> Array[Array[Bool]] {
self.incidence.map(fn(row) { row.copy() })
}
///|
pub fn Context::has(
self : Context,
object : Int32,
attribute : Int32,
) -> Bool raise FcaError {
if object < 0 || object >= self.object_count() {
raise Invalid("object index out of range")
}
if attribute < 0 || attribute >= self.attribute_count() {
raise Invalid("attribute index out of range")
}
self.incidence[object][attribute]
}
///|
pub fn Context::object_index(self : Context, name : String) -> Int32? {
for i = 0; i < self.object_count(); i = i + 1 {
if self.object_names[i] == name {
return Some(i)
}
}
None
}
///|
pub fn Context::attribute_index(self : Context, name : String) -> Int32? {
for i = 0; i < self.attribute_count(); i = i + 1 {
if self.attribute_names[i] == name {
return Some(i)
}
}
None
}
///|
pub fn Context::density(self : Context) -> Double {
let cells = self.object_count() * self.attribute_count()
if cells == 0 {
return 0.0
}
let mut present = 0
for row in self.incidence {
for value in row {
if value {
present = present + 1
}
}
}
present.to_double() / cells.to_double()
}
///|
pub extend FcaError with Eq::{equal, not_equal}
///|
pub extend FcaError with Debug::{to_repr}
///|
pub extend Context with Eq::{equal, not_equal}
///|
pub extend Context with Debug::{to_repr}