Skip to main content

ferray_strings/
align.rs

1// ferray-strings: Alignment and padding operations (REQ-6)
2//
3// Implements center, ljust, rjust, zfill — elementwise on StringArray.
4
5use ferray_core::dimension::Dimension;
6use ferray_core::error::FerrayResult;
7
8use crate::string_array::StringArray;
9
10/// Center each string in a field of the given width, padded with `fillchar`.
11///
12/// If the string is already longer than `width`, it is returned unchanged.
13///
14/// # Errors
15/// Returns an error if the internal array construction fails.
16pub fn center<D: Dimension>(
17    a: &StringArray<D>,
18    width: usize,
19    fillchar: char,
20) -> FerrayResult<StringArray<D>> {
21    a.map(|s| {
22        let char_count = s.chars().count();
23        if char_count >= width {
24            return s.to_string();
25        }
26        let total_pad = width - char_count;
27        let left_pad = total_pad / 2;
28        let right_pad = total_pad - left_pad;
29        let mut result = String::with_capacity(s.len() + total_pad);
30        for _ in 0..left_pad {
31            result.push(fillchar);
32        }
33        result.push_str(s);
34        for _ in 0..right_pad {
35            result.push(fillchar);
36        }
37        result
38    })
39}
40
41/// Left-justify each string in a field of the given width, padded with spaces.
42///
43/// If the string is already longer than `width`, it is returned unchanged.
44///
45/// # Errors
46/// Returns an error if the internal array construction fails.
47pub fn ljust<D: Dimension>(a: &StringArray<D>, width: usize) -> FerrayResult<StringArray<D>> {
48    ljust_with(a, width, ' ')
49}
50
51/// Left-justify each string in a field of the given width, padded with `fillchar`.
52///
53/// If the string is already longer than `width`, it is returned unchanged.
54///
55/// # Errors
56/// Returns an error if the internal array construction fails.
57pub fn ljust_with<D: Dimension>(
58    a: &StringArray<D>,
59    width: usize,
60    fillchar: char,
61) -> FerrayResult<StringArray<D>> {
62    a.map(|s| {
63        let char_count = s.chars().count();
64        if char_count >= width {
65            return s.to_string();
66        }
67        let pad = width - char_count;
68        let mut result = String::with_capacity(s.len() + pad * fillchar.len_utf8());
69        result.push_str(s);
70        for _ in 0..pad {
71            result.push(fillchar);
72        }
73        result
74    })
75}
76
77/// Right-justify each string in a field of the given width, padded with spaces.
78///
79/// If the string is already longer than `width`, it is returned unchanged.
80///
81/// # Errors
82/// Returns an error if the internal array construction fails.
83pub fn rjust<D: Dimension>(a: &StringArray<D>, width: usize) -> FerrayResult<StringArray<D>> {
84    rjust_with(a, width, ' ')
85}
86
87/// Right-justify each string in a field of the given width, padded with `fillchar`.
88///
89/// If the string is already longer than `width`, it is returned unchanged.
90///
91/// # Errors
92/// Returns an error if the internal array construction fails.
93pub fn rjust_with<D: Dimension>(
94    a: &StringArray<D>,
95    width: usize,
96    fillchar: char,
97) -> FerrayResult<StringArray<D>> {
98    a.map(|s| {
99        let char_count = s.chars().count();
100        if char_count >= width {
101            return s.to_string();
102        }
103        let pad = width - char_count;
104        let mut result = String::with_capacity(s.len() + pad * fillchar.len_utf8());
105        for _ in 0..pad {
106            result.push(fillchar);
107        }
108        result.push_str(s);
109        result
110    })
111}
112
113/// Pad each string on the left with zeros to fill the given width.
114///
115/// If the string starts with a sign (`+` or `-`), the sign is placed
116/// before the zeros. If the string is already longer than `width`,
117/// it is returned unchanged.
118///
119/// # Errors
120/// Returns an error if the internal array construction fails.
121pub fn zfill<D: Dimension>(a: &StringArray<D>, width: usize) -> FerrayResult<StringArray<D>> {
122    a.map(|s| {
123        let char_count = s.chars().count();
124        if char_count >= width {
125            return s.to_string();
126        }
127        let pad = width - char_count;
128        let (sign, rest) = if s.starts_with('+') || s.starts_with('-') {
129            (&s[..1], &s[1..])
130        } else {
131            ("", s)
132        };
133        let mut result = String::with_capacity(s.len() + pad);
134        result.push_str(sign);
135        for _ in 0..pad {
136            result.push('0');
137        }
138        result.push_str(rest);
139        result
140    })
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::string_array::array;
147
148    #[test]
149    fn test_center() {
150        let a = array(&["hi", "x"]).unwrap();
151        let b = center(&a, 6, '*').unwrap();
152        assert_eq!(b.as_slice(), &["**hi**", "**x***"]);
153    }
154
155    #[test]
156    fn test_center_no_pad_needed() {
157        let a = array(&["hello"]).unwrap();
158        let b = center(&a, 3, ' ').unwrap();
159        assert_eq!(b.as_slice(), &["hello"]);
160    }
161
162    #[test]
163    fn test_ljust() {
164        let a = array(&["hi", "hello"]).unwrap();
165        let b = ljust(&a, 6).unwrap();
166        assert_eq!(b.as_slice(), &["hi    ", "hello "]);
167    }
168
169    #[test]
170    fn test_ljust_no_pad_needed() {
171        let a = array(&["hello"]).unwrap();
172        let b = ljust(&a, 3).unwrap();
173        assert_eq!(b.as_slice(), &["hello"]);
174    }
175
176    #[test]
177    fn test_rjust() {
178        let a = array(&["hi", "hello"]).unwrap();
179        let b = rjust(&a, 6).unwrap();
180        assert_eq!(b.as_slice(), &["    hi", " hello"]);
181    }
182
183    #[test]
184    fn test_ljust_with_fillchar() {
185        let a = array(&["hi"]).unwrap();
186        let b = ljust_with(&a, 6, '-').unwrap();
187        assert_eq!(b.as_slice(), &["hi----"]);
188    }
189
190    #[test]
191    fn test_rjust_with_fillchar() {
192        let a = array(&["hi"]).unwrap();
193        let b = rjust_with(&a, 6, '.').unwrap();
194        assert_eq!(b.as_slice(), &["....hi"]);
195    }
196
197    #[test]
198    fn test_ljust_with_unicode_fillchar() {
199        let a = array(&["ab"]).unwrap();
200        let b = ljust_with(&a, 5, '★').unwrap();
201        assert_eq!(b.as_slice(), &["ab★★★"]);
202    }
203
204    #[test]
205    fn test_zfill() {
206        let a = array(&["42", "-17", "+5", "abc"]).unwrap();
207        let b = zfill(&a, 5).unwrap();
208        assert_eq!(b.as_slice(), &["00042", "-0017", "+0005", "00abc"]);
209    }
210
211    #[test]
212    fn test_zfill_no_pad_needed() {
213        let a = array(&["12345"]).unwrap();
214        let b = zfill(&a, 3).unwrap();
215        assert_eq!(b.as_slice(), &["12345"]);
216    }
217}