use serde::{Deserialize, Serialize};
use crate::error::{Result, SankhyaError};
#[must_use]
pub fn to_vigesimal(mut n: u64) -> Vec<u8> {
if n == 0 {
return vec![0];
}
let mut digits = Vec::new();
while n > 0 {
digits.push((n % 20) as u8);
n /= 20;
}
digits.reverse();
digits
}
pub fn from_vigesimal(digits: &[u8]) -> Result<u64> {
let mut result: u64 = 0;
for &d in digits {
if d >= 20 {
return Err(SankhyaError::InvalidBase(format!(
"vigesimal digit {d} out of range 0..20"
)));
}
result = result
.checked_mul(20)
.and_then(|r| r.checked_add(u64::from(d)))
.ok_or_else(|| SankhyaError::OverflowError("vigesimal conversion overflow".into()))?;
}
Ok(result)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MayanNumeral {
pub dots: u8,
pub bars: u8,
pub shell: bool,
}
impl MayanNumeral {
pub fn from_value(value: u8) -> Result<Self> {
if value > 19 {
return Err(SankhyaError::InvalidBase(format!(
"Mayan numeral value {value} out of range 0..19"
)));
}
if value == 0 {
return Ok(Self {
dots: 0,
bars: 0,
shell: true,
});
}
Ok(Self {
dots: value % 5,
bars: value / 5,
shell: false,
})
}
#[must_use]
#[inline]
pub fn value(self) -> u8 {
if self.shell {
0
} else {
self.bars * 5 + self.dots
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LongCount {
pub baktun: u32,
pub katun: u32,
pub tun: u32,
pub uinal: u32,
pub kin: u32,
}
pub const EPOCH_JDN: u64 = 584_283;
impl LongCount {
pub fn new(baktun: u32, katun: u32, tun: u32, uinal: u32, kin: u32) -> Result<Self> {
if katun >= 20 {
return Err(SankhyaError::InvalidDate(format!(
"katun {katun} out of range 0..20"
)));
}
if tun >= 20 {
return Err(SankhyaError::InvalidDate(format!(
"tun {tun} out of range 0..20"
)));
}
if uinal >= 18 {
return Err(SankhyaError::InvalidDate(format!(
"uinal {uinal} out of range 0..18"
)));
}
if kin >= 20 {
return Err(SankhyaError::InvalidDate(format!(
"kin {kin} out of range 0..20"
)));
}
Ok(Self {
baktun,
katun,
tun,
uinal,
kin,
})
}
pub fn from_days(mut days: u64) -> Result<Self> {
let baktun = days / 144_000;
days %= 144_000;
let katun = days / 7_200;
days %= 7_200;
let tun = days / 360;
days %= 360;
let uinal = days / 20;
let kin = days % 20;
Ok(Self {
baktun: u32::try_from(baktun).map_err(|_| {
SankhyaError::OverflowError(format!(
"day count {days} exceeds maximum representable baktun"
))
})?,
katun: katun as u32,
tun: tun as u32,
uinal: uinal as u32,
kin: kin as u32,
})
}
#[must_use]
#[inline]
pub fn to_days(self) -> u64 {
u64::from(self.baktun) * 144_000
+ u64::from(self.katun) * 7_200
+ u64::from(self.tun) * 360
+ u64::from(self.uinal) * 20
+ u64::from(self.kin)
}
pub fn from_julian_day(jdn: u64) -> Result<Self> {
if jdn < EPOCH_JDN {
return Err(SankhyaError::InvalidDate(format!(
"JDN {jdn} is before the Mayan epoch (JDN {EPOCH_JDN})"
)));
}
Self::from_days(jdn - EPOCH_JDN)
}
#[must_use]
#[inline]
pub fn to_julian_day(self) -> u64 {
self.to_days() + EPOCH_JDN
}
}
impl core::fmt::Display for LongCount {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}.{}.{}.{}.{}",
self.baktun, self.katun, self.tun, self.uinal, self.kin
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DaySign {
Imix,
Ik,
Akbal,
Kan,
Chicchan,
Cimi,
Manik,
Lamat,
Muluc,
Oc,
Chuen,
Eb,
Ben,
Ix,
Men,
Cib,
Caban,
Etznab,
Cauac,
Ahau,
}
const DAY_SIGNS: [DaySign; 20] = [
DaySign::Imix,
DaySign::Ik,
DaySign::Akbal,
DaySign::Kan,
DaySign::Chicchan,
DaySign::Cimi,
DaySign::Manik,
DaySign::Lamat,
DaySign::Muluc,
DaySign::Oc,
DaySign::Chuen,
DaySign::Eb,
DaySign::Ben,
DaySign::Ix,
DaySign::Men,
DaySign::Cib,
DaySign::Caban,
DaySign::Etznab,
DaySign::Cauac,
DaySign::Ahau,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tzolkin {
pub number: u8,
pub day_sign: DaySign,
}
impl Tzolkin {
#[must_use]
pub fn from_days(days: u64) -> Self {
let number = ((days + 3) % 13 + 1) as u8;
let sign_index = ((days + 19) % 20) as usize;
Self {
number,
day_sign: DAY_SIGNS[sign_index],
}
}
}
impl core::fmt::Display for Tzolkin {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} {:?}", self.number, self.day_sign)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HaabMonth {
Pop,
Wo,
Sip,
Sotz,
Sek,
Xul,
Yaxkin,
Mol,
Chen,
Yax,
Sak,
Keh,
Mak,
Kankin,
Muan,
Pax,
Kayab,
Kumku,
Wayeb,
}
const HAAB_MONTHS: [HaabMonth; 19] = [
HaabMonth::Pop,
HaabMonth::Wo,
HaabMonth::Sip,
HaabMonth::Sotz,
HaabMonth::Sek,
HaabMonth::Xul,
HaabMonth::Yaxkin,
HaabMonth::Mol,
HaabMonth::Chen,
HaabMonth::Yax,
HaabMonth::Sak,
HaabMonth::Keh,
HaabMonth::Mak,
HaabMonth::Kankin,
HaabMonth::Muan,
HaabMonth::Pax,
HaabMonth::Kayab,
HaabMonth::Kumku,
HaabMonth::Wayeb,
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Haab {
pub day: u8,
pub month: HaabMonth,
}
impl Haab {
#[must_use]
pub fn from_days(days: u64) -> Self {
let haab_day = ((days + 348) % 365) as u16;
let month_index = (haab_day / 20) as usize;
let day = (haab_day % 20) as u8;
Self {
day,
month: HAAB_MONTHS[month_index],
}
}
}
impl core::fmt::Display for Haab {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{} {:?}", self.day, self.month)
}
}
#[must_use]
#[inline]
pub fn calendar_round(days: u64) -> (Tzolkin, Haab) {
(Tzolkin::from_days(days), Haab::from_days(days))
}
pub const CALENDAR_ROUND_DAYS: u64 = 18_980;
pub fn find_calendar_round(
tzolkin_number: u8,
tzolkin_sign: DaySign,
haab_day: u8,
haab_month: HaabMonth,
start_day: u64,
) -> Result<u64> {
if !(1..=13).contains(&tzolkin_number) {
return Err(SankhyaError::InvalidDate(format!(
"Tzolkin number {tzolkin_number} out of range 1..13"
)));
}
let haab_max = if haab_month == HaabMonth::Wayeb {
4
} else {
19
};
if haab_day > haab_max {
return Err(SankhyaError::InvalidDate(format!(
"Haab day {haab_day} out of range for {haab_month:?} (max {haab_max})"
)));
}
for offset in 0..CALENDAR_ROUND_DAYS {
let day = start_day + offset;
let tz = Tzolkin::from_days(day);
let hb = Haab::from_days(day);
if tz.number == tzolkin_number
&& tz.day_sign == tzolkin_sign
&& hb.day == haab_day
&& hb.month == haab_month
{
return Ok(day);
}
}
Err(SankhyaError::ComputationError(
"no matching Calendar Round date found within one cycle".into(),
))
}
pub fn find_tzolkin(tzolkin_number: u8, tzolkin_sign: DaySign, start_day: u64) -> Result<u64> {
if !(1..=13).contains(&tzolkin_number) {
return Err(SankhyaError::InvalidDate(format!(
"Tzolkin number {tzolkin_number} out of range 1..13"
)));
}
for offset in 0..260u64 {
let day = start_day + offset;
let tz = Tzolkin::from_days(day);
if tz.number == tzolkin_number && tz.day_sign == tzolkin_sign {
return Ok(day);
}
}
Err(SankhyaError::ComputationError(
"no matching Tzolkin date found within one cycle".into(),
))
}
pub const VENUS_SYNODIC_PERIOD: f64 = 583.92;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum VenusPhase {
MorningStar,
SuperiorConjunction,
EveningStar,
InferiorConjunction,
}
#[must_use]
pub fn venus_phase(days_from_epoch: u64) -> VenusPhase {
let phase_day = (days_from_epoch % 584) as u16;
if phase_day < 236 {
VenusPhase::MorningStar
} else if phase_day < 236 + 90 {
VenusPhase::SuperiorConjunction
} else if phase_day < 236 + 90 + 250 {
VenusPhase::EveningStar
} else {
VenusPhase::InferiorConjunction
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vigesimal_zero() {
assert_eq!(to_vigesimal(0), vec![0]);
assert_eq!(from_vigesimal(&[0]).unwrap(), 0);
}
#[test]
fn vigesimal_roundtrip() {
for n in [1, 19, 20, 399, 400, 8000, 160_000, 1_000_000] {
let digits = to_vigesimal(n);
assert_eq!(from_vigesimal(&digits).unwrap(), n, "failed for {n}");
}
}
#[test]
fn mayan_numeral_values() {
let zero = MayanNumeral::from_value(0).unwrap();
assert!(zero.shell);
assert_eq!(zero.value(), 0);
let thirteen = MayanNumeral::from_value(13).unwrap();
assert_eq!(thirteen.dots, 3);
assert_eq!(thirteen.bars, 2);
assert_eq!(thirteen.value(), 13);
}
#[test]
fn long_count_creation_date() {
let lc = LongCount::from_days(0).unwrap();
assert_eq!(lc.to_days(), 0);
assert_eq!(lc.baktun, 0);
}
#[test]
fn long_count_dec_21_2012() {
let days = 13u64 * 144_000;
let lc = LongCount::from_days(days).unwrap();
assert_eq!(lc.baktun, 13);
assert_eq!(lc.katun, 0);
assert_eq!(lc.tun, 0);
assert_eq!(lc.uinal, 0);
assert_eq!(lc.kin, 0);
assert_eq!(lc.to_days(), days);
}
#[test]
fn tzolkin_at_creation() {
let tz = Tzolkin::from_days(0);
assert_eq!(tz.number, 4);
assert_eq!(tz.day_sign, DaySign::Ahau);
}
#[test]
fn haab_at_creation() {
let haab = Haab::from_days(0);
assert_eq!(haab.day, 8);
assert_eq!(haab.month, HaabMonth::Kumku);
}
#[test]
fn venus_cycle_length() {
assert_eq!(236 + 90 + 250 + 8, 584);
}
#[test]
fn find_calendar_round_at_creation() {
let day = find_calendar_round(4, DaySign::Ahau, 8, HaabMonth::Kumku, 0).unwrap();
assert_eq!(day, 0);
}
#[test]
fn find_calendar_round_next_cycle() {
let day = find_calendar_round(4, DaySign::Ahau, 8, HaabMonth::Kumku, 1).unwrap();
assert_eq!(day, CALENDAR_ROUND_DAYS);
}
#[test]
fn find_calendar_round_invalid_tzolkin() {
assert!(find_calendar_round(0, DaySign::Ahau, 0, HaabMonth::Pop, 0).is_err());
assert!(find_calendar_round(14, DaySign::Ahau, 0, HaabMonth::Pop, 0).is_err());
}
#[test]
fn find_tzolkin_at_creation() {
let day = find_tzolkin(4, DaySign::Ahau, 0).unwrap();
assert_eq!(day, 0);
}
#[test]
fn find_tzolkin_next_occurrence() {
let day = find_tzolkin(4, DaySign::Ahau, 1).unwrap();
assert_eq!(day, 260);
}
}