1#![expect(clippy::similar_names, reason = "stop and step are Python's names")]
21
22use crate::error::{Error, Result};
23use crate::int::Int;
24use crate::object::Object;
25
26#[derive(Debug, Clone)]
28pub struct Slice {
29 pub start: Object,
31 pub stop: Object,
33 pub step: Object,
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub struct Indices {
43 pub start: isize,
45 pub stop: isize,
47 pub step: isize,
49 pub len: usize,
51}
52
53impl Indices {
54 pub fn offsets(self) -> impl Iterator<Item = usize> {
56 (0..self.len).map(move |i| {
59 let step = self.step.saturating_mul(i.cast_signed());
60 self.start.saturating_add(step).cast_unsigned()
61 })
62 }
63
64 #[must_use]
67 pub const fn is_contiguous(self) -> bool {
68 self.step == 1
69 }
70}
71
72impl Slice {
73 #[must_use]
75 pub const fn new(start: Object, stop: Object, step: Object) -> Self {
76 Slice { start, stop, step }
77 }
78
79 #[must_use]
82 pub fn repr(&self) -> String {
83 format!(
84 "slice({}, {}, {})",
85 self.start.repr(),
86 self.stop.repr(),
87 self.step.repr()
88 )
89 }
90
91 #[must_use]
93 pub const fn parts(&self) -> [&Object; 3] {
94 [&self.start, &self.stop, &self.step]
95 }
96
97 pub fn indices(&self, len: usize) -> Result<Indices> {
103 let step = bound(&self.step)?.unwrap_or(1);
104 if step == 0 {
105 return Err(Error::value_error("slice step cannot be zero"));
106 }
107 let len = len.cast_signed();
110 let backwards = step < 0;
111
112 let start = match bound(&self.start)? {
113 Some(start) => clamp(start, len, backwards),
114 None if backwards => len - 1,
115 None => 0,
116 };
117 let stop = match bound(&self.stop)? {
118 Some(stop) => clamp(stop, len, backwards),
119 None if backwards => -1,
120 None => len,
121 };
122
123 let span = if backwards {
126 start - stop
127 } else {
128 stop - start
129 };
130 let count = if span > 0 {
131 ((span - 1) / step.abs()) + 1
132 } else {
133 0
134 };
135
136 Ok(Indices {
137 start,
138 stop,
139 step,
140 len: count.cast_unsigned(),
141 })
142 }
143}
144
145const fn clamp(value: isize, len: isize, backwards: bool) -> isize {
153 if value < 0 {
154 let shifted = value.saturating_add(len);
155 return if shifted < 0 {
158 if backwards { -1 } else { 0 }
159 } else {
160 shifted
161 };
162 }
163 if value >= len {
164 return if backwards { len - 1 } else { len };
166 }
167 value
168}
169
170fn bound(value: &Object) -> Result<Option<isize>> {
176 match value {
177 Object::None => Ok(None),
178 Object::Bool(value) => Ok(Some(isize::from(*value))),
179 Object::Int(Int::Small(value)) => {
180 Ok(Some(isize::try_from(*value).unwrap_or(if *value < 0 {
181 isize::MIN
182 } else {
183 isize::MAX
184 })))
185 }
186 Object::Int(big @ Int::Big(_)) => Ok(Some(if big.is_negative() {
187 isize::MIN
188 } else {
189 isize::MAX
190 })),
191 _ => Err(Error::type_error(
192 "slice indices must be integers or None or have an __index__ method",
193 )),
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200
201 fn slice(start: Option<i64>, stop: Option<i64>, step: Option<i64>) -> Slice {
203 let part = |value: Option<i64>| value.map_or(Object::None, Object::int);
204 Slice::new(part(start), part(stop), part(step))
205 }
206
207 fn walk(start: Option<i64>, stop: Option<i64>, step: Option<i64>, len: usize) -> Vec<usize> {
210 slice(start, stop, step)
211 .indices(len)
212 .expect("these are all integers")
213 .offsets()
214 .collect()
215 }
216
217 #[test]
218 fn a_slice_with_nothing_written_down_is_the_whole_sequence() {
219 assert_eq!(walk(None, None, None, 3), [0, 1, 2]);
220 assert_eq!(walk(None, None, None, 0), []);
221 }
222
223 #[test]
224 fn a_bound_past_the_end_is_pulled_back_to_the_end() {
225 assert_eq!(walk(Some(5), Some(100), None, 3), []);
226 assert_eq!(walk(Some(1), Some(100), None, 3), [1, 2]);
227 assert_eq!(walk(Some(-100), None, None, 3), [0, 1, 2]);
228 }
229
230 #[test]
231 fn a_bound_too_big_for_the_machine_is_pulled_back_as_well() {
232 let huge = Object::Int(Int::Small(1).shl(&Int::Small(100)).expect("2 ** 100"));
234 let far = Slice::new(huge.clone(), Object::None, Object::None);
235 assert_eq!(far.indices(3).expect("clamped").len, 0);
236 let near = Slice::new(Object::None, huge, Object::None);
237 assert_eq!(near.indices(3).expect("clamped").len, 3);
238 }
239
240 #[test]
241 fn a_negative_step_walks_backwards_and_stops_before_the_front() {
242 assert_eq!(walk(None, None, Some(-1), 3), [2, 1, 0]);
243 assert_eq!(walk(Some(100), None, Some(-1), 3), [2, 1, 0]);
244 assert_eq!(walk(None, Some(0), Some(-1), 3), [2, 1]);
245 }
246
247 #[test]
248 fn a_step_skips_and_the_count_rounds_up() {
249 assert_eq!(walk(None, None, Some(2), 5), [0, 2, 4]);
250 assert_eq!(walk(None, None, Some(3), 5), [0, 3]);
251 assert_eq!(walk(None, None, Some(-2), 5), [4, 2, 0]);
252 }
253
254 #[test]
255 fn a_slice_that_runs_the_wrong_way_is_empty_rather_than_negative() {
256 assert_eq!(walk(Some(3), Some(1), None, 5), []);
257 assert_eq!(walk(Some(1), Some(3), Some(-1), 5), []);
258 }
259
260 #[test]
261 fn only_a_plain_run_is_contiguous() {
262 let contiguous = |step| {
263 slice(None, None, step)
264 .indices(5)
265 .expect("integers")
266 .is_contiguous()
267 };
268 assert!(contiguous(None));
269 assert!(contiguous(Some(1)));
270 assert!(!contiguous(Some(2)));
271 assert!(!contiguous(Some(-1)));
272 }
273
274 #[test]
275 fn a_step_of_zero_and_a_bound_that_is_not_a_number_are_refused() {
276 let zero = slice(None, None, Some(0))
277 .indices(3)
278 .expect_err("zero step");
279 assert_eq!(zero.to_string(), "ValueError: slice step cannot be zero");
280 let text = Slice::new(Object::str("a"), Object::None, Object::None);
281 assert_eq!(
282 text.indices(3).expect_err("not a number").to_string(),
283 "TypeError: slice indices must be integers or None or have an __index__ method"
284 );
285 }
286
287 #[test]
288 fn a_bool_is_a_bound_because_it_is_an_int() {
289 let from_true = Slice::new(Object::Bool(true), Object::None, Object::None);
290 assert_eq!(from_true.indices(3).expect("a bool is an int").start, 1);
291 }
292
293 #[test]
294 fn repr_names_all_three_parts_even_when_none_were_written() {
295 assert_eq!(slice(None, None, None).repr(), "slice(None, None, None)");
296 assert_eq!(slice(Some(1), Some(2), Some(3)).repr(), "slice(1, 2, 3)");
297 }
298}