#[must_use]
pub fn gcd(a: i64, b: i64) -> i64 {
let (mut a, mut b) = (a.abs(), b.abs());
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a
}
#[must_use]
pub fn lcm(a: i64, b: i64) -> i64 {
if a == 0 || b == 0 {
0
} else {
(a / gcd(a, b)) * b
}
}
fn ext_gcd(a: i64, b: i64) -> (i64, i64, i64) {
if b == 0 {
(a, 1, 0)
} else {
let (g, x, y) = ext_gcd(b, a % b);
(g, y, x - (a / b) * y)
}
}
fn crt_pair(r1: i64, m1: i64, r2: i64, m2: i64) -> Option<(i64, i64)> {
let (g, p, _) = ext_gcd(m1, m2);
if (r2 - r1).rem_euclid(g) != 0 {
return None; }
let l = m1 / g * m2;
let mul = (r2 - r1) / g;
let step = m2 / g;
let x = r1 + m1 * (p.rem_euclid(step) * mul.rem_euclid(step)).rem_euclid(step);
Some((x.rem_euclid(l), l))
}
#[must_use]
pub fn crt_combine(congruences: &[(i64, i64)]) -> Option<i64> {
let mut acc = (0i64, 1i64); for &(r, m) in congruences {
acc = crt_pair(acc.0, acc.1, r.rem_euclid(m), m)?;
}
Some(acc.0)
}
#[must_use]
pub fn cycle_period(moduli: &[i64]) -> i64 {
moduli.iter().fold(1, |a, &m| lcm(a, m))
}
#[must_use]
pub fn parallel_phases(day: i64, moduli: &[i64]) -> Vec<i64> {
moduli.iter().map(|&m| day.rem_euclid(m)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
fn reachable(m: i64, n: i64) -> usize {
let mut set = HashSet::new();
for a in 0..m {
for b in 0..n {
if let Some(x) = crt_combine(&[(a, m), (b, n)]) {
set.insert(x);
}
}
}
set.len()
}
#[test]
fn ganzhi_is_diagonal_subgroup_not_product() {
assert_eq!(gcd(10, 12), 2);
assert_eq!(cycle_period(&[10, 12]), 60);
assert_eq!(reachable(10, 12), 60, "干支应为 60 个对角子群元素,非 120");
assert_eq!(crt_combine(&[(0, 10), (0, 12)]), Some(0));
assert!(crt_combine(&[(0, 10), (1, 12)]).is_none());
}
#[test]
fn maya_tzolkin_is_clean_crt() {
assert_eq!(gcd(13, 20), 1);
assert_eq!(cycle_period(&[13, 20]), 260);
assert_eq!(reachable(13, 20), 260);
}
#[test]
fn tibetan_5x12_cleaner_than_ganzhi() {
assert_eq!(gcd(5, 12), 1);
assert_eq!(reachable(5, 12), 60, "藏历 5×12 应 60 个全可达");
}
#[test]
fn crt_recovers_residues() {
let x = crt_combine(&[(3, 13), (7, 20)]).unwrap();
assert_eq!(x % 13, 3);
assert_eq!(x % 20, 7);
}
#[test]
fn crt_recovers_residues_on_the_non_coprime_pair_too() {
let mut reached = 0;
for stem in 0..10i64 {
for branch in 0..12i64 {
let Some(x) = crt_combine(&[(stem, 10), (branch, 12)]) else {
assert_ne!(stem % 2, branch % 2, "干{stem} 支{branch} 同阴阳却不可达");
continue;
};
assert_eq!(stem % 2, branch % 2, "干{stem} 支{branch} 异阴阳却可达");
assert!((0..60).contains(&x), "干{stem} 支{branch} 落在 0..60 之外:{x}");
assert_eq!(x.rem_euclid(10), stem, "干{stem} 支{branch} 还原不出干");
assert_eq!(x.rem_euclid(12), branch, "干{stem} 支{branch} 还原不出支");
reached += 1;
}
}
assert_eq!(reached, 60);
}
#[test]
fn gcd_lcm_edges() {
assert_eq!(gcd(-12, 8), 4); assert_eq!(lcm(0, 5), 0); assert_eq!(lcm(4, 6), 12);
}
#[test]
fn crt_inconsistent_returns_none() {
assert!(crt_combine(&[(0, 4), (1, 6)]).is_none());
}
#[test]
fn pawukon_parallel_phases() {
assert_eq!(cycle_period(&[2, 3, 5, 7]), 210);
let p = parallel_phases(211, &[2, 3, 5, 7]);
assert_eq!(p, vec![1, 1, 1, 1]); }
use proptest::prelude::*;
proptest! {
#[test]
fn prop_gcd_divides_both(a in 1i64..1_000_000, b in 1i64..1_000_000) {
let g = gcd(a, b);
prop_assert!(g >= 1);
prop_assert_eq!(a % g, 0);
prop_assert_eq!(b % g, 0);
}
#[test]
fn prop_gcd_lcm_product(a in 1i64..100_000, b in 1i64..100_000) {
prop_assert_eq!(gcd(a, b) * lcm(a, b), a * b);
}
#[test]
fn prop_crt_satisfies_all_congruences(
v in 0i64..1_000_000,
moduli in prop::collection::vec(2i64..50, 1..4),
) {
let cong: Vec<(i64, i64)> = moduli.iter().map(|&m| (v % m, m)).collect();
let r = crt_combine(&cong).expect("一致同余必有解");
for &(_, m) in &cong {
prop_assert_eq!(r.rem_euclid(m), v.rem_euclid(m));
}
}
#[test]
fn prop_parallel_phases_in_range(
day in any::<i64>(),
moduli in prop::collection::vec(1i64..100, 1..6),
) {
for (p, &m) in parallel_phases(day, &moduli).iter().zip(&moduli) {
prop_assert!(*p >= 0 && *p < m);
}
}
#[test]
fn prop_cycle_period_pair_is_lcm(a in 1i64..10_000, b in 1i64..10_000) {
prop_assert_eq!(cycle_period(&[a, b]), lcm(a, b));
}
}
}