///|
pub fn explanation_label(item : Explanation) -> String {
item.label + "=" + item.points.to_string()
}
///|
pub fn explanation_labels(items : Array[Explanation]) -> Array[String] {
items.map(explanation_label)
}
///|
pub fn render_explanation(item : Explanation) -> String {
item.label + ": " + item.points.to_string() + " (" + item.detail + ")"
}
///|
pub fn render_explanations(items : Array[Explanation]) -> String {
items.map(render_explanation).join("; ")
}
///|
pub fn report_severity_rank(report : ScoreReport) -> Int {
severity_rank(report.severity)
}
///|
pub fn report_header(report : ScoreReport) -> String {
report.instrument +
" score=" +
report.score.to_string() +
" severity=" +
severity_label(report.severity)
}
///|
pub fn render_report(report : ScoreReport) -> String {
report_header(report) +
"; interpretation=" +
report.interpretation +
"; explanations=[" +
render_explanations(report.explanations) +
"]"
}
///|
pub fn report_csv_header() -> String {
"instrument,score,severity,interpretation,explanations"
}
///|
pub fn report_csv_row(report : ScoreReport) -> String {
report.instrument +
"," +
report.score.to_string() +
"," +
severity_label(report.severity) +
"," +
report.interpretation +
"," +
explanation_labels(report.explanations).join("|")
}
///|
pub fn reports_csv(reports : Array[ScoreReport]) -> String {
let lines = [report_csv_header()]
for report in reports {
lines.push(report_csv_row(report))
}
lines.join("\n")
}
///|
pub fn report_is_more_concerning(
left : ScoreReport,
right : ScoreReport,
) -> Bool {
let left_rank = report_severity_rank(left)
let right_rank = report_severity_rank(right)
left_rank > right_rank ||
(left_rank == right_rank && left.score > right.score)
}
///|
pub fn highest_concern_report(reports : Array[ScoreReport]) -> ScoreReport? {
let mut highest : ScoreReport? = None
for report in reports {
match highest {
None => highest = Some(report)
Some(current) =>
if report_is_more_concerning(report, current) {
highest = Some(report)
}
}
}
highest
}
///|
pub fn report_interpretations(reports : Array[ScoreReport]) -> Array[String] {
reports.map(fn(report) { report.instrument + ": " + report.interpretation })
}
///|
pub fn report_explanation_count(reports : Array[ScoreReport]) -> Int {
let mut count = 0
for report in reports {
count += report.explanations.length()
}
count
}
///|
pub fn report_summary_line(reports : Array[ScoreReport]) -> String {
let summary = summarize_reports(reports)
"count=" +
summary.count.to_string() +
"; total=" +
summary.total_score.to_string() +
"; highest=" +
severity_label(summary.highest) +
"; explanations=" +
report_explanation_count(reports).to_string()
}
///|
pub fn report_find(
reports : Array[ScoreReport],
instrument : String,
) -> ScoreReport? {
for report in reports {
if report.instrument == instrument {
return Some(report)
}
}
None
}
///|
pub fn reports_with_severity(
reports : Array[ScoreReport],
severity : Severity,
) -> Array[ScoreReport] {
reports.filter(fn(report) { report.severity == severity })
}
///|
pub fn reports_at_or_above(
reports : Array[ScoreReport],
severity : Severity,
) -> Array[ScoreReport] {
reports.filter(fn(report) {
severity_rank(report.severity) >= severity_rank(severity)
})
}
///|
pub fn report_points_for_label(report : ScoreReport, label : String) -> Int? {
for item in report.explanations {
if item.label == label {
return Some(item.points)
}
}
None
}
///|
pub fn report_has_explanation(report : ScoreReport, label : String) -> Bool {
report_points_for_label(report, label) is Some(_)
}
///|
pub fn report_explanation_total(report : ScoreReport) -> Int {
let mut total = 0
for item in report.explanations {
total += item.points
}
total
}
///|
pub fn report_score_reconciles(report : ScoreReport) -> Bool {
report_score_reconciles_with_explanations(report, report.explanations)
}
///|
pub fn report_score_reconciles_with_explanations(
report : ScoreReport,
explanations : Array[Explanation],
) -> Bool {
let mut total = 0
for item in explanations {
total += item.points
}
total == report.score || report.explanations.length() == 0
}
///|
pub fn report_value_vector(reports : Array[ScoreReport]) -> Array[Int] {
reports.map(fn(report) { report.score })
}
///|
pub fn report_severity_vector(reports : Array[ScoreReport]) -> Array[String] {
reports.map(fn(report) { severity_label(report.severity) })
}
///|
pub fn reports_render_lines(reports : Array[ScoreReport]) -> Array[String] {
reports.map(render_report)
}
///|
pub fn report_catalog_line(report : ScoreReport) -> String {
report.instrument +
" | " +
severity_label(report.severity) +
" | " +
report.score.to_string() +
" | " +
report.explanations.length().to_string() +
" explanations"
}
///|
pub fn report_catalog(reports : Array[ScoreReport]) -> String {
reports.map(report_catalog_line).join("\n")
}
///|
pub fn report_disclaimer_set(reports : Array[ScoreReport]) -> Array[String] {
let disclaimers = []
for report in reports {
if !disclaimers.contains(report.disclaimer) {
disclaimers.push(report.disclaimer)
}
}
disclaimers
}
///|
pub fn report_bundle_is_coherent(reports : Array[ScoreReport]) -> Bool {
if reports.length() == 0 {
return true
}
let disclaimers = report_disclaimer_set(reports)
disclaimers.length() == 1 &&
reports.all(fn(report) { report.instrument != "" && report.score >= 0 })
}
///|
pub fn report_max_score(reports : Array[ScoreReport]) -> Int {
let mut maximum = 0
for report in reports {
if report.score > maximum {
maximum = report.score
}
}
maximum
}
///|
pub fn report_min_score(reports : Array[ScoreReport]) -> Int? {
if reports.length() == 0 {
None
} else {
let mut minimum = reports[0].score
for report in reports {
if report.score < minimum {
minimum = report.score
}
}
Some(minimum)
}
}
///|
pub fn report_range(reports : Array[ScoreReport]) -> Int {
match report_min_score(reports) {
None => 0
Some(minimum) => report_max_score(reports) - minimum
}
}
///|
pub fn report_instrument_names(reports : Array[ScoreReport]) -> Array[String] {
reports.map(fn(report) { report.instrument })
}
///|
pub fn report_is_empty(report : ScoreReport) -> Bool {
report.instrument == "" || report.explanations.length() == 0
}
///|
pub fn report_nonempty(reports : Array[ScoreReport]) -> Array[ScoreReport] {
reports.filter(fn(report) { !report_is_empty(report) })
}
///|
pub fn report_count_at_or_above(
reports : Array[ScoreReport],
severity : Severity,
) -> Int {
reports_at_or_above(reports, severity).length()
}
///|
pub fn report_score_sum(reports : Array[ScoreReport]) -> Int {
let mut total = 0
for report in reports {
total += report.score
}
total
}
///|
pub fn report_average_score_x100(reports : Array[ScoreReport]) -> Int {
if reports.length() == 0 {
0
} else {
report_score_sum(reports) * 100 / reports.length()
}
}
///|
pub fn report_has_severity(
reports : Array[ScoreReport],
severity : Severity,
) -> Bool {
reports.any(fn(report) { report.severity == severity })
}
///|
pub(all) struct ReportQuality {
total : Int
nonempty : Int
reconciled : Int
coherent : Bool
disclaimer_count : Int
} derive(Debug, Eq)
///|
pub fn report_quality(reports : Array[ScoreReport]) -> ReportQuality {
let valid = report_nonempty(reports)
let mut reconciled = 0
for report in valid {
if report_score_reconciles(report) {
reconciled += 1
}
}
{
total: reports.length(),
nonempty: valid.length(),
reconciled,
coherent: report_bundle_is_coherent(valid),
disclaimer_count: report_disclaimer_set(valid).length(),
}
}
///|
pub fn report_quality_ready(quality : ReportQuality) -> Bool {
quality.total > 0 &&
quality.total == quality.nonempty &&
quality.nonempty == quality.reconciled &&
quality.coherent &&
quality.disclaimer_count == 1
}
///|
pub fn render_report_quality(quality : ReportQuality) -> String {
"total=" +
quality.total.to_string() +
"; nonempty=" +
quality.nonempty.to_string() +
"; reconciled=" +
quality.reconciled.to_string() +
"; coherent=" +
quality.coherent.to_string() +
"; disclaimers=" +
quality.disclaimer_count.to_string()
}
///|
pub(all) struct ReportComparison {
left_instrument : String
right_instrument : String
left_score : Int
right_score : Int
left_more_concerning : Bool
score_delta : Int
} derive(Debug, Eq)
///|
pub fn compare_reports(
left : ScoreReport,
right : ScoreReport,
) -> ReportComparison {
{
left_instrument: left.instrument,
right_instrument: right.instrument,
left_score: left.score,
right_score: right.score,
left_more_concerning: report_is_more_concerning(left, right),
score_delta: left.score - right.score,
}
}
///|
pub fn render_report_comparison(comparison : ReportComparison) -> String {
comparison.left_instrument +
" vs " +
comparison.right_instrument +
": delta=" +
comparison.score_delta.to_string() +
"; left_more_concerning=" +
comparison.left_more_concerning.to_string()
}
///|
pub fn report_delta_label(delta : Int) -> String {
if delta > 0 {
"increased"
} else if delta < 0 {
"decreased"
} else {
"unchanged"
}
}
///|
pub fn report_comparison_label(comparison : ReportComparison) -> String {
comparison.left_instrument +
" " +
report_delta_label(comparison.score_delta) +
" vs " +
comparison.right_instrument
}
///|
pub fn report_average_delta_x100(
left : Array[ScoreReport],
right : Array[ScoreReport],
) -> Int {
if left.length() == 0 || right.length() == 0 {
0
} else {
report_score_sum(left) * 100 / left.length() -
report_score_sum(right) * 100 / right.length()
}
}