Skip to main content

use_collatz/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Collatz trajectory utilities for `RustUse`.
5
6#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
7/// The parity of a positive integer in a Collatz trajectory.
8pub enum CollatzParity {
9    /// The value is divisible by `2`.
10    Even,
11    /// The value is not divisible by `2`.
12    Odd,
13}
14
15#[derive(Clone, Debug, Eq, PartialEq)]
16/// Summary information for an inclusive range checked with [`verify_range`].
17///
18/// `max_total_stopping_time` stores `(input, total_stopping_time)`.
19///
20/// `max_trajectory_value` stores `(input, max_value_in_trajectory)`.
21pub struct CollatzRangeSummary {
22    /// The inclusive lower bound that was requested.
23    pub start: u64,
24    /// The inclusive upper bound that was requested.
25    pub end: u64,
26    /// The number of positive inputs checked in the inclusive range.
27    pub checked: u64,
28    /// The number of checked inputs whose trajectories reached `1` without overflow.
29    pub reached_one: u64,
30    /// The number of checked inputs whose odd-step arithmetic overflowed.
31    pub overflowed: u64,
32    /// The input with the largest total stopping time, if any values were checked.
33    pub max_total_stopping_time: Option<(u64, u64)>,
34    /// The input with the largest peak trajectory value, if any values were checked.
35    pub max_trajectory_value: Option<(u64, u64)>,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39struct TrajectoryMetrics {
40    stopping_time: u64,
41    total_stopping_time: u64,
42    max_value: u64,
43}
44
45const fn is_even(n: u64) -> bool {
46    (n & 1) == 0
47}
48
49const fn checked_odd_step(n: u64) -> Option<u64> {
50    match n.checked_mul(3) {
51        Some(value) => value.checked_add(1),
52        None => None,
53    }
54}
55
56fn trajectory_metrics(n: u64) -> Option<TrajectoryMetrics> {
57    if n == 0 {
58        return None;
59    }
60
61    let start = n;
62    let mut current = n;
63    let mut stopping_time = if n == 1 { Some(0) } else { None };
64    let mut total_stopping_time = 0_u64;
65    let mut max_value = n;
66
67    while current != 1 {
68        current = collatz_next(current)?;
69        total_stopping_time = total_stopping_time.checked_add(1)?;
70        max_value = max_value.max(current);
71
72        if stopping_time.is_none() && current < start {
73            stopping_time = Some(total_stopping_time);
74        }
75    }
76
77    Some(TrajectoryMetrics {
78        stopping_time: stopping_time.unwrap_or(total_stopping_time),
79        total_stopping_time,
80        max_value,
81    })
82}
83
84fn update_max_pair(slot: &mut Option<(u64, u64)>, input: u64, value: u64) {
85    if slot.is_none_or(|(_, current_max)| value > current_max) {
86        *slot = Some((input, value));
87    }
88}
89
90/// Returns the next value in the Collatz trajectory for a positive integer.
91///
92/// Returns `None` when `n == 0` or when the checked odd step `3 * n + 1`
93/// overflows `u64`.
94///
95/// # Examples
96///
97/// ```rust
98/// use use_collatz::collatz_next;
99///
100/// assert_eq!(collatz_next(1), Some(4));
101/// assert_eq!(collatz_next(2), Some(1));
102/// assert_eq!(collatz_next(3), Some(10));
103/// assert_eq!(collatz_next(0), None);
104/// ```
105#[must_use]
106pub const fn collatz_next(n: u64) -> Option<u64> {
107    if n == 0 {
108        return None;
109    }
110
111    if is_even(n) {
112        Some(n / 2)
113    } else {
114        checked_odd_step(n)
115    }
116}
117
118/// Returns the full Collatz trajectory from `n` down to `1`.
119///
120/// Returns `None` when `n == 0` or when a checked odd step overflows.
121///
122/// # Examples
123///
124/// ```rust
125/// use use_collatz::collatz_sequence;
126///
127/// assert_eq!(collatz_sequence(6), Some(vec![6, 3, 10, 5, 16, 8, 4, 2, 1]));
128/// assert_eq!(collatz_sequence(0), None);
129/// ```
130#[must_use]
131pub fn collatz_sequence(n: u64) -> Option<Vec<u64>> {
132    if n == 0 {
133        return None;
134    }
135
136    let mut current = n;
137    let mut sequence = vec![current];
138
139    while current != 1 {
140        current = collatz_next(current)?;
141        sequence.push(current);
142    }
143
144    Some(sequence)
145}
146
147/// Returns the number of steps needed to first reach a value smaller than `n`.
148///
149/// Returns `Some(0)` for `n == 1`.
150///
151/// Returns `None` when `n == 0` or when a checked odd step overflows.
152///
153/// # Examples
154///
155/// ```rust
156/// use use_collatz::stopping_time;
157///
158/// assert_eq!(stopping_time(1), Some(0));
159/// assert_eq!(stopping_time(6), Some(1));
160/// assert_eq!(stopping_time(3), Some(6));
161/// ```
162#[must_use]
163pub fn stopping_time(n: u64) -> Option<u64> {
164    trajectory_metrics(n).map(|metrics| metrics.stopping_time)
165}
166
167/// Returns the number of steps needed to reach `1`.
168///
169/// Returns `None` when `n == 0` or when a checked odd step overflows.
170///
171/// # Examples
172///
173/// ```rust
174/// use use_collatz::total_stopping_time;
175///
176/// assert_eq!(total_stopping_time(1), Some(0));
177/// assert_eq!(total_stopping_time(6), Some(8));
178/// ```
179#[must_use]
180pub fn total_stopping_time(n: u64) -> Option<u64> {
181    trajectory_metrics(n).map(|metrics| metrics.total_stopping_time)
182}
183
184/// Returns the full trajectory length including the starting value and terminal `1`.
185///
186/// Returns `None` when `n == 0` or when a checked odd step overflows.
187///
188/// # Examples
189///
190/// ```rust
191/// use use_collatz::trajectory_len;
192///
193/// assert_eq!(trajectory_len(1), Some(1));
194/// assert_eq!(trajectory_len(6), Some(9));
195/// ```
196#[must_use]
197pub fn trajectory_len(n: u64) -> Option<u64> {
198    total_stopping_time(n)?.checked_add(1)
199}
200
201/// Returns the largest value reached in the trajectory from `n` to `1`.
202///
203/// Returns `None` when `n == 0` or when a checked odd step overflows.
204///
205/// # Examples
206///
207/// ```rust
208/// use use_collatz::max_value_in_trajectory;
209///
210/// assert_eq!(max_value_in_trajectory(1), Some(1));
211/// assert_eq!(max_value_in_trajectory(6), Some(16));
212/// ```
213#[must_use]
214pub fn max_value_in_trajectory(n: u64) -> Option<u64> {
215    trajectory_metrics(n).map(|metrics| metrics.max_value)
216}
217
218/// Returns `true` when the trajectory reaches `1` without overflow.
219///
220/// Returns `false` for `n == 0` or when a checked odd step overflows.
221///
222/// # Examples
223///
224/// ```rust
225/// use use_collatz::reaches_one;
226///
227/// assert!(reaches_one(6));
228/// assert!(!reaches_one(0));
229/// ```
230#[must_use]
231pub fn reaches_one(n: u64) -> bool {
232    trajectory_metrics(n).is_some()
233}
234
235/// Returns the parity of a positive input.
236///
237/// Returns `None` when `n == 0`.
238///
239/// # Examples
240///
241/// ```rust
242/// use use_collatz::{CollatzParity, parity};
243///
244/// assert_eq!(parity(8), Some(CollatzParity::Even));
245/// assert_eq!(parity(9), Some(CollatzParity::Odd));
246/// assert_eq!(parity(0), None);
247/// ```
248#[must_use]
249pub const fn parity(n: u64) -> Option<CollatzParity> {
250    if n == 0 {
251        return None;
252    }
253
254    if is_even(n) {
255        Some(CollatzParity::Even)
256    } else {
257        Some(CollatzParity::Odd)
258    }
259}
260
261/// Returns the parity pattern for the trajectory from `n` to `1`.
262///
263/// The returned vector includes the starting value and every nonterminal value
264/// in the trajectory, but it excludes the final `1`. For `n == 1`, this means
265/// the parity vector is empty.
266///
267/// Returns `None` when `n == 0` or when a checked odd step overflows.
268///
269/// # Examples
270///
271/// ```rust
272/// use use_collatz::{CollatzParity, parity_vector};
273///
274/// assert_eq!(
275///     parity_vector(6),
276///     Some(vec![
277///         CollatzParity::Even,
278///         CollatzParity::Odd,
279///         CollatzParity::Even,
280///         CollatzParity::Odd,
281///         CollatzParity::Even,
282///         CollatzParity::Even,
283///         CollatzParity::Even,
284///         CollatzParity::Even,
285///     ])
286/// );
287/// assert_eq!(parity_vector(1), Some(vec![]));
288/// ```
289#[must_use]
290pub fn parity_vector(n: u64) -> Option<Vec<CollatzParity>> {
291    if n == 0 {
292        return None;
293    }
294
295    let mut current = n;
296    let mut parities = Vec::new();
297
298    while current != 1 {
299        parities.push(parity(current)?);
300        current = collatz_next(current)?;
301    }
302
303    Some(parities)
304}
305
306/// Verifies the inclusive range `[start, end]` with bounded Collatz exploration.
307///
308/// The function skips `0`, counts the number of positive inputs checked, counts
309/// how many trajectories reach `1`, counts how many overflow during checked odd
310/// steps, tracks the input with the largest total stopping time, and tracks the
311/// input with the largest peak trajectory value.
312///
313/// When `start > end`, the returned summary is empty and no values are checked.
314///
315/// # Examples
316///
317/// ```rust
318/// use use_collatz::verify_range;
319///
320/// let summary = verify_range(1, 10);
321///
322/// assert_eq!(summary.checked, 10);
323/// assert_eq!(summary.reached_one, 10);
324/// assert_eq!(summary.overflowed, 0);
325/// assert_eq!(summary.max_total_stopping_time, Some((9, 19)));
326/// assert_eq!(summary.max_trajectory_value, Some((7, 52)));
327/// ```
328#[must_use]
329pub fn verify_range(start: u64, end: u64) -> CollatzRangeSummary {
330    let mut summary = CollatzRangeSummary {
331        start,
332        end,
333        checked: 0,
334        reached_one: 0,
335        overflowed: 0,
336        max_total_stopping_time: None,
337        max_trajectory_value: None,
338    };
339
340    if start > end {
341        return summary;
342    }
343
344    for input in start..=end {
345        if input == 0 {
346            continue;
347        }
348
349        summary.checked += 1;
350
351        if let Some(metrics) = trajectory_metrics(input) {
352            summary.reached_one += 1;
353            update_max_pair(
354                &mut summary.max_total_stopping_time,
355                input,
356                metrics.total_stopping_time,
357            );
358            update_max_pair(&mut summary.max_trajectory_value, input, metrics.max_value);
359        } else {
360            summary.overflowed += 1;
361        }
362    }
363
364    summary
365}
366
367#[cfg(test)]
368mod tests {
369    use super::{
370        CollatzParity, CollatzRangeSummary, collatz_next, collatz_sequence,
371        max_value_in_trajectory, parity, parity_vector, reaches_one, stopping_time,
372        total_stopping_time, trajectory_len, verify_range,
373    };
374
375    #[test]
376    fn computes_next_values() {
377        assert_eq!(collatz_next(1), Some(4));
378        assert_eq!(collatz_next(2), Some(1));
379        assert_eq!(collatz_next(3), Some(10));
380        assert_eq!(collatz_next(0), None);
381    }
382
383    #[test]
384    fn detects_checked_overflow_for_odd_steps() {
385        assert_eq!(collatz_next(u64::MAX), None);
386    }
387
388    #[test]
389    fn builds_full_sequences() {
390        assert_eq!(collatz_sequence(6), Some(vec![6, 3, 10, 5, 16, 8, 4, 2, 1]));
391        assert_eq!(collatz_sequence(0), None);
392    }
393
394    #[test]
395    fn computes_total_stopping_times() {
396        assert_eq!(total_stopping_time(1), Some(0));
397        assert_eq!(total_stopping_time(6), Some(8));
398    }
399
400    #[test]
401    fn computes_trajectory_lengths() {
402        assert_eq!(trajectory_len(1), Some(1));
403        assert_eq!(trajectory_len(6), Some(9));
404    }
405
406    #[test]
407    fn computes_trajectory_maxima() {
408        assert_eq!(max_value_in_trajectory(6), Some(16));
409        assert_eq!(max_value_in_trajectory(1), Some(1));
410    }
411
412    #[test]
413    fn computes_stopping_times() {
414        assert_eq!(stopping_time(1), Some(0));
415        assert_eq!(stopping_time(6), Some(1));
416        assert_eq!(stopping_time(3), Some(6));
417    }
418
419    #[test]
420    fn reports_reachability_without_panicking() {
421        assert!(reaches_one(6));
422        assert!(!reaches_one(0));
423    }
424
425    #[test]
426    fn classifies_parity_helpers() {
427        assert_eq!(parity(8), Some(CollatzParity::Even));
428        assert_eq!(parity(9), Some(CollatzParity::Odd));
429        assert_eq!(parity(0), None);
430    }
431
432    #[test]
433    fn builds_parity_vectors_without_terminal_one() {
434        assert_eq!(
435            parity_vector(6),
436            Some(vec![
437                CollatzParity::Even,
438                CollatzParity::Odd,
439                CollatzParity::Even,
440                CollatzParity::Odd,
441                CollatzParity::Even,
442                CollatzParity::Even,
443                CollatzParity::Even,
444                CollatzParity::Even,
445            ])
446        );
447        assert_eq!(parity_vector(1), Some(vec![]));
448        assert_eq!(parity_vector(0), None);
449    }
450
451    #[test]
452    fn verifies_bounded_ranges() {
453        assert_eq!(
454            verify_range(1, 10),
455            CollatzRangeSummary {
456                start: 1,
457                end: 10,
458                checked: 10,
459                reached_one: 10,
460                overflowed: 0,
461                max_total_stopping_time: Some((9, 19)),
462                max_trajectory_value: Some((7, 52)),
463            }
464        );
465    }
466
467    #[test]
468    fn skips_zero_and_allows_empty_ranges() {
469        assert_eq!(
470            verify_range(0, 0),
471            CollatzRangeSummary {
472                start: 0,
473                end: 0,
474                checked: 0,
475                reached_one: 0,
476                overflowed: 0,
477                max_total_stopping_time: None,
478                max_trajectory_value: None,
479            }
480        );
481
482        assert_eq!(
483            verify_range(10, 1),
484            CollatzRangeSummary {
485                start: 10,
486                end: 1,
487                checked: 0,
488                reached_one: 0,
489                overflowed: 0,
490                max_total_stopping_time: None,
491                max_trajectory_value: None,
492            }
493        );
494    }
495}