use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct RodNumeral {
pub value: i64,
}
impl RodNumeral {
#[must_use]
#[inline]
pub fn new(value: i64) -> Self {
Self { value }
}
}
impl core::fmt::Display for RodNumeral {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.value == 0 {
return write!(f, "[ ]");
}
let abs_val = self.value.unsigned_abs();
let sign = if self.value < 0 { "-" } else { "" };
write!(f, "{sign}[")?;
let digits: Vec<u8> = {
let mut d = Vec::new();
let mut n = abs_val;
if n == 0 {
d.push(0);
} else {
while n > 0 {
d.push((n % 10) as u8);
n /= 10;
}
d.reverse();
}
d
};
for (i, &digit) in digits.iter().enumerate() {
if i > 0 {
write!(f, " ")?;
}
let pos_from_right = digits.len() - 1 - i;
let rod_char = if pos_from_right.is_multiple_of(2) {
'|'
} else {
'-'
};
if digit == 0 {
write!(f, " ")?;
} else {
for _ in 0..digit.min(5) {
write!(f, "{rod_char}")?;
}
if digit > 5 {
write!(f, "+")?;
for _ in 0..(digit - 5) {
write!(f, "{rod_char}")?;
}
}
}
}
write!(f, "]")
}
}
#[must_use]
#[inline]
pub fn rod_add(a: RodNumeral, b: RodNumeral) -> RodNumeral {
RodNumeral::new(a.value.wrapping_add(b.value))
}
#[must_use]
#[inline]
pub fn rod_subtract(a: RodNumeral, b: RodNumeral) -> RodNumeral {
RodNumeral::new(a.value.wrapping_sub(b.value))
}
#[must_use]
#[inline]
pub fn rod_multiply(a: RodNumeral, b: RodNumeral) -> RodNumeral {
RodNumeral::new(a.value.wrapping_mul(b.value))
}
#[must_use]
pub fn chinese_remainder(residues: &[(u64, u64)]) -> Option<u64> {
if residues.is_empty() {
return None;
}
if residues.iter().any(|&(_, m)| m == 0) {
return None;
}
if residues.len() == 1 {
let (r, m) = residues[0];
return Some(r % m);
}
let mut product: u128 = 1;
for &(_, m) in residues {
product = product.checked_mul(u128::from(m))?;
}
let mut sum: u128 = 0;
for &(remainder, modulus) in residues {
let m = u128::from(modulus);
let r = u128::from(remainder);
let p = product / m;
let inv = mod_inverse(p % m, m)?;
sum = (sum + r * p % product * inv % product) % product;
}
u64::try_from(sum % product).ok()
}
fn mod_inverse(a: u128, m: u128) -> Option<u128> {
if m == 1 {
return Some(0);
}
let (mut old_r, mut r) = (a as i128, m as i128);
let (mut old_s, mut s) = (1i128, 0i128);
while r != 0 {
let quotient = old_r / r;
let temp_r = r;
r = old_r - quotient * r;
old_r = temp_r;
let temp_s = s;
s = old_s - quotient * s;
old_s = temp_s;
}
if old_r != 1 {
return None;
}
let result = ((old_s % m as i128) + m as i128) % m as i128;
Some(result as u128)
}
#[must_use]
pub fn magic_square(n: usize) -> Option<Vec<Vec<u64>>> {
if n < 3 || n.is_multiple_of(2) {
return None;
}
if n == 3 {
return Some(vec![vec![2, 7, 6], vec![9, 5, 1], vec![4, 3, 8]]);
}
let mut square = vec![vec![0u64; n]; n];
let mut row = 0;
let mut col = n / 2;
for num in 1..=((n * n) as u64) {
square[row][col] = num;
let new_row = if row == 0 { n - 1 } else { row - 1 };
let new_col = (col + 1) % n;
if square[new_row][new_col] != 0 {
row = (row + 1) % n;
} else {
row = new_row;
col = new_col;
}
}
Some(square)
}
#[must_use]
pub fn is_magic_square(square: &[Vec<u64>]) -> bool {
let n = square.len();
if n == 0 {
return false;
}
if square.iter().any(|row| row.len() != n) {
return false;
}
let magic_sum: u64 = square[0].iter().sum();
for row in square {
let s: u64 = row.iter().sum();
if s != magic_sum {
return false;
}
}
for col in 0..n {
let s: u64 = square.iter().map(|row| row[col]).sum();
if s != magic_sum {
return false;
}
}
let s: u64 = (0..n).map(|i| square[i][i]).sum();
if s != magic_sum {
return false;
}
let s: u64 = (0..n).map(|i| square[i][n - 1 - i]).sum();
if s != magic_sum {
return false;
}
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HeavenlyStem {
Jia,
Yi,
Bing,
Ding,
Wu,
Ji,
Geng,
Xin,
Ren,
Gui,
}
const HEAVENLY_STEMS: [HeavenlyStem; 10] = [
HeavenlyStem::Jia,
HeavenlyStem::Yi,
HeavenlyStem::Bing,
HeavenlyStem::Ding,
HeavenlyStem::Wu,
HeavenlyStem::Ji,
HeavenlyStem::Geng,
HeavenlyStem::Xin,
HeavenlyStem::Ren,
HeavenlyStem::Gui,
];
impl core::fmt::Display for HeavenlyStem {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let name = match self {
Self::Jia => "Jiǎ (甲)",
Self::Yi => "Yǐ (乙)",
Self::Bing => "Bǐng (丙)",
Self::Ding => "Dīng (丁)",
Self::Wu => "Wù (戊)",
Self::Ji => "Jǐ (己)",
Self::Geng => "Gēng (庚)",
Self::Xin => "Xīn (辛)",
Self::Ren => "Rén (壬)",
Self::Gui => "Guǐ (癸)",
};
write!(f, "{name}")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EarthlyBranch {
Zi,
Chou,
Yin,
Mao,
Chen,
Si,
Wu,
Wei,
Shen,
You,
Xu,
Hai,
}
const EARTHLY_BRANCHES: [EarthlyBranch; 12] = [
EarthlyBranch::Zi,
EarthlyBranch::Chou,
EarthlyBranch::Yin,
EarthlyBranch::Mao,
EarthlyBranch::Chen,
EarthlyBranch::Si,
EarthlyBranch::Wu,
EarthlyBranch::Wei,
EarthlyBranch::Shen,
EarthlyBranch::You,
EarthlyBranch::Xu,
EarthlyBranch::Hai,
];
const ZODIAC_ANIMALS: [&str; 12] = [
"Rat", "Ox", "Tiger", "Rabbit", "Dragon", "Snake", "Horse", "Goat", "Monkey", "Rooster", "Dog",
"Pig",
];
impl core::fmt::Display for EarthlyBranch {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let idx = EARTHLY_BRANCHES
.iter()
.position(|&b| b == *self)
.unwrap_or(0);
let name = match self {
Self::Zi => "Zǐ (子)",
Self::Chou => "Chǒu (丑)",
Self::Yin => "Yín (寅)",
Self::Mao => "Mǎo (卯)",
Self::Chen => "Chén (辰)",
Self::Si => "Sì (巳)",
Self::Wu => "Wǔ (午)",
Self::Wei => "Wèi (未)",
Self::Shen => "Shēn (申)",
Self::You => "Yǒu (酉)",
Self::Xu => "Xū (戌)",
Self::Hai => "Hài (亥)",
};
write!(f, "{name} — {}", ZODIAC_ANIMALS[idx])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct SexagenaryYear {
pub stem: HeavenlyStem,
pub branch: EarthlyBranch,
pub cycle_position: u8,
}
impl core::fmt::Display for SexagenaryYear {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let branch_idx = EARTHLY_BRANCHES
.iter()
.position(|&b| b == self.branch)
.unwrap_or(0);
write!(
f,
"{:?}-{:?} ({}, {})",
self.stem, self.branch, ZODIAC_ANIMALS[branch_idx], self.cycle_position
)
}
}
#[must_use]
pub fn sexagenary_from_year(year: i64) -> SexagenaryYear {
let offset = (year - 4).rem_euclid(60);
let stem_idx = offset.rem_euclid(10) as usize;
let branch_idx = offset.rem_euclid(12) as usize;
let cycle_position = (offset % 60 + 1) as u8;
SexagenaryYear {
stem: HEAVENLY_STEMS[stem_idx],
branch: EARTHLY_BRANCHES[branch_idx],
cycle_position,
}
}
#[must_use]
pub fn sexagenary_from_jdn(jdn: f64) -> SexagenaryYear {
let greg = crate::gregorian::jdn_to_gregorian(jdn);
sexagenary_from_year(greg.year)
}
#[cfg(feature = "varna")]
#[must_use = "returns the Unicode rod numeral string without side effects"]
pub fn to_unicode_rods(n: u64) -> crate::error::Result<String> {
if n == 0 {
return Err(crate::error::SankhyaError::InvalidBase(
"zero has no rod numeral representation".into(),
));
}
let system = varna::script::numerals::chinese_rod_numerals();
let mut digits = Vec::new();
let mut remaining = n;
while remaining > 0 {
let d = (remaining % 10) as u32;
if d == 0 {
digits.push(" ".to_string());
} else if let Some(ch) = system.char_for(d) {
digits.push(ch.to_string());
}
remaining /= 10;
}
digits.reverse();
Ok(digits.join(""))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rod_arithmetic() {
let a = RodNumeral::new(42);
let b = RodNumeral::new(17);
assert_eq!(rod_add(a, b).value, 59);
assert_eq!(rod_subtract(a, b).value, 25);
assert_eq!(rod_multiply(a, b).value, 714);
}
#[test]
fn crt_sun_tzu_problem() {
let result = chinese_remainder(&[(2, 3), (3, 5), (2, 7)]);
assert_eq!(result, Some(23));
}
#[test]
fn crt_single() {
assert_eq!(chinese_remainder(&[(3, 7)]), Some(3));
}
#[test]
fn crt_empty() {
assert_eq!(chinese_remainder(&[]), None);
}
#[test]
fn lo_shu_magic_square() {
let sq = magic_square(3).unwrap();
assert!(is_magic_square(&sq));
let sum: u64 = sq[0].iter().sum();
assert_eq!(sum, 15);
}
#[test]
fn magic_square_5x5() {
let sq = magic_square(5).unwrap();
assert!(is_magic_square(&sq));
let sum: u64 = sq[0].iter().sum();
assert_eq!(sum, 65);
}
#[test]
fn magic_square_even_returns_none() {
assert!(magic_square(4).is_none());
}
#[test]
fn sexagenary_2024_wood_dragon() {
let s = sexagenary_from_year(2024);
assert_eq!(s.stem, HeavenlyStem::Jia);
assert_eq!(s.branch, EarthlyBranch::Chen); assert_eq!(s.cycle_position, 41);
}
#[test]
fn sexagenary_2025_wood_snake() {
let s = sexagenary_from_year(2025);
assert_eq!(s.stem, HeavenlyStem::Yi);
assert_eq!(s.branch, EarthlyBranch::Si); }
#[test]
fn sexagenary_4_ce_jia_zi() {
let s = sexagenary_from_year(4);
assert_eq!(s.stem, HeavenlyStem::Jia);
assert_eq!(s.branch, EarthlyBranch::Zi);
assert_eq!(s.cycle_position, 1);
}
#[test]
fn sexagenary_60_year_cycle() {
let s1 = sexagenary_from_year(2024);
let s2 = sexagenary_from_year(2024 + 60);
assert_eq!(s1.stem, s2.stem);
assert_eq!(s1.branch, s2.branch);
}
#[test]
fn sexagenary_all_60_unique() {
let mut pairs = std::collections::HashSet::new();
for i in 0..60 {
let s = sexagenary_from_year(4 + i);
pairs.insert((format!("{:?}", s.stem), format!("{:?}", s.branch)));
}
assert_eq!(pairs.len(), 60);
}
#[test]
fn sexagenary_from_jdn_matches_year() {
let s = sexagenary_from_jdn(2_460_676.5);
let s2 = sexagenary_from_year(2025);
assert_eq!(s, s2);
}
#[test]
fn sexagenary_serde_roundtrip() {
let s = sexagenary_from_year(2024);
let json = serde_json::to_string(&s).unwrap();
let back: SexagenaryYear = serde_json::from_str(&json).unwrap();
assert_eq!(s, back);
}
#[test]
fn stem_display() {
assert_eq!(HeavenlyStem::Jia.to_string(), "Jiǎ (甲)");
assert_eq!(HeavenlyStem::Gui.to_string(), "Guǐ (癸)");
}
#[test]
fn branch_display() {
let d = EarthlyBranch::Chen.to_string();
assert!(d.contains("Dragon"));
assert!(d.contains("辰"));
}
#[cfg(feature = "varna")]
mod unicode_rod_tests {
use super::*;
#[test]
fn unicode_rods_single_digit() {
assert_eq!(to_unicode_rods(1).unwrap(), "𝍠");
assert_eq!(to_unicode_rods(9).unwrap(), "𝍨");
}
#[test]
fn unicode_rods_multi_digit() {
let s = to_unicode_rods(42).unwrap();
assert_eq!(s, "𝍣𝍡");
}
#[test]
fn unicode_rods_with_zero() {
let s = to_unicode_rods(101).unwrap();
assert_eq!(s, "𝍠 𝍠");
}
#[test]
fn unicode_rods_zero_errors() {
assert!(to_unicode_rods(0).is_err());
}
}
}