///|
pub(all) struct RepairFeatureUsage {
feature : String
uses : Int
first_rank : Int
total_cost : Int
values : Array[Value]
common_to_all : Bool
single_target_value : Bool
} derive(Debug, Eq)
///|
pub(all) struct RepairPortfolio {
policy_name : String
baseline : Truth
target : Truth
repair_count : Int
cheapest_cost : Int
most_expensive_cost : Int
total_changes : Int
average_changes : Int
consensus_features : Array[String]
optional_features : Array[String]
usages : Array[RepairFeatureUsage]
} derive(Debug, Eq)
///|
pub fn Plan::portfolio(self : Plan) -> RepairPortfolio {
let usages : Array[RepairFeatureUsage] = []
let mut cheapest_cost = -1
let mut most_expensive_cost = -1
let mut total_changes = 0
for repair_index, repair in self.repairs {
if cheapest_cost < 0 || repair.cost < cheapest_cost {
cheapest_cost = repair.cost
}
if repair.cost > most_expensive_cost {
most_expensive_cost = repair.cost
}
total_changes = total_changes + repair.changes.length()
for change in repair.changes {
record_feature_usage(usages, change, repair_index)
}
}
let consensus_features : Array[String] = []
let optional_features : Array[String] = []
for index, item in usages {
let common_to_all = item.uses == self.repairs.length() &&
self.repairs.length() > 0
let single_target_value = item.values.length() <= 1
usages[index] = {
feature: item.feature,
uses: item.uses,
first_rank: item.first_rank,
total_cost: item.total_cost,
values: item.values,
common_to_all,
single_target_value,
}
if common_to_all {
consensus_features.push(item.feature)
} else {
optional_features.push(item.feature)
}
}
usages.sort_by(compare_feature_usage)
{
policy_name: self.policy_name,
baseline: self.baseline,
target: self.target,
repair_count: self.repairs.length(),
cheapest_cost,
most_expensive_cost,
total_changes,
average_changes: if self.repairs.is_empty() {
0
} else {
total_changes / self.repairs.length()
},
consensus_features,
optional_features,
usages,
}
}
///|
fn record_feature_usage(
usages : Array[RepairFeatureUsage],
change : Change,
repair_index : Int,
) -> Unit {
let existing = find_usage_index(usages, change.feature)
match existing {
None =>
usages.push({
feature: change.feature,
uses: 1,
first_rank: repair_index + 1,
total_cost: change.cost,
values: [change.to],
common_to_all: false,
single_target_value: true,
})
Some(index) => {
let item = usages[index]
let values = item.values.copy()
if !values.contains(change.to) {
values.push(change.to)
}
usages[index] = {
feature: item.feature,
uses: item.uses + 1,
first_rank: item.first_rank,
total_cost: item.total_cost + change.cost,
values,
common_to_all: item.common_to_all,
single_target_value: item.single_target_value,
}
}
}
}
///|
fn find_usage_index(
usages : Array[RepairFeatureUsage],
feature : String,
) -> Int? {
for index, item in usages {
if item.feature == feature {
return Some(index)
}
}
None
}
///|
fn compare_feature_usage(
left : RepairFeatureUsage,
right : RepairFeatureUsage,
) -> Int {
let by_uses = right.uses.compare(left.uses)
if by_uses != 0 {
return by_uses
}
let by_cost = right.total_cost.compare(left.total_cost)
if by_cost != 0 {
return by_cost
}
left.feature.compare(right.feature)
}
///|
pub fn RepairPortfolio::has_consensus(self : RepairPortfolio) -> Bool {
!self.consensus_features.is_empty()
}
///|
pub fn RepairPortfolio::feature_usage(
self : RepairPortfolio,
feature : String,
) -> RepairFeatureUsage? {
for item in self.usages {
if item.feature == feature {
return Some(item)
}
}
None
}
///|
pub fn RepairFeatureUsage::values_text(self : RepairFeatureUsage) -> String {
if self.values.is_empty() {
return "none"
}
self.values.map(value => value.render()).join(", ")
}
///|
pub fn RepairFeatureUsage::average_cost(self : RepairFeatureUsage) -> Int {
if self.uses == 0 {
0
} else {
self.total_cost / self.uses
}
}
///|
pub fn RepairPortfolio::to_markdown(self : RepairPortfolio) -> String {
let out = StringBuilder::new()
out.write_string("# Repair portfolio: \{self.policy_name}\n\n")
out.write_string("- Current outcome: **\{self.baseline.render()}**\n")
out.write_string("- Target outcome: **\{self.target.render()}**\n")
out.write_string("- Repairs: \{self.repair_count}\n")
out.write_string("- Cheapest cost: \{portfolio_cost(self.cheapest_cost)}\n")
out.write_string(
"- Most expensive cost: \{portfolio_cost(self.most_expensive_cost)}\n",
)
out.write_string("- Total changes: \{self.total_changes}\n")
out.write_string("- Average changes: \{self.average_changes}\n")
out.write_string(
"- Consensus features: \{portfolio_names(self.consensus_features)}\n",
)
out.write_string(
"- Optional features: \{portfolio_names(self.optional_features)}\n\n",
)
if self.usages.is_empty() {
out.write_string("No repair changes were available.\n")
return out.to_string()
}
out.write_string(
"| feature | uses | first rank | total cost | average cost | values | common | single value |\n",
)
out.write_string("| --- | ---: | ---: | ---: | ---: | --- | --- | --- |\n")
for item in self.usages {
out.write_string(
"| \{portfolio_cell(item.feature)} | \{item.uses} | \{item.first_rank} | \{item.total_cost} | \{item.average_cost()} | \{portfolio_cell(item.values_text())} | \{item.common_to_all} | \{item.single_target_value} |\n",
)
}
out.to_string()
}
///|
pub fn RepairPortfolio::to_json(self : RepairPortfolio) -> String {
let out = StringBuilder::new()
out.write_string("{")
out.write_string("\"policy\":\"\{portfolio_json_escape(self.policy_name)}\",")
out.write_string("\"baseline\":\"\{self.baseline.render()}\",")
out.write_string("\"target\":\"\{self.target.render()}\",")
out.write_string("\"repair_count\":\{self.repair_count},")
out.write_string("\"cheapest_cost\":\{self.cheapest_cost},")
out.write_string("\"most_expensive_cost\":\{self.most_expensive_cost},")
out.write_string("\"total_changes\":\{self.total_changes},")
out.write_string("\"average_changes\":\{self.average_changes},")
out.write_string("\"consensus_features\":")
append_string_array(out, self.consensus_features)
out.write_string(",\"optional_features\":")
append_string_array(out, self.optional_features)
out.write_string(",\"usages\":[")
for index, item in self.usages {
if index > 0 {
out.write_char(',')
}
out.write_string("{")
out.write_string("\"feature\":\"\{portfolio_json_escape(item.feature)}\",")
out.write_string("\"uses\":\{item.uses},")
out.write_string("\"first_rank\":\{item.first_rank},")
out.write_string("\"total_cost\":\{item.total_cost},")
out.write_string("\"average_cost\":\{item.average_cost()},")
out.write_string("\"common_to_all\":\{item.common_to_all},")
out.write_string("\"single_target_value\":\{item.single_target_value},")
out.write_string("\"values\":[")
for value_index, value in item.values {
if value_index > 0 {
out.write_char(',')
}
out.write_string("\"\{portfolio_json_escape(value.plain())}\"")
}
out.write_string("]}")
}
out.write_string("]}")
out.to_string()
}
///|
fn append_string_array(out : StringBuilder, values : Array[String]) -> Unit {
out.write_char('[')
for index, value in values {
if index > 0 {
out.write_char(',')
}
out.write_string("\"\{portfolio_json_escape(value)}\"")
}
out.write_char(']')
}
///|
fn portfolio_cost(value : Int) -> String {
if value < 0 {
"none"
} else {
value.to_string()
}
}
///|
fn portfolio_names(values : Array[String]) -> String {
if values.is_empty() {
"none"
} else {
values.join(", ")
}
}
///|
fn portfolio_cell(value : String) -> String {
value.replace_all(old="|", new="\\|").replace_all(old="\n", new=" ")
}
///|
fn portfolio_json_escape(text : String) -> String {
let out = StringBuilder::new()
for ch in text {
match ch {
'"' => out.write_string("\\\"")
'\\' => out.write_string("\\\\")
'\n' => out.write_string("\\n")
'\r' => out.write_string("\\r")
'\t' => out.write_string("\\t")
_ => out.write_char(ch)
}
}
out.to_string()
}