use serde::{Deserialize, Serialize};
use crate::error::{Result, SankhyaError};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AlJabrForm {
SquaresEqualRoots,
SquaresEqualNumber,
RootsEqualNumber,
SquaresAndRootsEqualNumber,
SquaresAndNumberEqualRoots,
RootsAndNumberEqualSquares,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AlJabrSolution {
pub form: AlJabrForm,
pub roots: Vec<f64>,
pub coefficients: (f64, f64, f64),
}
pub fn solve_al_jabr(a: f64, b: f64, c: f64) -> Result<AlJabrSolution> {
if a.abs() < f64::EPSILON && b.abs() < f64::EPSILON && c.abs() < f64::EPSILON {
return Err(SankhyaError::ComputationError(
"all coefficients are zero".into(),
));
}
if a.abs() < f64::EPSILON {
if b.abs() < f64::EPSILON {
return Err(SankhyaError::ComputationError(
"degenerate equation: no variable terms".into(),
));
}
let root = -c / b;
let mut roots = Vec::new();
if root > 0.0 {
roots.push(root);
}
return Ok(AlJabrSolution {
form: AlJabrForm::RootsEqualNumber,
roots,
coefficients: (a, b, c),
});
}
let p = b / a; let q = c / a;
let form = if p.abs() < f64::EPSILON && q.abs() < f64::EPSILON {
AlJabrForm::SquaresEqualRoots
} else if q.abs() < f64::EPSILON {
AlJabrForm::SquaresEqualRoots
} else if p.abs() < f64::EPSILON {
AlJabrForm::SquaresEqualNumber
} else if p > 0.0 && q > 0.0 {
AlJabrForm::SquaresAndRootsEqualNumber
} else if p > 0.0 && q < 0.0 {
AlJabrForm::SquaresAndRootsEqualNumber
} else if p < 0.0 && q > 0.0 {
AlJabrForm::SquaresAndNumberEqualRoots
} else {
AlJabrForm::RootsAndNumberEqualSquares
};
let discriminant = b * b - 4.0 * a * c;
let mut roots = Vec::new();
if discriminant >= 0.0 {
let sqrt_d = discriminant.sqrt();
let r1 = (-b + sqrt_d) / (2.0 * a);
let r2 = (-b - sqrt_d) / (2.0 * a);
if r1 > f64::EPSILON {
roots.push(r1);
}
if r2 > f64::EPSILON && (r1 - r2).abs() > f64::EPSILON {
roots.push(r2);
}
}
Ok(AlJabrSolution {
form,
roots,
coefficients: (a, b, c),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum KhayyamCubicType {
CubeEqualNumber,
CubeAndThingEqualNumber,
CubeAndSquareEqualNumber,
CubeEqualThingAndNumber,
CubeThingNumberEqualSquare,
}
pub fn classify_khayyam_cubic(
a3: f64,
a2: f64,
a1: f64,
a0: f64,
) -> Result<(KhayyamCubicType, Vec<f64>)> {
if a3.abs() < f64::EPSILON {
return Err(SankhyaError::ComputationError(
"leading coefficient is zero — not a cubic".into(),
));
}
let c = a2 / a3;
let b = a1 / a3;
let d = a0 / a3;
let cubic_type = if c.abs() < f64::EPSILON && b.abs() < f64::EPSILON {
KhayyamCubicType::CubeEqualNumber
} else if c.abs() < f64::EPSILON && d < 0.0 {
KhayyamCubicType::CubeAndThingEqualNumber
} else if b.abs() < f64::EPSILON && d < 0.0 {
KhayyamCubicType::CubeAndSquareEqualNumber
} else if c.abs() < f64::EPSILON && b < 0.0 {
KhayyamCubicType::CubeEqualThingAndNumber
} else {
KhayyamCubicType::CubeThingNumberEqualSquare
};
let f = |x: f64| a3 * x * x * x + a2 * x * x + a1 * x + a0;
let df = |x: f64| 3.0 * a3 * x * x + 2.0 * a2 * x + a1;
let mut roots = Vec::new();
for &x0 in &[0.0, 1.0, -1.0, 10.0, -10.0, 100.0, -100.0] {
let mut x = x0;
let mut converged = false;
for _ in 0..200 {
let fx = f(x);
let dfx = df(x);
if dfx.abs() < f64::EPSILON {
break;
}
let x_new = x - fx / dfx;
if (x_new - x).abs() < 1e-12 {
converged = true;
x = x_new;
break;
}
x = x_new;
}
if converged && f(x).abs() < 1e-8 {
let is_duplicate = roots.iter().any(|&r: &f64| (r - x).abs() < 1e-8);
if !is_duplicate {
roots.push(x);
}
}
}
if roots.is_empty() {
return Err(SankhyaError::ComputationError(
"Newton's method failed to find a real root".into(),
));
}
roots.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
Ok((cubic_type, roots))
}
pub fn complete_the_square(b: f64, c: f64) -> Result<(f64, f64)> {
if b <= 0.0 || c <= 0.0 {
return Err(SankhyaError::InvalidBase(
"al-Khwarizmi's completion requires positive b and c".into(),
));
}
let half_b = b / 2.0;
let completed_area = c + half_b * half_b;
let x = completed_area.sqrt() - half_b;
Ok((x, completed_area))
}
pub const HIJRI_EPOCH_JDN: f64 = 1_948_439.5;
pub const ISLAMIC_MONTH_DAYS: f64 = 29.530_588_86;
pub const HIJRI_30_YEAR_CYCLE_DAYS: u64 = 10_631;
const HIJRI_MONTH_DAYS: [u8; 12] = [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HijriMonth {
Muharram,
Safar,
RabiAlAwwal,
RabiAlThani,
JumadaAlUla,
JumadaAlThani,
Rajab,
Shaban,
Ramadan,
Shawwal,
DhulQadah,
DhulHijjah,
}
const HIJRI_MONTHS: [HijriMonth; 12] = [
HijriMonth::Muharram,
HijriMonth::Safar,
HijriMonth::RabiAlAwwal,
HijriMonth::RabiAlThani,
HijriMonth::JumadaAlUla,
HijriMonth::JumadaAlThani,
HijriMonth::Rajab,
HijriMonth::Shaban,
HijriMonth::Ramadan,
HijriMonth::Shawwal,
HijriMonth::DhulQadah,
HijriMonth::DhulHijjah,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct HijriDate {
pub year: i64,
pub month: HijriMonth,
pub day: u8,
}
#[must_use]
#[inline]
pub fn hijri_is_leap(year: i64) -> bool {
let y = year.rem_euclid(30);
matches!(y, 2 | 5 | 7 | 10 | 13 | 16 | 18 | 21 | 24 | 26 | 29)
}
#[must_use]
#[inline]
pub fn hijri_year_days(year: i64) -> u16 {
if hijri_is_leap(year) { 355 } else { 354 }
}
#[must_use]
pub fn jdn_to_hijri(jdn: f64) -> HijriDate {
let days_since_epoch = (jdn - HIJRI_EPOCH_JDN).floor() as i64;
let cycles = days_since_epoch.div_euclid(HIJRI_30_YEAR_CYCLE_DAYS as i64);
let mut remaining = days_since_epoch.rem_euclid(HIJRI_30_YEAR_CYCLE_DAYS as i64);
let mut year = cycles * 30 + 1;
loop {
let yd = i64::from(hijri_year_days(year));
if remaining < yd {
break;
}
remaining -= yd;
year += 1;
}
let mut month_idx = 0;
loop {
let md = if month_idx == 11 && hijri_is_leap(year) {
30i64 } else {
i64::from(HIJRI_MONTH_DAYS[month_idx])
};
if remaining < md {
break;
}
remaining -= md;
month_idx += 1;
if month_idx >= 12 {
month_idx = 11;
break;
}
}
HijriDate {
year,
month: HIJRI_MONTHS[month_idx],
day: remaining as u8 + 1, }
}
pub fn hijri_to_jdn(date: &HijriDate) -> Result<f64> {
let month_idx = HIJRI_MONTHS
.iter()
.position(|&m| m == date.month)
.unwrap_or(0);
let max_day = if month_idx == 11 && hijri_is_leap(date.year) {
30
} else {
HIJRI_MONTH_DAYS[month_idx]
};
if date.day == 0 || date.day > max_day {
return Err(SankhyaError::InvalidDate(format!(
"day {} out of range for {:?} (max {max_day})",
date.day, date.month
)));
}
let mut days: i64 = 0;
let base_year = 1i64;
for y in base_year..date.year {
days += i64::from(hijri_year_days(y));
}
for (m, &md) in HIJRI_MONTH_DAYS.iter().enumerate().take(month_idx) {
days += if m == 11 && hijri_is_leap(date.year) {
30
} else {
i64::from(md)
};
}
days += i64::from(date.day - 1);
Ok(HIJRI_EPOCH_JDN + days as f64)
}
impl core::fmt::Display for HijriDate {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} {:?} {} AH", self.day, self.month, self.year)
}
}
impl core::fmt::Display for HijriMonth {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let name = match self {
Self::Muharram => "Muharram",
Self::Safar => "Safar",
Self::RabiAlAwwal => "Rabi al-Awwal",
Self::RabiAlThani => "Rabi al-Thani",
Self::JumadaAlUla => "Jumada al-Ula",
Self::JumadaAlThani => "Jumada al-Thani",
Self::Rajab => "Rajab",
Self::Shaban => "Sha'ban",
Self::Ramadan => "Ramadan",
Self::Shawwal => "Shawwal",
Self::DhulQadah => "Dhul Qa'dah",
Self::DhulHijjah => "Dhul Hijjah",
};
write!(f, "{name}")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn al_jabr_linear() {
let sol = solve_al_jabr(0.0, 2.0, -6.0).unwrap();
assert_eq!(sol.roots.len(), 1);
assert!((sol.roots[0] - 3.0).abs() < 1e-10);
}
#[test]
fn al_jabr_squares_and_roots_equal_number() {
let sol = solve_al_jabr(1.0, 10.0, -39.0).unwrap();
assert!(sol.roots.contains(&3.0) || sol.roots.iter().any(|&r| (r - 3.0).abs() < 1e-10));
}
#[test]
fn al_jabr_two_positive_roots() {
let sol = solve_al_jabr(1.0, -5.0, 6.0).unwrap();
assert_eq!(sol.roots.len(), 2);
let mut roots = sol.roots.clone();
roots.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!((roots[0] - 2.0).abs() < 1e-10);
assert!((roots[1] - 3.0).abs() < 1e-10);
}
#[test]
fn al_jabr_no_positive_roots() {
let sol = solve_al_jabr(1.0, 1.0, 1.0).unwrap();
assert!(sol.roots.is_empty());
}
#[test]
fn al_jabr_all_zero_errors() {
assert!(solve_al_jabr(0.0, 0.0, 0.0).is_err());
}
#[test]
fn complete_square_al_khwarizmi_example() {
let (x, _area) = complete_the_square(10.0, 39.0).unwrap();
assert!((x - 3.0).abs() < 1e-10);
}
#[test]
fn complete_square_negative_errors() {
assert!(complete_the_square(-1.0, 5.0).is_err());
assert!(complete_the_square(1.0, -5.0).is_err());
}
#[test]
fn khayyam_cube_equal_number() {
let (ctype, roots) = classify_khayyam_cubic(1.0, 0.0, 0.0, -8.0).unwrap();
assert_eq!(ctype, KhayyamCubicType::CubeEqualNumber);
assert!(roots.iter().any(|&r| (r - 2.0).abs() < 1e-6));
}
#[test]
fn khayyam_cube_and_thing() {
let (_ctype, roots) = classify_khayyam_cubic(1.0, 0.0, 6.0, -20.0).unwrap();
assert!(roots.iter().any(|&r| (r - 2.0).abs() < 1e-6));
}
#[test]
fn khayyam_not_cubic_errors() {
assert!(classify_khayyam_cubic(0.0, 1.0, 2.0, 3.0).is_err());
}
#[test]
fn hijri_leap_years() {
assert!(hijri_is_leap(2));
assert!(hijri_is_leap(5));
assert!(hijri_is_leap(29));
assert!(!hijri_is_leap(1));
assert!(!hijri_is_leap(3));
assert!(!hijri_is_leap(30));
}
#[test]
fn hijri_year_length() {
assert_eq!(hijri_year_days(1), 354);
assert_eq!(hijri_year_days(2), 355);
}
#[test]
fn hijri_epoch_roundtrip() {
let date = jdn_to_hijri(HIJRI_EPOCH_JDN);
assert_eq!(date.year, 1);
assert_eq!(date.month, HijriMonth::Muharram);
assert_eq!(date.day, 1);
let jdn = hijri_to_jdn(&date).unwrap();
assert!((jdn - HIJRI_EPOCH_JDN).abs() < 0.5);
}
#[test]
fn hijri_known_date() {
let jdn = HIJRI_EPOCH_JDN + 236.0;
let date = jdn_to_hijri(jdn);
assert_eq!(date.month, HijriMonth::Ramadan);
assert_eq!(date.day, 1);
}
#[test]
fn hijri_to_jdn_invalid_day() {
let date = HijriDate {
year: 1,
month: HijriMonth::Muharram,
day: 31, };
assert!(hijri_to_jdn(&date).is_err());
}
#[test]
fn hijri_30_year_cycle_consistent() {
let mut total: u64 = 0;
for y in 1..=30 {
total += u64::from(hijri_year_days(y));
}
assert_eq!(total, HIJRI_30_YEAR_CYCLE_DAYS);
}
#[test]
fn serde_roundtrip_hijri_date() {
let date = jdn_to_hijri(HIJRI_EPOCH_JDN + 1000.0);
let json = serde_json::to_string(&date).unwrap();
let back: HijriDate = serde_json::from_str(&json).unwrap();
assert_eq!(date, back);
}
#[test]
fn serde_roundtrip_al_jabr_solution() {
let sol = solve_al_jabr(1.0, -5.0, 6.0).unwrap();
let json = serde_json::to_string(&sol).unwrap();
let back: AlJabrSolution = serde_json::from_str(&json).unwrap();
assert_eq!(sol, back);
}
}