#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum CollatzParity {
Even,
Odd,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CollatzRangeSummary {
pub start: u64,
pub end: u64,
pub checked: u64,
pub reached_one: u64,
pub overflowed: u64,
pub max_total_stopping_time: Option<(u64, u64)>,
pub max_trajectory_value: Option<(u64, u64)>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct TrajectoryMetrics {
stopping_time: u64,
total_stopping_time: u64,
max_value: u64,
}
const fn is_even(n: u64) -> bool {
(n & 1) == 0
}
const fn checked_odd_step(n: u64) -> Option<u64> {
match n.checked_mul(3) {
Some(value) => value.checked_add(1),
None => None,
}
}
fn trajectory_metrics(n: u64) -> Option<TrajectoryMetrics> {
if n == 0 {
return None;
}
let start = n;
let mut current = n;
let mut stopping_time = if n == 1 { Some(0) } else { None };
let mut total_stopping_time = 0_u64;
let mut max_value = n;
while current != 1 {
current = collatz_next(current)?;
total_stopping_time = total_stopping_time.checked_add(1)?;
max_value = max_value.max(current);
if stopping_time.is_none() && current < start {
stopping_time = Some(total_stopping_time);
}
}
Some(TrajectoryMetrics {
stopping_time: stopping_time.unwrap_or(total_stopping_time),
total_stopping_time,
max_value,
})
}
fn update_max_pair(slot: &mut Option<(u64, u64)>, input: u64, value: u64) {
if slot.is_none_or(|(_, current_max)| value > current_max) {
*slot = Some((input, value));
}
}
#[must_use]
pub const fn collatz_next(n: u64) -> Option<u64> {
if n == 0 {
return None;
}
if is_even(n) {
Some(n / 2)
} else {
checked_odd_step(n)
}
}
#[must_use]
pub fn collatz_sequence(n: u64) -> Option<Vec<u64>> {
if n == 0 {
return None;
}
let mut current = n;
let mut sequence = vec![current];
while current != 1 {
current = collatz_next(current)?;
sequence.push(current);
}
Some(sequence)
}
#[must_use]
pub fn stopping_time(n: u64) -> Option<u64> {
trajectory_metrics(n).map(|metrics| metrics.stopping_time)
}
#[must_use]
pub fn total_stopping_time(n: u64) -> Option<u64> {
trajectory_metrics(n).map(|metrics| metrics.total_stopping_time)
}
#[must_use]
pub fn trajectory_len(n: u64) -> Option<u64> {
total_stopping_time(n)?.checked_add(1)
}
#[must_use]
pub fn max_value_in_trajectory(n: u64) -> Option<u64> {
trajectory_metrics(n).map(|metrics| metrics.max_value)
}
#[must_use]
pub fn reaches_one(n: u64) -> bool {
trajectory_metrics(n).is_some()
}
#[must_use]
pub const fn parity(n: u64) -> Option<CollatzParity> {
if n == 0 {
return None;
}
if is_even(n) {
Some(CollatzParity::Even)
} else {
Some(CollatzParity::Odd)
}
}
#[must_use]
pub fn parity_vector(n: u64) -> Option<Vec<CollatzParity>> {
if n == 0 {
return None;
}
let mut current = n;
let mut parities = Vec::new();
while current != 1 {
parities.push(parity(current)?);
current = collatz_next(current)?;
}
Some(parities)
}
#[must_use]
pub fn verify_range(start: u64, end: u64) -> CollatzRangeSummary {
let mut summary = CollatzRangeSummary {
start,
end,
checked: 0,
reached_one: 0,
overflowed: 0,
max_total_stopping_time: None,
max_trajectory_value: None,
};
if start > end {
return summary;
}
for input in start..=end {
if input == 0 {
continue;
}
summary.checked += 1;
if let Some(metrics) = trajectory_metrics(input) {
summary.reached_one += 1;
update_max_pair(
&mut summary.max_total_stopping_time,
input,
metrics.total_stopping_time,
);
update_max_pair(&mut summary.max_trajectory_value, input, metrics.max_value);
} else {
summary.overflowed += 1;
}
}
summary
}
#[cfg(test)]
mod tests {
use super::{
CollatzParity, CollatzRangeSummary, collatz_next, collatz_sequence,
max_value_in_trajectory, parity, parity_vector, reaches_one, stopping_time,
total_stopping_time, trajectory_len, verify_range,
};
#[test]
fn computes_next_values() {
assert_eq!(collatz_next(1), Some(4));
assert_eq!(collatz_next(2), Some(1));
assert_eq!(collatz_next(3), Some(10));
assert_eq!(collatz_next(0), None);
}
#[test]
fn detects_checked_overflow_for_odd_steps() {
assert_eq!(collatz_next(u64::MAX), None);
}
#[test]
fn builds_full_sequences() {
assert_eq!(collatz_sequence(6), Some(vec![6, 3, 10, 5, 16, 8, 4, 2, 1]));
assert_eq!(collatz_sequence(0), None);
}
#[test]
fn computes_total_stopping_times() {
assert_eq!(total_stopping_time(1), Some(0));
assert_eq!(total_stopping_time(6), Some(8));
}
#[test]
fn computes_trajectory_lengths() {
assert_eq!(trajectory_len(1), Some(1));
assert_eq!(trajectory_len(6), Some(9));
}
#[test]
fn computes_trajectory_maxima() {
assert_eq!(max_value_in_trajectory(6), Some(16));
assert_eq!(max_value_in_trajectory(1), Some(1));
}
#[test]
fn computes_stopping_times() {
assert_eq!(stopping_time(1), Some(0));
assert_eq!(stopping_time(6), Some(1));
assert_eq!(stopping_time(3), Some(6));
}
#[test]
fn reports_reachability_without_panicking() {
assert!(reaches_one(6));
assert!(!reaches_one(0));
}
#[test]
fn classifies_parity_helpers() {
assert_eq!(parity(8), Some(CollatzParity::Even));
assert_eq!(parity(9), Some(CollatzParity::Odd));
assert_eq!(parity(0), None);
}
#[test]
fn builds_parity_vectors_without_terminal_one() {
assert_eq!(
parity_vector(6),
Some(vec![
CollatzParity::Even,
CollatzParity::Odd,
CollatzParity::Even,
CollatzParity::Odd,
CollatzParity::Even,
CollatzParity::Even,
CollatzParity::Even,
CollatzParity::Even,
])
);
assert_eq!(parity_vector(1), Some(vec![]));
assert_eq!(parity_vector(0), None);
}
#[test]
fn verifies_bounded_ranges() {
assert_eq!(
verify_range(1, 10),
CollatzRangeSummary {
start: 1,
end: 10,
checked: 10,
reached_one: 10,
overflowed: 0,
max_total_stopping_time: Some((9, 19)),
max_trajectory_value: Some((7, 52)),
}
);
}
#[test]
fn skips_zero_and_allows_empty_ranges() {
assert_eq!(
verify_range(0, 0),
CollatzRangeSummary {
start: 0,
end: 0,
checked: 0,
reached_one: 0,
overflowed: 0,
max_total_stopping_time: None,
max_trajectory_value: None,
}
);
assert_eq!(
verify_range(10, 1),
CollatzRangeSummary {
start: 10,
end: 1,
checked: 0,
reached_one: 0,
overflowed: 0,
max_total_stopping_time: None,
max_trajectory_value: None,
}
);
}
}