use std::process::Command;
use std::process::Stdio;
pub fn is_quote_balanced(s: &str, quote_char: char) -> bool {
let mut closed = true;
let mut escaped = false;
s.chars().for_each(|c| {
if c == '\\' {
escaped = true;
} else if !escaped && c == quote_char {
closed = !closed;
} else {
escaped = false;
}
});
closed
}
pub fn lines_with_offset(s: &str) -> impl Iterator<Item = (&str, usize, usize)> {
let mut offset = 0;
std::iter::from_fn(move || {
if offset >= s.len() {
return None;
}
let start = offset;
loop {
match s[offset..].find(|c| ['\r', '\n'].contains(&c)) {
Some(i) => {
let end = offset + i;
offset = end + 1;
if s.as_bytes().get(end) == Some(&b'\r') {
if s.as_bytes().get(end + 1) != Some(&b'\n') {
continue;
}
offset += 1;
}
return Some((&s[start..end], start, offset));
}
None => {
offset = s.len();
return Some((&s[start..], start, offset));
}
}
}
})
}
pub fn program_exists(exec: &str) -> bool {
let finder = if cfg!(windows) { "where.exe" } else { "which" };
Command::new(finder)
.arg(exec)
.stdout(Stdio::null())
.stdin(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|r| r.success())
}
pub fn serialize_oxford_comma<T: std::fmt::Display>(items: &[T]) -> Option<String> {
let len = items.len();
match len {
0 => None,
1 => Some(items.iter().next().unwrap().to_string()),
2 => {
let mut items = items.iter();
Some(format!(
"{a} and {b}",
a = items.next().unwrap(),
b = items.next().unwrap()
))
}
_ => {
let mut result = String::new();
for item in items.iter().take(len - 1) {
if !result.is_empty() {
result.push_str(", ")
}
result.push_str(&item.to_string());
}
result.push_str(", and ");
result.push_str(&items[len - 1].to_string());
Some(result)
}
}
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn test_lines_with_offset() {
let s = "This string\nhas many\n\nnewlines, including Windows\r\n\r\nand even a \r that \
should not be a newline\n";
let lines = lines_with_offset(s).collect::<Vec<_>>();
assert_eq!(
lines,
&[
("This string", 0, 12),
("has many", 12, 21),
("", 21, 22),
("newlines, including Windows", 22, 51),
("", 51, 53),
("and even a \r that should not be a newline", 53, 95),
]
);
}
#[test]
fn test_program_exists() {
if cfg!(windows) {
assert!(program_exists("where.exe"));
} else {
assert!(program_exists("which"));
}
}
#[test]
fn test_is_properly_quoted() {
let s = "\"this string is quoted properly.\"";
assert!(is_quote_balanced(s, '"'));
let s = "\"this string has an escaped \\\" quote.\"";
assert!(is_quote_balanced(s, '"'));
let s = "\"this string is missing an end quote";
assert_eq!(is_quote_balanced(s, '"'), false);
let s = "this string is missing an open quote\"";
assert_eq!(is_quote_balanced(s, '"'), false);
let s = "\"this string has an irrelevant escape \\ \"";
assert!(is_quote_balanced(s, '"'));
let s = "'this string has single quotes'";
assert!(is_quote_balanced(s, '\''));
let s = "this string has unclosed single quotes'";
assert_eq!(is_quote_balanced(s, '\''), false);
}
#[test]
fn test_itemize_oxford_comma() {
assert_eq!(serialize_oxford_comma(&Vec::<String>::default()), None);
assert_eq!(
serialize_oxford_comma(&["hello"]),
Some(String::from("hello"))
);
assert_eq!(
serialize_oxford_comma(&["hello", "world"]),
Some(String::from("hello and world"))
);
assert_eq!(
serialize_oxford_comma(&["hello", "there", "world"]),
Some(String::from("hello, there, and world"))
);
}
}