///|
fn value_stats_visit(
value : Value,
depth : Int,
output : ValueStats,
) -> ValueStats {
let next_depth = depth + 1
let maximum_depth = if next_depth > output.maximum_depth {
next_depth
} else {
output.maximum_depth
}
match value {
Null => { ..output, node_count: output.node_count + 1, maximum_depth }
Omitted =>
{
..output,
node_count: output.node_count + 1,
maximum_depth,
omitted_count: output.omitted_count + 1,
}
Integer(_) =>
{
..output,
node_count: output.node_count + 1,
maximum_depth,
number_count: output.number_count + 1,
}
Real(_) =>
{
..output,
node_count: output.node_count + 1,
maximum_depth,
number_count: output.number_count + 1,
}
String(_) =>
{
..output,
node_count: output.node_count + 1,
maximum_depth,
string_count: output.string_count + 1,
}
Enumeration(_) =>
{ ..output, node_count: output.node_count + 1, maximum_depth }
Binary(_) => { ..output, node_count: output.node_count + 1, maximum_depth }
Reference(_) =>
{
..output,
node_count: output.node_count + 1,
maximum_depth,
reference_count: output.reference_count + 1,
}
Typed(name~, value~) => {
ignore(name)
value_stats_visit(value, depth, {
..output,
node_count: output.node_count + 1,
maximum_depth,
})
}
List(values) => {
let result = {
..output,
node_count: output.node_count + 1,
maximum_depth,
list_count: output.list_count + 1,
}
values.fold(init=result, (current, item) => {
value_stats_visit(item, next_depth, current)
})
}
}
}
///|
pub fn value_stats(value : Value) -> ValueStats {
value_stats_visit(value, 0, {
node_count: 0,
maximum_depth: 0,
reference_count: 0,
string_count: 0,
number_count: 0,
list_count: 0,
omitted_count: 0,
})
}
///|
pub fn model_value_stats(model : Model) -> ValueStats {
model.entities.fold(
init={
node_count: 0,
maximum_depth: 0,
reference_count: 0,
string_count: 0,
number_count: 0,
list_count: 0,
omitted_count: 0,
},
(result, entity) => {
entity.arguments.fold(init=result, (current, value) => {
let item = value_stats(value)
{
node_count: current.node_count + item.node_count,
maximum_depth: if current.maximum_depth > item.maximum_depth {
current.maximum_depth
} else {
item.maximum_depth
},
reference_count: current.reference_count + item.reference_count,
string_count: current.string_count + item.string_count,
number_count: current.number_count + item.number_count,
list_count: current.list_count + item.list_count,
omitted_count: current.omitted_count + item.omitted_count,
}
})
},
)
}
///|
pub fn ValueStats::summary(self : ValueStats) -> String {
"nodes=" +
self.node_count.to_string() +
", depth=" +
self.maximum_depth.to_string() +
", refs=" +
self.reference_count.to_string() +
", strings=" +
self.string_count.to_string() +
", numbers=" +
self.number_count.to_string() +
", lists=" +
self.list_count.to_string() +
", omitted=" +
self.omitted_count.to_string()
}
///|
pub fn ValueStats::is_reasonable(
self : ValueStats,
maximum_depth : Int,
) -> Bool {
self.maximum_depth <= maximum_depth && self.node_count >= 0
}
///|
pub fn value_references(value : Value) -> Array[Int] {
match value {
Reference(id) => [id]
Typed(name~, value~) => {
ignore(name)
value_references(value)
}
List(values) =>
values.fold(init=Array::new(), (result, item) => {
result.append(value_references(item))
result
})
_ => []
}
}
///|
pub fn value_contains_reference(value : Value, target : Int) -> Bool {
value_references(value).contains(target)
}
///|
pub fn value_maximum_depth(value : Value) -> Int {
value_stats(value).maximum_depth
}
///|
fn source_parentheses(source : String) -> (Int, Bool, Bool) {
let mut depth = 0
let mut balanced = true
let mut inside_string = false
for character in source {
if character == '\'' {
inside_string = !inside_string
} else if !inside_string {
if character == '(' {
depth += 1
} else if character == ')' {
depth -= 1
if depth < 0 {
balanced = false
depth = 0
}
}
}
}
(depth, balanced, !inside_string)
}
///|
pub fn source_health(source : String) -> SourceHealth {
let stats = stream_statistics(source)
let (depth, balanced, strings_balanced) = source_parentheses(source)
{
byte_length: source.length(),
line_count: stats.line_count,
record_count: stats.record_count,
maximum_record_length: stats.max_record_length,
reference_count: stats.reference_count,
balanced_parentheses: balanced && depth == 0,
balanced_strings: strings_balanced,
has_iso_header: source.to_upper().has_prefix("ISO-10303-21"),
has_end_marker: source.to_upper().contains("END-ISO-10303-21"),
}
}
///|
pub fn SourceHealth::is_well_formed(self : SourceHealth) -> Bool {
self.balanced_parentheses &&
self.balanced_strings &&
self.has_iso_header &&
self.has_end_marker
}
///|
pub fn SourceHealth::summary(self : SourceHealth) -> String {
"chars=" +
self.byte_length.to_string() +
", lines=" +
self.line_count.to_string() +
", records=" +
self.record_count.to_string() +
", max_record=" +
self.maximum_record_length.to_string() +
", refs=" +
self.reference_count.to_string() +
", balanced=" +
self.balanced_parentheses.to_string() +
", header=" +
self.has_iso_header.to_string() +
", end=" +
self.has_end_marker.to_string()
}
///|
pub fn source_window(source : String, start : Int, end : Int) -> SourceWindow {
let length = source.length()
let safe_start = if start < 0 {
0
} else if start > length {
length
} else {
start
}
let safe_end = if end < safe_start {
safe_start
} else if end > length {
length
} else {
end
}
let line = source[:safe_start].split("\n").length()
{
start: safe_start,
end: safe_end,
text: source[safe_start:safe_end].to_owned(),
line,
}
}
///|
pub fn source_line(source : String, line : Int) -> SourceWindow? {
if line < 1 {
return None
}
let lines = source.split("\n").to_array()
match lines.get(line - 1) {
Some(text) => {
let start = lines[:line - 1].fold(init=0, (offset, item) => {
offset + item.length() + 1
})
Some({ start, end: start + text.length(), text: text.to_owned(), line })
}
None => None
}
}
///|
pub fn entity_range(model : Model) -> EntityRange {
if model.entities.length() == 0 {
return { first_id: 0, last_id: 0, entity_count: 0, contiguous: true }
}
let mut first = model.entities[0].id
let mut last = first
let ids : Map[Int, Bool] = Map([])
for entity in model.entities {
ids[entity.id] = true
if entity.id < first {
first = entity.id
}
if entity.id > last {
last = entity.id
}
}
let mut contiguous = true
let end_id = last + 1
for id in first.. String {
"#" +
self.first_id.to_string() +
"..#" +
self.last_id.to_string() +
" (" +
self.entity_count.to_string() +
", contiguous=" +
self.contiguous.to_string() +
")"
}
///|
pub fn entity_ids_in_range(
model : Model,
first : Int,
last : Int,
) -> Array[Int] {
model.entities
.filter(entity => entity.id >= first && entity.id <= last)
.map(entity => entity.id)
}
///|
pub fn entity_names_case_insensitive(
model : Model,
name : String,
) -> Array[String] {
let wanted = name.to_upper()
model.entities
.filter(entity => entity.name.to_upper() == wanted)
.map(entity => entity.name)
}
///|
pub fn entity_shape_signature(entity : Entity) -> String {
entity.name.to_upper() +
"(" +
entity.arguments.length().to_string() +
") refs=" +
entity_references(entity).length().to_string()
}
///|
pub fn model_shape_signatures(model : Model) -> Array[String] {
model.entities.map(entity_shape_signature)
}
///|
pub fn source_entity_names(source : String) -> Array[String] {
scan_step_records(source).map(record => record.name)
}
///|
pub fn source_record_at(source : String, index : Int) -> StepRecord? {
scan_step_records(source).get(index)
}
///|
pub fn source_range(source : String) -> EntityRange {
let records = scan_step_records(source)
if records.length() == 0 {
return { first_id: 0, last_id: 0, entity_count: 0, contiguous: true }
}
let mut first = records[0].id
let mut last = first
let ids : Map[Int, Bool] = Map([])
for record in records {
ids[record.id] = true
if record.id < first {
first = record.id
}
if record.id > last {
last = record.id
}
}
let mut contiguous = true
let end_id = last + 1
for id in first.. Array[String] {
let health = source_health(source)
let result : Array[String] = []
if !health.has_iso_header {
result.push("missing ISO-10303-21 header")
}
if !health.has_end_marker {
result.push("missing END-ISO-10303-21 marker")
}
if !health.balanced_parentheses {
result.push("unbalanced parentheses outside strings")
}
if !health.balanced_strings {
result.push("unterminated STEP string")
}
if health.record_count == 0 {
result.push("no physical entity records found")
}
result
}
///|
pub fn source_is_safe_for_full_parse(
source : String,
maximum_chars : Int,
maximum_records : Int,
) -> Bool {
let health = source_health(source)
(maximum_chars <= 0 || health.byte_length <= maximum_chars) &&
(maximum_records <= 0 || health.record_count <= maximum_records) &&
health.is_well_formed()
}
///|
pub fn normalize_step_newlines(source : String) -> String {
source.replace(old="\r\n", new="\n").replace(old="\r", new="\n")
}
///|
pub fn source_line_count(source : String) -> Int {
normalize_step_newlines(source).split("\n").length()
}
///|
pub fn source_reference_density(source : String) -> Double {
let health = source_health(source)
if health.byte_length == 0 {
0.0
} else {
health.reference_count.to_double() / health.byte_length.to_double()
}
}
///|
pub fn record_length_histogram(source : String) -> Map[Int, Int] {
let result : Map[Int, Int] = Map([])
for record in scan_step_records(source) {
let bucket = record.length() / 10 * 10
result.update_or_default(bucket, 1, count => count + 1)
}
result
}
///|
pub fn records_named_safe(source : String, name : String) -> Array[StepRecord] {
let wanted = name.to_upper()
scan_step_records(source).filter(record => record.name.to_upper() == wanted)
}
///|
pub fn source_health_score(source : String) -> Int {
let health = source_health(source)
let mut score = 0
if health.has_iso_header {
score += 20
}
if health.has_end_marker {
score += 20
}
if health.balanced_parentheses {
score += 20
}
if health.balanced_strings {
score += 20
}
if health.record_count > 0 {
score += 20
}
score
}
///|
pub fn source_health_grade(source : String) -> String {
let score = source_health_score(source)
if score >= 100 {
"A"
} else if score >= 80 {
"B"
} else if score >= 60 {
"C"
} else {
"D"
}
}
///|
pub fn model_reference_targets(model : Model) -> Array[Int] {
let result : Array[Int] = []
for entity in model.entities {
result.append(entity_references(entity))
}
result
}
///|
pub fn model_orphan_reference_ids(model : Model) -> Array[Int] {
let index = model.index()
let result : Array[Int] = []
for target in model_reference_targets(model) {
if !index.has(target) && !result.contains(target) {
result.push(target)
}
}
result
}
///|
pub fn source_window_for_record(
source : String,
record : StepRecord,
) -> SourceWindow {
source_window(source, record.start_offset, record.end_offset)
}
///|
pub fn source_records_cover_document(source : String) -> Bool {
let records = scan_step_records(source)
for record in records {
if record.start_offset < 0 ||
record.end_offset > source.length() ||
record.start_offset >= record.end_offset {
return false
}
}
true
}