///|
/// String helpers shared by the parser, the audit and the report renderer.
///|
/// Parse a run of ASCII digits. Empty input, signs, spaces and separators are
/// rejected, so `parse_digits("+7")` and `parse_digits(" 7")` both return
/// `None`.
fn parse_digits(text : StringView) -> Int? {
if text.length() == 0 {
return None
}
let mut total = 0
for index = 0; index < text.length(); index = index + 1 {
let code = text[index].to_int()
if code < 48 || code > 57 {
return None
}
total = total * 10 + (code - 48)
}
Some(total)
}
///|
/// Left pad a non-negative number with `0` up to `width` characters.
fn pad_left(value : Int, width : Int) -> String {
let mut text = value.to_string()
while text.length() < width {
text = "0" + text
}
text
}
///|
/// Join strings with a separator. An empty array yields an empty string.
fn join(values : Array[String], separator : String) -> String {
let builder = StringBuilder()
for index = 0; index < values.length(); index = index + 1 {
if index > 0 {
builder.write_string(separator)
}
builder.write_string(values[index])
}
builder.to_string()
}
///|
/// Window and dependency identifiers accept ASCII letters, digits, `_`, `.` and
/// `-`. They must be non empty and at most 64 characters long, which keeps
/// reports readable and rejects pasted prose or stray quoting.
fn is_valid_identifier(value : String) -> Bool {
if value.length() == 0 || value.length() > 64 {
return false
}
for character in value.iter() {
let code = character.to_int()
let is_digit = code >= 48 && code <= 57
let is_upper = code >= 65 && code <= 90
let is_lower = code >= 97 && code <= 122
let is_punctuation = code == 95 || code == 45 || code == 46
if !(is_digit || is_upper || is_lower || is_punctuation) {
return false
}
}
true
}
///|
/// Split `key=value` at the first `=` and trim both sides.
fn split_field(text : StringView) -> (String, String)? {
for index = 0; index < text.length(); index = index + 1 {
if text[index] == '=' {
let key = text[0:index].to_owned().trim().to_owned()
let value = text[index + 1:text.length()].to_owned().trim().to_owned()
return Some((key, value))
}
}
None
}
///|
/// Compare two strings by code point: `-1`, `0` or `1`.
///
/// Ordering is defined here instead of relying on a trait so that every sorted
/// report is reproducible on all four backends.
fn compare_strings(left : String, right : String) -> Int {
let left_chars = left.iter().to_array()
let right_chars = right.iter().to_array()
let mut index = 0
while index < left_chars.length() && index < right_chars.length() {
let left_code = left_chars[index].to_int()
let right_code = right_chars[index].to_int()
if left_code < right_code {
return -1
}
if left_code > right_code {
return 1
}
index = index + 1
}
if left_chars.length() < right_chars.length() {
return -1
}
if left_chars.length() > right_chars.length() {
return 1
}
0
}
///|
/// Copy strings ordered by code point, keeping input order for equals.
fn sorted_strings(values : Array[String]) -> Array[String] {
let ordered : Array[String] = []
for value in values {
ordered.push(value)
}
let mut index = 1
while index < ordered.length() {
let current = ordered[index]
let mut position = index
while position > 0 && compare_strings(current, ordered[position - 1]) < 0 {
ordered[position] = ordered[position - 1]
position = position - 1
}
ordered[position] = current
index = index + 1
}
ordered
}
///|
/// Sorted copy of an array without repeated values.
fn distinct_strings(values : Array[String]) -> Array[String] {
let ordered = sorted_strings(values)
let unique : Array[String] = []
for value in ordered {
if unique.length() == 0 || unique[unique.length() - 1] != value {
unique.push(value)
}
}
unique
}
///|
/// ` ` for one, ` ` otherwise, so generated sentences
/// never read `1 changes`.
fn count_text(value : Int, singular : String, plural : String) -> String {
let noun = if value == 1 { singular } else { plural }
value.to_string() + " " + noun
}