fn parse_digits(bytes: &[u8]) -> (usize, usize) {
let mut i = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
let s = str::from_utf8(&bytes[..i]).expect("ASCII digits are valid UTF-8");
(s.parse::<usize>().unwrap_or(0), i)
}
#[derive(Debug, Clone, Copy)]
enum Align {
Left,
Right,
}
#[derive(Debug, Clone, Copy)]
pub(super) struct Spec {
align: Align,
min: usize,
max: usize,
}
impl Default for Spec {
fn default() -> Self {
Self {
align: Align::Right,
min: 0,
max: 0,
}
}
}
impl Spec {
pub(super) fn parse(s: &str) -> (Self, usize) {
let mut spec = Self::default();
let bytes = s.as_bytes();
let mut i = 0;
if i < bytes.len() && bytes[i] == b'-' {
spec.align = Align::Left;
i += 1;
}
let (min, j) = parse_digits(&bytes[i..]);
let mut max = 0;
i += j;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
let (mx, j) = parse_digits(&bytes[i..]);
i += j;
max = mx;
}
spec.max = max;
spec.min = min;
(spec, i)
}
pub(super) fn apply(&self, s: &str) -> String {
let mut s = s.to_string();
if self.max > 0 {
s = s.chars().take(self.max).collect();
}
let len = s.chars().count();
if self.min > 0 && len < self.min {
let addition = " ".repeat(self.min - len);
match self.align {
Align::Left => s = format!("{s}{addition}"),
Align::Right => s = format!("{addition}{s}"),
}
}
s
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_empty_spec_consumes_nothing() {
let (spec, consumed) = Spec::parse("");
assert_eq!(consumed, 0);
assert_eq!(spec.min, 0);
assert_eq!(spec.max, 0);
assert!(matches!(spec.align, Align::Right));
}
#[test]
fn parse_min_only() {
let (spec, consumed) = Spec::parse("7");
assert_eq!(consumed, 1);
assert_eq!(spec.min, 7);
assert_eq!(spec.max, 0);
}
#[test]
fn parse_left_align_min_and_max() {
let (spec, consumed) = Spec::parse("-7.4");
assert_eq!(consumed, 4);
assert!(matches!(spec.align, Align::Left));
assert_eq!(spec.min, 7);
assert_eq!(spec.max, 4);
}
#[test]
fn parse_stops_at_first_non_spec_char() {
let (spec, consumed) = Spec::parse("-7.4] more");
assert_eq!(consumed, 4); assert!(matches!(spec.align, Align::Left));
}
#[test]
fn parse_min_not_clamped_when_below_max() {
let (spec, consumed) = Spec::parse("3.8");
assert_eq!(consumed, 3);
assert_eq!(spec.min, 3);
assert_eq!(spec.max, 8);
}
#[test]
fn apply_pads_right_aligned_by_default() {
let (spec, _) = Spec::parse("5");
assert_eq!(spec.apply("ab"), " ab");
}
#[test]
fn apply_pads_left_aligned() {
let (spec, _) = Spec::parse("-5");
assert_eq!(spec.apply("ab"), "ab ");
}
#[test]
fn apply_truncates_to_max() {
let (spec, _) = Spec::parse("0.3");
assert_eq!(spec.apply("WARNING"), "WAR");
}
#[test]
fn apply_truncates_then_pads_using_truncated_len() {
let (spec, _) = Spec::parse("8.3");
assert_eq!(spec.apply("WARNING"), " WAR");
assert_eq!(spec.apply("WARNING").chars().count(), 8);
}
#[test]
fn apply_counts_characters_not_bytes() {
let (spec, _) = Spec::parse("-6");
assert_eq!(spec.apply("café"), "café ");
assert_eq!(spec.apply("café").chars().count(), 6);
}
#[test]
fn apply_noop_when_already_long_enough() {
let (spec, _) = Spec::parse("3");
assert_eq!(spec.apply("hello"), "hello");
}
}