///|
/// Comma separated export, for spreadsheets and for tools that only speak CSV.
///
/// The dialect is the usual one: fields are separated by commas, a field that
/// contains a comma, a double quote or a line break is wrapped in double quotes,
/// and an inner double quote is doubled. Every document ends with exactly one
/// newline.
///|
/// Quote a field when it needs it.
///
/// A field needs quoting when it holds a comma, a double quote, a newline or a
/// carriage return; inside the quotes every double quote is doubled.
pub fn csv_escape(text : String) -> String {
let mut needs_quotes = false
for character in text {
match character {
',' | '"' | '\n' | '\r' => needs_quotes = true
_ => ()
}
}
if !needs_quotes {
return text
}
let builder = StringBuilder()
builder.write_char('"')
for character in text {
if character == '"' {
builder.write_string("\"\"")
} else {
builder.write_char(character)
}
}
builder.write_char('"')
builder.to_string()
}
///|
/// One CSV line from already escaped fields.
fn csv_line(fields : Array[String]) -> String {
let builder = StringBuilder()
for index = 0; index < fields.length(); index = index + 1 {
if index > 0 {
builder.write_char(',')
}
builder.write_string(fields[index])
}
builder.write_char('\n')
builder.to_string()
}
///|
/// Every record of the plan as CSV, ordered by start minute.
///
/// Columns are `id,service,kind,start,end,minutes,risk,depends`; a dependency
/// list is joined with `|`, because a comma inside a cell would be ambiguous
/// even when quoted.
pub fn render_windows_csv(plan : Plan) -> String {
let builder = StringBuilder()
builder.write_string("id,service,kind,start,end,minutes,risk,depends\n")
for window in sort_windows_by_start(plan.windows) {
builder.write_string(
csv_line([
csv_escape(window.id),
csv_escape(window.service),
csv_escape(window.kind.label()),
csv_escape(format_instant(window.start)),
csv_escape(format_instant(window.end)),
csv_escape(window.duration().to_string()),
csv_escape(window.risk.label()),
csv_escape(join(window.depends, "|")),
]),
)
}
builder.to_string()
}
///|
/// The overlaps of an audit as CSV, with the four columns
/// `first,second,minutes,impact`.
pub fn render_overlaps_csv(report : AuditReport) -> String {
let builder = StringBuilder()
builder.write_string("first,second,minutes,impact\n")
for overlap in report.overlaps {
builder.write_string(
csv_line([
csv_escape(overlap.first),
csv_escape(overlap.second),
csv_escape(overlap.minutes.to_string()),
csv_escape(overlap.impact.label()),
]),
)
}
builder.to_string()
}
///|
/// Safe slots as CSV, with the columns `start,end,minutes`.
pub fn render_slots_csv(slots : Array[Slot]) -> String {
let builder = StringBuilder()
builder.write_string("start,end,minutes\n")
for slot in slots {
builder.write_string(
csv_line([
csv_escape(format_instant(slot.start)),
csv_escape(format_instant(slot.end)),
csv_escape(slot.duration().to_string()),
]),
)
}
builder.to_string()
}
///|
/// Freeze conflicts as CSV, with the columns `window,freeze,minutes`.
pub fn render_freeze_conflicts_csv(report : AuditReport) -> String {
let builder = StringBuilder()
builder.write_string("window,freeze,minutes\n")
for conflict in report.freeze_conflicts {
builder.write_string(
csv_line([
csv_escape(conflict.window_id),
csv_escape(conflict.freeze_id),
csv_escape(conflict.minutes.to_string()),
]),
)
}
builder.to_string()
}
///|
/// Dependency issues as CSV, with the columns
/// `window,dependency,problem,severity,detail`.
pub fn render_dependency_issues_csv(report : AuditReport) -> String {
let builder = StringBuilder()
builder.write_string("window,dependency,problem,severity,detail\n")
for issue in report.dependency_issues {
builder.write_string(
csv_line([
csv_escape(issue.window_id),
csv_escape(issue.dependency),
csv_escape(issue.problem.label()),
csv_escape(issue.severity.label()),
csv_escape(issue.detail),
]),
)
}
builder.to_string()
}