use serde::{Deserialize, Serialize};
use crate::error::{Result, SankhyaError};
pub fn decompose(mut numerator: u64, mut denominator: u64) -> Result<Vec<u64>> {
if denominator == 0 {
return Err(SankhyaError::InvalidFraction(
"denominator cannot be zero".into(),
));
}
if numerator == 0 {
return Ok(Vec::new());
}
let g = gcd(numerator, denominator);
numerator /= g;
denominator /= g;
if numerator == 1 {
return Ok(vec![denominator]);
}
let mut result = Vec::new();
let max_iterations = 100;
for _ in 0..max_iterations {
if numerator == 0 {
break;
}
if numerator == 1 {
result.push(denominator);
break;
}
let d = denominator.div_ceil(numerator);
result.push(d);
let new_num = numerator
.checked_mul(d)
.and_then(|nd| nd.checked_sub(denominator))
.ok_or_else(|| {
SankhyaError::OverflowError("Egyptian fraction decomposition overflow".into())
})?;
let new_den = denominator.checked_mul(d).ok_or_else(|| {
SankhyaError::OverflowError("Egyptian fraction decomposition overflow".into())
})?;
if new_num == 0 {
break;
}
let g = gcd(new_num, new_den);
numerator = new_num / g;
denominator = new_den / g;
}
if numerator != 0 && (result.is_empty() || numerator != 1) {
return Err(SankhyaError::ComputationError(
"Egyptian fraction decomposition did not terminate".into(),
));
}
Ok(result)
}
fn gcd(mut a: u64, mut b: u64) -> u64 {
while b != 0 {
let t = b;
b = a % b;
a = t;
}
a
}
#[must_use]
#[inline]
pub fn egyptian_multiply(a: u64, b: u64) -> u64 {
let mut result: u64 = 0;
let mut multiplicand = a;
let mut multiplier = b;
while multiplier > 0 {
if multiplier & 1 == 1 {
result = result.wrapping_add(multiplicand);
}
multiplicand = multiplicand.wrapping_shl(1);
multiplier >>= 1;
}
result
}
pub fn egyptian_divide(dividend: u64, divisor: u64) -> Result<(u64, Vec<u64>)> {
if divisor == 0 {
return Err(SankhyaError::InvalidFraction(
"divisor cannot be zero".into(),
));
}
let quotient = dividend / divisor;
let remainder = dividend % divisor;
if remainder == 0 {
return Ok((quotient, Vec::new()));
}
let fractions = decompose(remainder, divisor)?;
Ok((quotient, fractions))
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Decan {
pub number: u8,
pub name: &'static str,
pub ecliptic_longitude: f64,
}
pub static DECANS: [Decan; 36] = [
Decan {
number: 1,
name: "Kenmet",
ecliptic_longitude: 0.0,
},
Decan {
number: 2,
name: "Khentet Hrt",
ecliptic_longitude: 10.0,
},
Decan {
number: 3,
name: "Khentet Khrt",
ecliptic_longitude: 20.0,
},
Decan {
number: 4,
name: "Hat Djat",
ecliptic_longitude: 30.0,
},
Decan {
number: 5,
name: "Pehui Djat",
ecliptic_longitude: 40.0,
},
Decan {
number: 6,
name: "Temat Hrt",
ecliptic_longitude: 50.0,
},
Decan {
number: 7,
name: "Temat Khrt",
ecliptic_longitude: 60.0,
},
Decan {
number: 8,
name: "Ushat",
ecliptic_longitude: 70.0,
},
Decan {
number: 9,
name: "Bekati",
ecliptic_longitude: 80.0,
},
Decan {
number: 10,
name: "Tepai",
ecliptic_longitude: 90.0,
},
Decan {
number: 11,
name: "Khentu Hrt",
ecliptic_longitude: 100.0,
},
Decan {
number: 12,
name: "Khentu Khrt",
ecliptic_longitude: 110.0,
},
Decan {
number: 13,
name: "Sapt Khenmet",
ecliptic_longitude: 120.0,
},
Decan {
number: 14,
name: "Khenmet",
ecliptic_longitude: 130.0,
},
Decan {
number: 15,
name: "Seshmu",
ecliptic_longitude: 140.0,
},
Decan {
number: 16,
name: "Kenmu",
ecliptic_longitude: 150.0,
},
Decan {
number: 17,
name: "Semed",
ecliptic_longitude: 160.0,
},
Decan {
number: 18,
name: "Seret",
ecliptic_longitude: 170.0,
},
Decan {
number: 19,
name: "Sah",
ecliptic_longitude: 180.0,
},
Decan {
number: 20,
name: "Sopdet",
ecliptic_longitude: 190.0,
},
Decan {
number: 21,
name: "Knmt",
ecliptic_longitude: 200.0,
},
Decan {
number: 22,
name: "Sah Sapt",
ecliptic_longitude: 210.0,
},
Decan {
number: 23,
name: "Tepy Khentet",
ecliptic_longitude: 220.0,
},
Decan {
number: 24,
name: "Khentet Hrt S",
ecliptic_longitude: 230.0,
},
Decan {
number: 25,
name: "Khentet Khrt S",
ecliptic_longitude: 240.0,
},
Decan {
number: 26,
name: "Apt Hnt",
ecliptic_longitude: 250.0,
},
Decan {
number: 27,
name: "Ipds",
ecliptic_longitude: 260.0,
},
Decan {
number: 28,
name: "Sba N Hry Ib",
ecliptic_longitude: 270.0,
},
Decan {
number: 29,
name: "Kher Khept Khentet",
ecliptic_longitude: 280.0,
},
Decan {
number: 30,
name: "Tepy Ahui",
ecliptic_longitude: 290.0,
},
Decan {
number: 31,
name: "Ahui",
ecliptic_longitude: 300.0,
},
Decan {
number: 32,
name: "Pehui Ahui",
ecliptic_longitude: 310.0,
},
Decan {
number: 33,
name: "Tepy Baka",
ecliptic_longitude: 320.0,
},
Decan {
number: 34,
name: "Baka",
ecliptic_longitude: 330.0,
},
Decan {
number: 35,
name: "Tepy Akhui",
ecliptic_longitude: 340.0,
},
Decan {
number: 36,
name: "Akhui",
ecliptic_longitude: 350.0,
},
];
#[must_use]
pub fn decan_from_longitude(degrees: f64) -> &'static Decan {
let normalized = ((degrees % 360.0) + 360.0) % 360.0;
let index = (normalized / 10.0) as usize;
let index = if index >= 36 { 35 } else { index };
&DECANS[index]
}
pub const SOTHIC_CYCLE_CIVIL_YEARS: u32 = 1461;
pub const SOTHIC_CYCLE_DAYS: u64 = 533_265;
pub const SOTHIC_DRIFT_PER_YEAR: f64 = 0.25;
pub const CENSORINUS_EPOCH_JDN: f64 = 1_772_028.5;
#[must_use]
#[inline]
pub fn sopdet() -> &'static Decan {
&DECANS[19] }
#[must_use]
pub fn sothic_drift(years_since_epoch: u32) -> f64 {
let years_in_cycle = years_since_epoch % SOTHIC_CYCLE_CIVIL_YEARS;
f64::from(years_in_cycle) * SOTHIC_DRIFT_PER_YEAR
}
#[must_use]
pub fn sothic_position(jdn: f64) -> (i32, u32, f64) {
let days_from_epoch = jdn - CENSORINUS_EPOCH_JDN;
let civil_years_from_epoch = days_from_epoch / 365.0;
let cycle_number =
(civil_years_from_epoch / f64::from(SOTHIC_CYCLE_CIVIL_YEARS)).floor() as i32;
let year_in_cycle = ((civil_years_from_epoch % f64::from(SOTHIC_CYCLE_CIVIL_YEARS))
+ f64::from(SOTHIC_CYCLE_CIVIL_YEARS))
% f64::from(SOTHIC_CYCLE_CIVIL_YEARS);
let year_in_cycle_u32 = year_in_cycle as u32;
let drift = sothic_drift(year_in_cycle_u32);
(cycle_number, year_in_cycle_u32, drift)
}
pub fn next_sopdet_rising(jdn: f64, latitude: f64) -> Result<f64> {
if !(-60.0..=60.0).contains(&latitude) {
return Err(SankhyaError::InvalidDate(format!(
"latitude {latitude} outside observable range for Sirius (-60 to +60)"
)));
}
let latitude_offset = latitude - 30.0;
let year_approx = 2000.0 + (jdn - 2_451_545.0) / 365.25;
let jan1_jdn = 2_451_545.0 + (year_approx.floor() - 2000.0) * 365.25;
let rising_jdn = jan1_jdn + 200.0 + latitude_offset;
if rising_jdn <= jdn {
Ok(rising_jdn + 365.25)
} else {
Ok(rising_jdn)
}
}
#[cfg(feature = "varna")]
#[must_use = "returns the hieroglyphic string without side effects"]
pub fn to_hieroglyphic(n: u64) -> Result<String> {
if n == 0 {
return Ok(String::new());
}
if n > 9_999_999 {
return Err(SankhyaError::OverflowError(format!(
"cannot represent {n} in Egyptian hieroglyphic numerals (max 9,999,999)"
)));
}
let system = varna::script::numerals::egyptian_hieroglyphic();
let powers: &[u64] = &[1_000_000, 100_000, 10_000, 1_000, 100, 10, 1];
let mut result = String::new();
let mut remainder = n;
for &power in powers {
let count = remainder / power;
remainder %= power;
if count > 0
&& let Some(ch) = system.char_for(power as u32)
{
for _ in 0..count {
result.push_str(ch);
}
}
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decompose_two_thirds() {
let result = decompose(2, 3).unwrap();
assert_eq!(result, vec![2, 6]);
let sum: f64 = result.iter().map(|&d| 1.0 / d as f64).sum();
assert!((sum - 2.0 / 3.0).abs() < 1e-15);
}
#[test]
fn decompose_unit_fraction() {
let result = decompose(1, 5).unwrap();
assert_eq!(result, vec![5]);
}
#[test]
fn multiply_12_13() {
assert_eq!(egyptian_multiply(12, 13), 156);
}
#[test]
fn multiply_commutative() {
assert_eq!(egyptian_multiply(7, 11), egyptian_multiply(11, 7));
}
#[test]
fn divide_7_3() {
let (q, rem) = egyptian_divide(7, 3).unwrap();
assert_eq!(q, 2);
assert_eq!(rem, vec![3]); }
#[test]
fn decan_lookup() {
let d = decan_from_longitude(0.0);
assert_eq!(d.number, 1);
assert_eq!(d.name, "Kenmet");
let d = decan_from_longitude(195.0);
assert_eq!(d.number, 20); }
#[test]
fn decan_negative_longitude() {
let d = decan_from_longitude(-10.0);
assert_eq!(d.number, 36); }
#[test]
fn sopdet_is_decan_20() {
let s = sopdet();
assert_eq!(s.number, 20);
assert_eq!(s.name, "Sopdet");
}
#[test]
fn sothic_drift_at_epoch_is_zero() {
let drift = sothic_drift(0);
assert!((drift - 0.0).abs() < f64::EPSILON);
}
#[test]
fn sothic_drift_after_4_years() {
let drift = sothic_drift(4);
assert!((drift - 1.0).abs() < f64::EPSILON);
}
#[test]
fn sothic_drift_full_cycle_resets() {
let drift = sothic_drift(SOTHIC_CYCLE_CIVIL_YEARS);
assert!((drift - 0.0).abs() < f64::EPSILON);
}
#[test]
fn sothic_position_at_censorinus_epoch() {
let (cycle, year, drift) = sothic_position(CENSORINUS_EPOCH_JDN);
assert_eq!(cycle, 0);
assert_eq!(year, 0);
assert!((drift - 0.0).abs() < f64::EPSILON);
}
#[test]
fn sothic_cycle_days_consistent() {
assert_eq!(365 * u64::from(SOTHIC_CYCLE_CIVIL_YEARS), SOTHIC_CYCLE_DAYS);
}
#[test]
fn next_sopdet_rising_memphis() {
let rising = next_sopdet_rising(2_451_545.0, 30.0).unwrap(); assert!(rising > 2_451_545.0);
}
#[test]
fn next_sopdet_rising_invalid_latitude() {
assert!(next_sopdet_rising(2_451_545.0, 80.0).is_err());
}
#[cfg(feature = "varna")]
mod hieroglyphic_tests {
use super::*;
#[test]
fn hieroglyphic_single_digit() {
let s = to_hieroglyphic(3).unwrap();
assert_eq!(s, "πΊπΊπΊ");
}
#[test]
fn hieroglyphic_mixed() {
let s = to_hieroglyphic(23).unwrap();
assert_eq!(s, "πππΊπΊπΊ");
}
#[test]
fn hieroglyphic_powers() {
let s = to_hieroglyphic(1_000).unwrap();
assert_eq!(s, "πΌ");
let s = to_hieroglyphic(1_000_000).unwrap();
assert_eq!(s, "π¨");
}
#[test]
fn hieroglyphic_zero() {
assert_eq!(to_hieroglyphic(0).unwrap(), "");
}
#[test]
fn hieroglyphic_overflow() {
assert!(to_hieroglyphic(10_000_000).is_err());
}
}
}