use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use crate::error::{Result, SankhyaError};
#[must_use]
pub fn to_sexagesimal(mut n: u64) -> Vec<u8> {
if n == 0 {
return vec![0];
}
let mut digits = Vec::new();
while n > 0 {
digits.push((n % 60) as u8);
n /= 60;
}
digits.reverse();
digits
}
pub fn from_sexagesimal(digits: &[u8]) -> Result<u64> {
let mut result: u64 = 0;
for &d in digits {
if d >= 60 {
return Err(SankhyaError::InvalidBase(format!(
"sexagesimal digit {d} out of range 0..60"
)));
}
result = result
.checked_mul(60)
.and_then(|r| r.checked_add(u64::from(d)))
.ok_or_else(|| SankhyaError::OverflowError("sexagesimal conversion overflow".into()))?;
}
Ok(result)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BabylonianNumeral {
pub tens: u8,
pub units: u8,
}
impl BabylonianNumeral {
pub fn from_value(value: u8) -> Result<Self> {
if value >= 60 {
return Err(SankhyaError::InvalidBase(format!(
"Babylonian digit {value} out of range 0..60"
)));
}
Ok(Self {
tens: value / 10,
units: value % 10,
})
}
#[must_use]
#[inline]
pub fn value(self) -> u8 {
self.tens * 10 + self.units
}
}
pub const SAROS_DAYS: f64 = 6585.3211;
#[must_use]
#[inline]
pub fn saros_cycle(eclipse_jdn: f64) -> f64 {
eclipse_jdn + SAROS_DAYS
}
pub const SYNODIC_MONTH_DAYS: f64 = 29.530_594;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BabylonianMonth {
Nisannu,
Ayaru,
Simanu,
Dumuzu,
Abu,
Ululu,
Tashritu,
Arahsamna,
Kislimu,
Tebetu,
Shabatu,
Addaru,
}
const BABYLONIAN_MONTHS: [BabylonianMonth; 12] = [
BabylonianMonth::Nisannu,
BabylonianMonth::Ayaru,
BabylonianMonth::Simanu,
BabylonianMonth::Dumuzu,
BabylonianMonth::Abu,
BabylonianMonth::Ululu,
BabylonianMonth::Tashritu,
BabylonianMonth::Arahsamna,
BabylonianMonth::Kislimu,
BabylonianMonth::Tebetu,
BabylonianMonth::Shabatu,
BabylonianMonth::Addaru,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct BabylonianDate {
pub year: i64,
pub month: BabylonianMonth,
pub day: u8,
}
pub const BABYLONIAN_EPOCH_JDN: f64 = 1_607_738.5;
pub const BABYLONIAN_YEAR_DAYS: u16 = 354;
const BABYLONIAN_MONTH_DAYS: [u8; 12] = [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29];
#[must_use]
pub fn jdn_to_babylonian(jdn: f64) -> BabylonianDate {
let days_since_epoch = (jdn - BABYLONIAN_EPOCH_JDN).floor() as i64;
let year_days = i64::from(BABYLONIAN_YEAR_DAYS);
let years = days_since_epoch.div_euclid(year_days);
let mut remaining = days_since_epoch.rem_euclid(year_days);
let year = years + 1;
let mut month_idx = 0;
for (i, &md) in BABYLONIAN_MONTH_DAYS.iter().enumerate() {
if remaining < i64::from(md) {
month_idx = i;
break;
}
remaining -= i64::from(md);
if i == 11 {
month_idx = 11;
}
}
BabylonianDate {
year,
month: BABYLONIAN_MONTHS[month_idx],
day: remaining as u8 + 1,
}
}
pub fn babylonian_to_jdn(date: &BabylonianDate) -> Result<f64> {
let month_idx = BABYLONIAN_MONTHS
.iter()
.position(|&m| m == date.month)
.unwrap_or(0);
let max_day = BABYLONIAN_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::from(BABYLONIAN_YEAR_DAYS) * (date.year - 1);
for &md in &BABYLONIAN_MONTH_DAYS[..month_idx] {
days += i64::from(md);
}
days += i64::from(date.day - 1);
Ok(BABYLONIAN_EPOCH_JDN + days as f64)
}
#[must_use]
pub fn synodic_months_between(jdn1: f64, jdn2: f64) -> (u64, f64) {
let elapsed = (jdn2 - jdn1).abs();
let months = (elapsed / SYNODIC_MONTH_DAYS).floor();
let remainder = elapsed - months * SYNODIC_MONTH_DAYS;
(months as u64, remainder)
}
impl core::fmt::Display for BabylonianDate {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} {:?}, Year {} SE", self.day, self.month, self.year)
}
}
impl core::fmt::Display for BabylonianMonth {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let name = match self {
Self::Nisannu => "Nisannu",
Self::Ayaru => "Ayaru",
Self::Simanu => "Simanu",
Self::Dumuzu => "Dumuzu",
Self::Abu => "Abu",
Self::Ululu => "Ululu",
Self::Tashritu => "Tashritu",
Self::Arahsamna => "Arahsamna",
Self::Kislimu => "Kislimu",
Self::Tebetu => "Tebetu",
Self::Shabatu => "Shabatu",
Self::Addaru => "Addaru",
};
write!(f, "{name}")
}
}
#[must_use]
pub fn reciprocal_table() -> BTreeMap<u64, Vec<u8>> {
let pairs: &[(u64, &[u8])] = &[
(2, &[30]), (3, &[20]), (4, &[15]), (5, &[12]), (6, &[10]), (8, &[7, 30]), (9, &[6, 40]), (10, &[6]), (12, &[5]), (15, &[4]), (16, &[3, 45]), (18, &[3, 20]), (20, &[3]), (24, &[2, 30]), (25, &[2, 24]), (27, &[2, 13, 20]), (30, &[2]), (32, &[1, 52, 30]), (36, &[1, 40]), (40, &[1, 30]), (45, &[1, 20]), (48, &[1, 15]), (50, &[1, 12]), (54, &[1, 6, 40]), (60, &[1]), (64, &[0, 56, 15]), (72, &[0, 50]), (80, &[0, 45]), (81, &[0, 44, 26, 40]), ];
let mut table = BTreeMap::new();
for &(n, recip) in pairs {
table.insert(n, recip.to_vec());
}
table
}
#[must_use]
pub fn generate_plimpton_triples() -> Vec<(u64, u64, u64)> {
vec![
(119, 120, 169),
(3367, 3456, 4825),
(4601, 4800, 6649),
(12709, 13500, 18541),
(65, 72, 97),
(319, 360, 481),
(2291, 2700, 3541),
(799, 960, 1249),
(481, 600, 769),
(4961, 6480, 8161),
(45, 60, 75),
(1679, 2400, 2929),
(161, 240, 289),
(1771, 2700, 3229),
(56, 90, 106),
]
}
pub fn babylonian_sqrt(n: f64, iterations: u32) -> Result<f64> {
if n.is_nan() || n.is_infinite() || n < 0.0 {
return Err(SankhyaError::ComputationError(
"cannot compute square root of negative, NaN, or infinite number".into(),
));
}
if n == 0.0 {
return Ok(0.0);
}
if iterations == 0 {
return Err(SankhyaError::InvalidBase(
"iterations must be at least 1".into(),
));
}
let mut x = if n < 2.0 { 1.0 } else { n / 2.0 };
for _ in 0..iterations {
x = (x + n / x) / 2.0;
}
Ok(x)
}
#[cfg(feature = "varna")]
pub fn cuneiform_digit(digit: u8) -> Result<String> {
if digit >= 60 {
return Err(SankhyaError::InvalidBase(format!(
"cuneiform digit {digit} out of range 0..60"
)));
}
if digit == 0 {
return Ok(" ".into());
}
let system = varna::script::numerals::babylonian_sexagesimal();
let tens = digit / 10;
let units = digit % 10;
let mut result = String::new();
if tens > 0 {
let mut remaining_tens = tens;
for &val in &[30u8, 20, 10] {
if remaining_tens * 10 >= val
&& let Some(ch) = system.char_for(u32::from(val))
{
result.push_str(ch);
remaining_tens -= val / 10;
}
if remaining_tens == 0 {
break;
}
}
}
if units > 0
&& let Some(ch) = system.char_for(u32::from(units))
{
result.push_str(ch);
}
Ok(result)
}
#[cfg(feature = "varna")]
pub fn to_cuneiform(n: u64) -> Result<String> {
let digits = to_sexagesimal(n);
let mut parts = Vec::with_capacity(digits.len());
for &d in &digits {
parts.push(cuneiform_digit(d)?);
}
Ok(parts.join("ยท"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sexagesimal_zero() {
assert_eq!(to_sexagesimal(0), vec![0]);
assert_eq!(from_sexagesimal(&[0]).unwrap(), 0);
}
#[test]
fn sexagesimal_roundtrip() {
for n in [1, 59, 60, 3599, 3600, 216_000, 1_000_000] {
let digits = to_sexagesimal(n);
assert_eq!(from_sexagesimal(&digits).unwrap(), n, "failed for {n}");
}
}
#[test]
fn babylonian_numeral_value() {
let n = BabylonianNumeral::from_value(42).unwrap();
assert_eq!(n.tens, 4);
assert_eq!(n.units, 2);
assert_eq!(n.value(), 42);
}
#[test]
fn plimpton_triples_valid() {
let triples = generate_plimpton_triples();
assert_eq!(triples.len(), 15);
for (a, b, c) in &triples {
assert_eq!(a * a + b * b, c * c, "invalid triple: ({a}, {b}, {c})");
}
}
#[test]
fn sqrt_2_accuracy() {
let result = babylonian_sqrt(2.0, 10).unwrap();
assert!((result - std::f64::consts::SQRT_2).abs() < 1e-15);
}
#[test]
fn saros_cycle_test() {
let next = saros_cycle(2451545.0); assert!((next - (2451545.0 + SAROS_DAYS)).abs() < 1e-10);
}
#[test]
fn babylonian_epoch_roundtrip() {
let date = jdn_to_babylonian(BABYLONIAN_EPOCH_JDN);
assert_eq!(date.year, 1);
assert_eq!(date.month, BabylonianMonth::Nisannu);
assert_eq!(date.day, 1);
let jdn = babylonian_to_jdn(&date).unwrap();
assert!((jdn - BABYLONIAN_EPOCH_JDN).abs() < 0.5);
}
#[test]
fn babylonian_year_is_354() {
let total: u16 = BABYLONIAN_MONTH_DAYS.iter().map(|&d| u16::from(d)).sum();
assert_eq!(total, BABYLONIAN_YEAR_DAYS);
}
#[test]
fn babylonian_month_alternates() {
for (i, &d) in BABYLONIAN_MONTH_DAYS.iter().enumerate() {
if i % 2 == 0 {
assert_eq!(d, 30);
} else {
assert_eq!(d, 29);
}
}
}
#[test]
fn babylonian_to_jdn_invalid_day() {
let date = BabylonianDate {
year: 1,
month: BabylonianMonth::Ayaru, day: 30,
};
assert!(babylonian_to_jdn(&date).is_err());
}
#[test]
fn synodic_months_one_year() {
let (months, _rem) = synodic_months_between(0.0, 365.25);
assert_eq!(months, 12);
}
#[test]
fn serde_roundtrip_babylonian_date() {
let date = jdn_to_babylonian(BABYLONIAN_EPOCH_JDN + 500.0);
let json = serde_json::to_string(&date).unwrap();
let back: BabylonianDate = serde_json::from_str(&json).unwrap();
assert_eq!(date, back);
}
#[cfg(feature = "varna")]
mod cuneiform_tests {
use super::*;
#[test]
fn cuneiform_digit_units() {
let s = cuneiform_digit(1).unwrap();
assert_eq!(s, "๐");
let s = cuneiform_digit(9).unwrap();
assert_eq!(s, "๐");
}
#[test]
fn cuneiform_digit_tens() {
let s = cuneiform_digit(10).unwrap();
assert_eq!(s, "๐");
let s = cuneiform_digit(30).unwrap();
assert_eq!(s, "๐");
}
#[test]
fn cuneiform_digit_composite() {
let s = cuneiform_digit(42).unwrap();
assert!(s.contains("๐"));
assert!(s.contains("๐"));
}
#[test]
fn cuneiform_digit_zero() {
assert_eq!(cuneiform_digit(0).unwrap(), " ");
}
#[test]
fn cuneiform_digit_out_of_range() {
assert!(cuneiform_digit(60).is_err());
}
#[test]
fn to_cuneiform_basic() {
let s = to_cuneiform(60).unwrap();
assert!(s.contains('ยท'));
assert!(s.contains("๐"));
}
}
}