1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct Span {
14 pub start: u32,
15 pub end: u32,
16}
17
18impl Span {
19 #[must_use]
20 pub const fn new(start: u32, end: u32) -> Self {
21 Self { start, end }
22 }
23
24 #[must_use]
25 pub const fn len(self) -> u32 {
26 self.end - self.start
27 }
28
29 #[must_use]
30 pub const fn is_empty(self) -> bool {
31 self.start == self.end
32 }
33
34 #[must_use]
43 pub fn slice(self, source: &str) -> &str {
44 let start = self.start as usize;
45 let end = self.end as usize;
46 source
47 .get(start..end)
48 .expect("span must align to UTF-8 char boundaries in source")
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn new_records_endpoints() {
58 let s = Span::new(3, 7);
59 assert_eq!(s.start, 3);
60 assert_eq!(s.end, 7);
61 }
62
63 #[test]
64 fn len_is_end_minus_start() {
65 assert_eq!(Span::new(2, 5).len(), 3);
66 assert_eq!(Span::new(0, 0).len(), 0);
67 }
68
69 #[test]
70 fn empty_span_reports_empty() {
71 assert!(Span::new(4, 4).is_empty());
72 assert!(!Span::new(4, 5).is_empty());
73 }
74
75 #[test]
76 fn slice_extracts_exact_byte_range() {
77 let src = "hello, world";
78 assert_eq!(Span::new(7, 12).slice(src), "world");
79 assert_eq!(Span::new(0, 5).slice(src), "hello");
80 }
81
82 #[test]
83 fn slice_works_at_utf8_boundary() {
84 let src = "青空文庫";
85 assert_eq!(Span::new(3, 6).slice(src), "空");
87 }
88
89 #[test]
90 #[should_panic(expected = "span must align to UTF-8 char boundaries")]
91 fn slice_panics_on_misaligned_boundary() {
92 let src = "青空"; let _slice: &str = Span::new(1, 4).slice(src);
96 }
97
98 #[test]
99 fn span_is_8_bytes_on_64_bit_target() {
100 use core::mem::size_of;
103 assert_eq!(size_of::<Span>(), 8);
104 }
105}