sankhya 1.0.0

sankhya — Ancient mathematical systems: Mayan, Babylonian, Egyptian, Vedic, Chinese, Greek, Roman, Islamic, and cross-civilizational epoch correlation
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
//! Mayan mathematics and calendar systems.
//!
//! Implements the vigesimal (base-20) number system, Long Count calendar,
//! Tzolkin (sacred 260-day cycle), Haab (solar 365-day calendar),
//! Calendar Round, and Venus synodic cycle computations.
//!
//! # Historical Context
//!
//! The Maya developed one of the most sophisticated mathematical and
//! astronomical systems in the ancient world. Their vigesimal number
//! system included a true zero (the shell glyph) centuries before
//! the concept appeared in the Old World. The Long Count calendar
//! tracked time from a mythological creation date of August 11, 3114 BCE
//! (GMT correlation constant 584283).

use serde::{Deserialize, Serialize};

use crate::error::{Result, SankhyaError};

// ---------------------------------------------------------------------------
// Vigesimal (base-20) number system
// ---------------------------------------------------------------------------

/// Convert a decimal number to vigesimal (base-20) digits, most significant first.
///
/// Returns `[0]` for zero (the Maya had an explicit zero glyph — the shell).
/// Use [`MayanNumeral`] for glyph-level representation.
#[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
}

/// Convert vigesimal (base-20) digits back to a decimal number.
///
/// Digits are most-significant first. Each digit must be in 0..20.
///
/// # Errors
///
/// Returns [`SankhyaError::InvalidBase`] if any digit is >= 20.
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)
}

// ---------------------------------------------------------------------------
// Mayan numeral glyphs
// ---------------------------------------------------------------------------

/// A single Mayan numeral (0-19) represented as dots and bars.
///
/// - A dot represents 1 (up to 4 dots)
/// - A bar represents 5 (up to 3 bars)
/// - A shell glyph represents 0
///
/// For example, 13 = 3 dots + 2 bars, 0 = shell.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct MayanNumeral {
    /// Number of dots (0-4), each worth 1.
    pub dots: u8,
    /// Number of bars (0-3), each worth 5.
    pub bars: u8,
    /// True if this is a zero (shell glyph).
    pub shell: bool,
}

impl MayanNumeral {
    /// Create a Mayan numeral from a value 0-19.
    ///
    /// # Errors
    ///
    /// Returns [`SankhyaError::InvalidBase`] if value > 19.
    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,
        })
    }

    /// The decimal value of this numeral (0-19).
    #[must_use]
    #[inline]
    pub fn value(self) -> u8 {
        if self.shell {
            0
        } else {
            self.bars * 5 + self.dots
        }
    }
}

// ---------------------------------------------------------------------------
// Long Count calendar
// ---------------------------------------------------------------------------

/// The Mayan Long Count calendar date.
///
/// Composed of five place-value positions:
/// - kin = 1 day
/// - uinal = 20 kin = 20 days
/// - tun = 18 uinal = 360 days
/// - katun = 20 tun = 7,200 days
/// - baktun = 20 katun = 144,000 days
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct LongCount {
    /// Baktun (144,000 days each).
    pub baktun: u32,
    /// Katun (7,200 days each).
    pub katun: u32,
    /// Tun (360 days each).
    pub tun: u32,
    /// Uinal (20 days each).
    pub uinal: u32,
    /// Kin (1 day each).
    pub kin: u32,
}

/// Julian Day Number of the Mayan creation date (August 11, 3114 BCE).
///
/// This is the GMT (Goodman-Martinez-Thompson) correlation constant,
/// the most widely accepted correlation between the Mayan and Western calendars.
pub const EPOCH_JDN: u64 = 584_283;

impl LongCount {
    /// Create a Long Count from its component periods.
    ///
    /// # Errors
    ///
    /// Returns [`SankhyaError::InvalidDate`] if katun >= 20, tun >= 20,
    /// uinal >= 18, or kin >= 20.
    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,
        })
    }

    /// Convert a day count (days since the Mayan creation date) to Long Count.
    ///
    /// # Errors
    ///
    /// Returns [`SankhyaError::OverflowError`] if the day count produces a
    /// baktun value that exceeds `u32::MAX`.
    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,
        })
    }

    /// Convert this Long Count to a day count (days since creation date).
    #[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)
    }

    /// Convert a Julian Day Number to a Mayan Long Count.
    ///
    /// # Errors
    ///
    /// Returns [`SankhyaError::InvalidDate`] if `jdn` is before the Mayan epoch.
    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)
    }

    /// Convert this Long Count to a Julian Day Number.
    #[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
        )
    }
}

// ---------------------------------------------------------------------------
// Tzolkin (sacred 260-day cycle)
// ---------------------------------------------------------------------------

/// The 20 day signs of the Tzolkin cycle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum DaySign {
    /// Imix (Water Lily / Crocodile)
    Imix,
    /// Ik (Wind)
    Ik,
    /// Akbal (Night / Darkness)
    Akbal,
    /// Kan (Seed / Corn)
    Kan,
    /// Chicchan (Serpent)
    Chicchan,
    /// Cimi (Death)
    Cimi,
    /// Manik (Deer / Hand)
    Manik,
    /// Lamat (Star / Venus)
    Lamat,
    /// Muluc (Water / Offering)
    Muluc,
    /// Oc (Dog)
    Oc,
    /// Chuen (Monkey)
    Chuen,
    /// Eb (Grass / Road)
    Eb,
    /// Ben (Reed)
    Ben,
    /// Ix (Jaguar)
    Ix,
    /// Men (Eagle)
    Men,
    /// Cib (Vulture / Owl)
    Cib,
    /// Caban (Earth)
    Caban,
    /// Etznab (Flint / Knife)
    Etznab,
    /// Cauac (Storm / Rain)
    Cauac,
    /// Ahau (Lord / Sun)
    Ahau,
}

/// Ordered list of all 20 Tzolkin day signs.
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,
];

/// A Tzolkin date (sacred 260-day cycle).
///
/// Composed of a number (1-13) cycling with one of 20 day signs.
/// The full cycle is lcm(13, 20) = 260 days.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Tzolkin {
    /// The trecena number (1-13).
    pub number: u8,
    /// The day sign (one of 20).
    pub day_sign: DaySign,
}

impl Tzolkin {
    /// Compute the Tzolkin date for a given day count from the Mayan epoch.
    ///
    /// The base Tzolkin date at day 0 (creation) is 4 Ahau.
    #[must_use]
    pub fn from_days(days: u64) -> Self {
        // At day 0 the Tzolkin is 4 Ahau.
        // Number cycles 1-13: base offset is 4 (for 4 Ahau at day 0).
        // (days + 3) mod 13 gives 0-based, then +1 for 1-based.
        let number = ((days + 3) % 13 + 1) as u8;
        // Day sign cycles through 20: Ahau is index 19, so offset 19.
        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)
    }
}

// ---------------------------------------------------------------------------
// Haab (solar 365-day calendar)
// ---------------------------------------------------------------------------

/// The 19 months of the Haab calendar (18 regular months + Wayeb).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum HaabMonth {
    /// Pop (month 1)
    Pop,
    /// Wo (month 2)
    Wo,
    /// Sip (month 3)
    Sip,
    /// Sotz (month 4)
    Sotz,
    /// Sek (month 5)
    Sek,
    /// Xul (month 6)
    Xul,
    /// Yaxkin (month 7)
    Yaxkin,
    /// Mol (month 8)
    Mol,
    /// Chen (month 9)
    Chen,
    /// Yax (month 10)
    Yax,
    /// Sak (month 11)
    Sak,
    /// Keh (month 12)
    Keh,
    /// Mak (month 13)
    Mak,
    /// Kankin (month 14)
    Kankin,
    /// Muan (month 15)
    Muan,
    /// Pax (month 16)
    Pax,
    /// Kayab (month 17)
    Kayab,
    /// Kumku (month 18)
    Kumku,
    /// Wayeb (5 unlucky days)
    Wayeb,
}

/// Ordered list of all 19 Haab months.
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,
];

/// A Haab date (solar 365-day calendar).
///
/// 18 months of 20 days each (0-19), plus Wayeb (5 "nameless" days, 0-4).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Haab {
    /// Day within the month (0-19 for regular months, 0-4 for Wayeb).
    pub day: u8,
    /// The Haab month.
    pub month: HaabMonth,
}

impl Haab {
    /// Compute the Haab date for a given day count from the Mayan epoch.
    ///
    /// The base Haab date at day 0 (creation) is 8 Kumku.
    #[must_use]
    pub fn from_days(days: u64) -> Self {
        // At day 0, the Haab is 8 Kumku.
        // Kumku is month index 17 (0-based). Day 8.
        // The offset into the Haab cycle: 17*20 + 8 = 348.
        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)
    }
}

// ---------------------------------------------------------------------------
// Calendar Round
// ---------------------------------------------------------------------------

/// Compute the Calendar Round (Tzolkin + Haab) for a given day count.
///
/// The Calendar Round repeats every lcm(260, 365) = 18,980 days (~52 years).
#[must_use]
#[inline]
pub fn calendar_round(days: u64) -> (Tzolkin, Haab) {
    (Tzolkin::from_days(days), Haab::from_days(days))
}

/// The Calendar Round cycle length in days: lcm(260, 365) = 18,980.
pub const CALENDAR_ROUND_DAYS: u64 = 18_980;

/// Find the next occurrence of a specific Calendar Round date on or after
/// a given day count.
///
/// A Calendar Round date is a (Tzolkin, Haab) pair that repeats every
/// 18,980 days (~52 years). Given a target Tzolkin number, day sign, Haab
/// day, and Haab month, this function searches forward from `start_day`
/// to find the next matching date.
///
/// # Errors
///
/// Returns [`SankhyaError::InvalidDate`] if the Tzolkin number is not 1–13
/// or the Haab day is out of range for the month.
/// Returns [`SankhyaError::ComputationError`] if no match is found within
/// one full Calendar Round cycle (should not happen for valid inputs).
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})"
        )));
    }

    // Search day by day within one Calendar Round cycle
    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(),
    ))
}

/// Find the next occurrence of a given Tzolkin date on or after a given day count.
///
/// The Tzolkin cycle repeats every 260 days.
///
/// # Errors
///
/// Returns [`SankhyaError::InvalidDate`] if the Tzolkin number is not 1–13.
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(),
    ))
}

// ---------------------------------------------------------------------------
// Venus table
// ---------------------------------------------------------------------------

/// The synodic period of Venus as computed by the Maya: 583.92 days.
///
/// The Dresden Codex Venus table used 584 days (5 × 584 = 2920 = 8 × 365),
/// accurate to within 2 hours over 500 years.
pub const VENUS_SYNODIC_PERIOD: f64 = 583.92;

/// Phases of Venus as tracked in the Mayan Venus tables (Dresden Codex).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum VenusPhase {
    /// Venus visible as the morning star (236 days in the Mayan model).
    MorningStar,
    /// Venus hidden at superior conjunction (90 days).
    SuperiorConjunction,
    /// Venus visible as the evening star (250 days).
    EveningStar,
    /// Venus hidden at inferior conjunction (8 days).
    InferiorConjunction,
}

/// Determine the phase of Venus given days from the Mayan epoch.
///
/// Uses the Mayan model from the Dresden Codex:
/// - Morning Star: 236 days
/// - Superior Conjunction: 90 days
/// - Evening Star: 250 days
/// - Inferior Conjunction: 8 days
///
/// Total: 584 days (Mayan rounded synodic period)
#[must_use]
pub fn venus_phase(days_from_epoch: u64) -> VenusPhase {
    // The Mayan Venus cycle is 584 days (rounded from 583.92).
    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() {
        // The creation date is 0.0.0.0.0
        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() {
        // Dec 21, 2012 = 13.0.0.0.0 = 1,872,000 days from creation
        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() {
        // 236 + 90 + 250 + 8 = 584
        assert_eq!(236 + 90 + 250 + 8, 584);
    }

    // -- Calendar Round search --

    #[test]
    fn find_calendar_round_at_creation() {
        // Creation date is 4 Ahau 8 Kumku — should find day 0
        let day = find_calendar_round(4, DaySign::Ahau, 8, HaabMonth::Kumku, 0).unwrap();
        assert_eq!(day, 0);
    }

    #[test]
    fn find_calendar_round_next_cycle() {
        // Same date should recur at day 18,980
        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() {
        // 4 Ahau at day 0
        let day = find_tzolkin(4, DaySign::Ahau, 0).unwrap();
        assert_eq!(day, 0);
    }

    #[test]
    fn find_tzolkin_next_occurrence() {
        // 4 Ahau should recur every 260 days
        let day = find_tzolkin(4, DaySign::Ahau, 1).unwrap();
        assert_eq!(day, 260);
    }
}