///|
///|
fn property_display_value(value : Value) -> String {
match value {
Null | Omitted => ""
String(text) => text
Enumeration(text) => text
Integer(number) => number.to_string()
Real(number) => number.to_string()
Reference(id) => "#" + id.to_string()
Binary(text) => text
Typed(name~, value~) => name + "(" + property_display_value(value) + ")"
List(values) =>
"(" + values.map(item => property_display_value(item)).join(",") + ")"
}
}
///|
fn property_is_numeric(text : String) -> Bool {
let parsed = @string.parse_double(text) catch { _ => 0.0 }
parsed != 0.0 || text == "0" || text == "0.0"
}
///|
fn property_names_for_product(
index : ModelIndex,
product : Product,
) -> Array[String] raise ParseError {
property_sets_for_product(index, product).fold(init=Array::new(), (
result,
set,
) => {
for property in set.properties {
if !result.contains(property.name) {
result.push(property.name)
}
}
result
})
}
///|
fn property_map_for_product(
index : ModelIndex,
product : Product,
) -> Map[String, String] raise ParseError {
property_sets_for_product(index, product).fold(init=Map([]), (result, set) => {
for property in set.properties {
result[property.name] = property_display_value(property.value)
}
result
})
}
///|
fn property_table_names(
index : ModelIndex,
products : Array[Product],
requested : Array[String],
) -> Array[String] raise ParseError {
if requested.length() > 0 {
requested.copy()
} else {
products.fold(init=Array::new(), (result, product) => {
for name in property_names_for_product(index, product) {
if !result.contains(name) {
result.push(name)
}
}
result
})
}
}
///|
fn property_column_for(
name : String,
rows : Array[PropertyRow],
) -> PropertyColumn {
let mut populated = 0
let mut numeric = 0
let distinct : Map[String, Bool] = Map([])
let mut value_kind = "missing"
for row in rows {
match row.values.get(name) {
Some(value) if value.length() > 0 => {
populated += 1
distinct[value] = true
if property_is_numeric(value) {
numeric += 1
value_kind = "number"
} else {
value_kind = "string"
}
}
_ => ()
}
}
{
name,
value_type: value_kind,
populated_count: populated,
numeric_count: numeric,
distinct_count: distinct.length(),
}
}
///|
fn property_table_row(
index : ModelIndex,
product : Product,
names : Array[String],
) -> PropertyRow raise ParseError {
let values = property_map_for_product(index, product)
let missing = names.fold(init=0, (result, name) => {
if values.get(name) is Some(_) {
result
} else {
result + 1
}
})
{
product_id: product.id,
ifc_type: product.ifc_type,
name: product.name,
values,
missing_count: missing,
}
}
///|
/// Flatten selected IFC properties into a deterministic table.
pub fn property_table(
model : Model,
names? : Array[String] = [],
) -> PropertyTable raise ParseError {
property_table_for_products(model, model.products(), names~)
}
///|
pub fn property_table_for_products(
model : Model,
products : Array[Product],
names? : Array[String] = [],
) -> PropertyTable raise ParseError {
let index = model.index()
let columns_names = property_table_names(index, products, names)
let rows = products.fold(init=Array::new(), (result, product) => {
result.push(property_table_row(index, product, columns_names))
result
})
{
columns: columns_names.map(name => property_column_for(name, rows)),
rows,
product_count: products.length(),
missing_value_count: rows.fold(init=0, (result, row) => {
result + row.missing_count
}),
}
}
///|
pub fn property_table_for_kind(
model : Model,
kind : ProductKind,
names? : Array[String] = [],
) -> PropertyTable raise ParseError {
property_table_for_products(
model,
model.products().filter(product => product.kind == kind),
names~,
)
}
///|
pub fn PropertyColumn::coverage(
self : PropertyColumn,
row_count : Int,
) -> Double {
if row_count <= 0 {
0.0
} else {
self.populated_count.to_double() / row_count.to_double()
}
}
///|
pub fn PropertyColumn::is_numeric(self : PropertyColumn) -> Bool {
self.numeric_count > 0 && self.numeric_count == self.populated_count
}
///|
pub fn PropertyColumn::headline(self : PropertyColumn) -> String {
self.name +
" type=" +
self.value_type +
" populated=" +
self.populated_count.to_string() +
" distinct=" +
self.distinct_count.to_string()
}
///|
pub fn PropertyRow::value(self : PropertyRow, name : String) -> String? {
self.values.get(name)
}
///|
pub fn PropertyRow::has_value(self : PropertyRow, name : String) -> Bool {
match self.values.get(name) {
Some(value) => value.length() > 0
None => false
}
}
///|
pub fn PropertyRow::headline(self : PropertyRow) -> String {
"#" +
self.product_id.to_string() +
" " +
self.ifc_type +
" missing=" +
self.missing_count.to_string()
}
///|
pub fn PropertyTable::column(
self : PropertyTable,
name : String,
) -> PropertyColumn? {
for column in self.columns {
if column.name.to_lower() == name.to_lower() {
return Some(column)
}
}
None
}
///|
pub fn PropertyTable::row(
self : PropertyTable,
product_id : Int,
) -> PropertyRow? {
for row in self.rows {
if row.product_id == product_id {
return Some(row)
}
}
None
}
///|
pub fn PropertyTable::rows_with_value(
self : PropertyTable,
name : String,
value : String,
) -> Array[PropertyRow] {
let wanted = value.to_lower()
self.rows.filter(row => {
row
.value(name)
.map(text => text.to_lower().contains(wanted))
.unwrap_or(false)
})
}
///|
pub fn PropertyTable::numeric_rows(
self : PropertyTable,
name : String,
comparison : NumberComparison,
expected : Double,
upper? : Double? = None,
) -> Array[PropertyRow] {
self.rows.filter(row => {
match row.value(name) {
Some(text) =>
query_compare_number(
@string.parse_double(text),
comparison,
expected,
upper,
) catch {
_ => false
}
None => false
}
})
}
///|
pub fn PropertyTable::headline(self : PropertyTable) -> String {
"rows=" +
self.rows.length().to_string() +
" columns=" +
self.columns.length().to_string() +
" missing_values=" +
self.missing_value_count.to_string()
}
///|
pub fn PropertyTable::coverage(self : PropertyTable, name : String) -> Double {
self
.column(name)
.map(column => column.coverage(self.rows.length()))
.unwrap_or(0.0)
}
///|
pub fn PropertyTable::complete_rows(self : PropertyTable) -> Array[PropertyRow] {
self.rows.filter(row => row.missing_count == 0)
}
///|
pub fn PropertyTable::missing_rows(self : PropertyTable) -> Array[PropertyRow] {
self.rows.filter(row => row.missing_count > 0)
}
///|
fn property_table_csv_escape(text : String) -> String {
if text.contains(",") || text.contains("\"") || text.contains("\n") {
"\"" + text.replace(old="\"", new="\"\"") + "\""
} else {
text
}
}
///|
pub fn PropertyTable::to_csv(self : PropertyTable) -> String {
let builder = StringBuilder()
builder <+ "product_id,type,name"
for column in self.columns {
builder <+ ",\{property_table_csv_escape(column.name)}"
}
builder <+ "\n"
for row in self.rows {
let row_name = property_table_csv_escape(row.name.unwrap_or(""))
builder <+
"\{row.product_id},\{property_table_csv_escape(row.ifc_type)},\{row_name}"
for column in self.columns {
let cell = property_table_csv_escape(row.value(column.name).unwrap_or(""))
builder <+ ",\{cell}"
}
builder <+ "\n"
}
builder.to_string()
}
///|
pub fn PropertyTable::to_json(self : PropertyTable) -> String {
let rows = self.rows
.map(row => {
let values : Array[String] = []
for key, value in row.values {
values.push("\"" + key + "\":\"" + value + "\"")
}
"{\"product_id\":" +
row.product_id.to_string() +
",\"type\":\"" +
row.ifc_type +
"\",\"name\":\"" +
row.name.unwrap_or("") +
"\",\"values\":{" +
values.join(",") +
"}}"
})
.join(",")
"{\"product_count\":" +
self.product_count.to_string() +
",\"missing_value_count\":" +
self.missing_value_count.to_string() +
",\"columns\":[" +
self.columns.map(column => "\"" + column.name + "\"").join(",") +
"],\"rows\":[" +
rows +
"]}"
}
///|
pub fn PropertyTable::to_markdown(self : PropertyTable) -> String {
let builder = StringBuilder()
builder <+ "# Property table\n\n"
builder <+ "- Rows: \{self.rows.length()}\n"
builder <+ "- Columns: \{self.columns.length()}\n"
builder <+ "- Missing values: \{self.missing_value_count}\n\n"
for column in self.columns {
builder <+ "- \{column.headline()}\n"
}
builder.to_string()
}
///|
pub fn PropertyTable::column_names(self : PropertyTable) -> Array[String] {
self.columns.map(column => column.name)
}
///|
pub fn PropertyTable::product_ids(self : PropertyTable) -> Array[Int] {
self.rows.map(row => row.product_id)
}
///|
pub fn property_values(
model : Model,
name : String,
) -> Map[String, Int] raise ParseError {
let table = property_table(model, names=[name])
table.rows.fold(init=Map([]), (result, row) => {
match row.value(name) {
Some(value) if value.length() > 0 => {
result.update_or_default(value, 1, count => count + 1)
result
}
_ => result
}
})
}
///|
pub fn property_coverage_report(
model : Model,
names : Array[String],
) -> Array[String] raise ParseError {
let table = property_table(model, names~)
table.columns.map(column => {
column.name + "=" + column.coverage(table.rows.length()).to_string()
})
}