Skip to main content

sankhya/
babylonian.rs

1//! Babylonian mathematics.
2//!
3//! Implements the sexagesimal (base-60) number system, Saros eclipse cycle,
4//! reciprocal tables for regular numbers, Plimpton 322 Pythagorean triples,
5//! and the Babylonian square root method (Heron's method).
6//!
7//! # Historical Context
8//!
9//! The Babylonians (c. 2000-300 BCE) developed one of the earliest
10//! positional number systems using base 60. This survives today in our
11//! 60-minute hours and 360-degree circles. They compiled extensive
12//! mathematical tables on clay tablets, including the famous Plimpton 322
13//! tablet containing Pythagorean triples, predating Pythagoras by over
14//! a millennium.
15
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18
19use crate::error::{Result, SankhyaError};
20
21// ---------------------------------------------------------------------------
22// Sexagesimal (base-60) number system
23// ---------------------------------------------------------------------------
24
25/// Convert a decimal number to sexagesimal (base-60) digits, most significant first.
26#[must_use]
27pub fn to_sexagesimal(mut n: u64) -> Vec<u8> {
28    if n == 0 {
29        return vec![0];
30    }
31    let mut digits = Vec::new();
32    while n > 0 {
33        digits.push((n % 60) as u8);
34        n /= 60;
35    }
36    digits.reverse();
37    digits
38}
39
40/// Convert sexagesimal (base-60) digits back to a decimal number.
41///
42/// # Errors
43///
44/// Returns [`SankhyaError::InvalidBase`] if any digit is >= 60.
45#[must_use = "returns the converted value or an error"]
46pub fn from_sexagesimal(digits: &[u8]) -> Result<u64> {
47    let mut result: u64 = 0;
48    for &d in digits {
49        if d >= 60 {
50            return Err(SankhyaError::InvalidBase(format!(
51                "sexagesimal digit {d} out of range 0..60"
52            )));
53        }
54        result = result
55            .checked_mul(60)
56            .and_then(|r| r.checked_add(u64::from(d)))
57            .ok_or_else(|| SankhyaError::OverflowError("sexagesimal conversion overflow".into()))?;
58    }
59    Ok(result)
60}
61
62// ---------------------------------------------------------------------------
63// Babylonian numeral
64// ---------------------------------------------------------------------------
65
66/// A single Babylonian sexagesimal digit (0-59).
67///
68/// Babylonian cuneiform used two symbols: a vertical wedge (units, 1-9)
69/// and a corner wedge (tens, 10-50). Each digit 0-59 is composed of
70/// some number of tens and units wedges.
71#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
72pub struct BabylonianNumeral {
73    /// Number of ten-wedges (0-5).
74    pub tens: u8,
75    /// Number of unit-wedges (0-9).
76    pub units: u8,
77}
78
79impl BabylonianNumeral {
80    /// Create a numeral from a value 0-59.
81    ///
82    /// # Errors
83    ///
84    /// Returns [`SankhyaError::InvalidBase`] if value >= 60.
85    #[must_use = "returns the numeral or an error"]
86    pub fn from_value(value: u8) -> Result<Self> {
87        if value >= 60 {
88            return Err(SankhyaError::InvalidBase(format!(
89                "Babylonian digit {value} out of range 0..60"
90            )));
91        }
92        Ok(Self {
93            tens: value / 10,
94            units: value % 10,
95        })
96    }
97
98    /// The decimal value of this numeral (0-59).
99    #[must_use]
100    #[inline]
101    pub fn value(self) -> u8 {
102        self.tens * 10 + self.units
103    }
104}
105
106// ---------------------------------------------------------------------------
107// Saros cycle
108// ---------------------------------------------------------------------------
109
110/// The Saros cycle in days: approximately 6585.32 days (223 synodic months).
111///
112/// The Babylonians discovered that eclipses repeat after 223 synodic months
113/// (6585 days, 7 hours, 43 minutes). This was recorded on the "Saros Canon"
114/// tablets found at Babylon, dating to around 500 BCE.
115pub const SAROS_DAYS: f64 = 6585.3211;
116
117/// Predict the Julian Day Number of the next eclipse in the Saros series.
118///
119/// Given the JDN of an observed eclipse, returns the predicted JDN
120/// of the next occurrence one Saros cycle later.
121#[must_use]
122#[inline]
123pub fn saros_cycle(eclipse_jdn: f64) -> f64 {
124    eclipse_jdn + SAROS_DAYS
125}
126
127// ---------------------------------------------------------------------------
128// Babylonian lunar calendar
129// ---------------------------------------------------------------------------
130
131/// Mean synodic month in days (Babylonian value, remarkably accurate).
132///
133/// The Babylonians determined this from centuries of eclipse records.
134/// Their value of 29.530594 days (from System B lunar theory) is within
135/// 0.5 seconds of the modern value (29.530589 days).
136pub const SYNODIC_MONTH_DAYS: f64 = 29.530_594;
137
138/// The 12 months of the Babylonian lunisolar calendar.
139///
140/// The calendar began in spring (Nisannu = March/April). An intercalary
141/// 13th month (Addaru II or Ululu II) was added ~7 times per 19 years
142/// following the Metonic cycle, regulated from 499 BCE onward.
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
144#[non_exhaustive]
145pub enum BabylonianMonth {
146    /// Nisannu (Month I, March/April) — New Year
147    Nisannu,
148    /// Ayaru (Month II)
149    Ayaru,
150    /// Simanu (Month III)
151    Simanu,
152    /// Dumuzu (Month IV)
153    Dumuzu,
154    /// Abu (Month V)
155    Abu,
156    /// Ululu (Month VI)
157    Ululu,
158    /// Tashritu (Month VII) — Autumn equinox festival
159    Tashritu,
160    /// Arahsamna (Month VIII)
161    Arahsamna,
162    /// Kislimu (Month IX)
163    Kislimu,
164    /// Tebetu (Month X)
165    Tebetu,
166    /// Shabatu (Month XI)
167    Shabatu,
168    /// Addaru (Month XII)
169    Addaru,
170}
171
172const BABYLONIAN_MONTHS: [BabylonianMonth; 12] = [
173    BabylonianMonth::Nisannu,
174    BabylonianMonth::Ayaru,
175    BabylonianMonth::Simanu,
176    BabylonianMonth::Dumuzu,
177    BabylonianMonth::Abu,
178    BabylonianMonth::Ululu,
179    BabylonianMonth::Tashritu,
180    BabylonianMonth::Arahsamna,
181    BabylonianMonth::Kislimu,
182    BabylonianMonth::Tebetu,
183    BabylonianMonth::Shabatu,
184    BabylonianMonth::Addaru,
185];
186
187/// A date in the Babylonian lunisolar calendar.
188///
189/// This is a simplified model using alternating 30/29-day months
190/// (the historical calendar relied on direct observation of the new
191/// crescent moon). Intercalary months are not modeled.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
193pub struct BabylonianDate {
194    /// Year (relative to the Seleucid Era, 311 BCE = year 1).
195    pub year: i64,
196    /// Month (one of the 12 standard months).
197    pub month: BabylonianMonth,
198    /// Day of month (1-30).
199    pub day: u8,
200}
201
202/// JDN of the Babylonian calendar epoch: 1 Nisannu, Year 1 of the
203/// Seleucid Era (April 3, 311 BCE Julian).
204///
205/// The Seleucid Era is the most precisely datable Babylonian chronological
206/// reference, used on cuneiform tablets from the late period.
207pub const BABYLONIAN_EPOCH_JDN: f64 = 1_607_738.5;
208
209/// Days in a standard Babylonian calendar year (12 months, alternating 30/29).
210/// Odd months (I, III, V, VII, IX, XI) have 30 days;
211/// Even months (II, IV, VI, VIII, X, XII) have 29 days.
212/// Total: 6 × 30 + 6 × 29 = 354 days.
213pub const BABYLONIAN_YEAR_DAYS: u16 = 354;
214
215/// Days in each Babylonian month (alternating 30/29).
216const BABYLONIAN_MONTH_DAYS: [u8; 12] = [30, 29, 30, 29, 30, 29, 30, 29, 30, 29, 30, 29];
217
218/// Convert a Julian Day Number to a Babylonian date.
219///
220/// Uses the simplified 354-day year model (no intercalary months).
221/// This is the civil approximation used for administrative purposes.
222#[must_use]
223pub fn jdn_to_babylonian(jdn: f64) -> BabylonianDate {
224    tracing::trace!(jdn, "JDN to Babylonian");
225    let days_since_epoch = (jdn - BABYLONIAN_EPOCH_JDN).floor() as i64;
226
227    let year_days = i64::from(BABYLONIAN_YEAR_DAYS);
228    let years = days_since_epoch.div_euclid(year_days);
229    let mut remaining = days_since_epoch.rem_euclid(year_days);
230
231    let year = years + 1; // Year 1 based
232
233    let mut month_idx = 0;
234    for (i, &md) in BABYLONIAN_MONTH_DAYS.iter().enumerate() {
235        if remaining < i64::from(md) {
236            month_idx = i;
237            break;
238        }
239        remaining -= i64::from(md);
240        if i == 11 {
241            month_idx = 11;
242        }
243    }
244
245    BabylonianDate {
246        year,
247        month: BABYLONIAN_MONTHS[month_idx],
248        day: remaining as u8 + 1,
249    }
250}
251
252/// Convert a Babylonian date to a Julian Day Number.
253///
254/// # Errors
255///
256/// Returns [`SankhyaError::InvalidDate`] if the day is out of range.
257#[must_use = "returns the JDN or an error"]
258pub fn babylonian_to_jdn(date: &BabylonianDate) -> Result<f64> {
259    tracing::trace!(year = date.year, ?date.month, day = date.day, "Babylonian to JDN");
260    let month_idx = BABYLONIAN_MONTHS
261        .iter()
262        .position(|&m| m == date.month)
263        .unwrap_or(0);
264
265    let max_day = BABYLONIAN_MONTH_DAYS[month_idx];
266    if date.day == 0 || date.day > max_day {
267        return Err(SankhyaError::InvalidDate(format!(
268            "day {} out of range for {:?} (max {max_day})",
269            date.day, date.month
270        )));
271    }
272
273    let mut days = i64::from(BABYLONIAN_YEAR_DAYS) * (date.year - 1);
274    for &md in &BABYLONIAN_MONTH_DAYS[..month_idx] {
275        days += i64::from(md);
276    }
277    days += i64::from(date.day - 1);
278
279    Ok(BABYLONIAN_EPOCH_JDN + days as f64)
280}
281
282/// Compute the number of synodic months elapsed between two JDNs.
283///
284/// Returns `(complete_months, remainder_days)`.
285#[must_use]
286pub fn synodic_months_between(jdn1: f64, jdn2: f64) -> (u64, f64) {
287    let elapsed = (jdn2 - jdn1).abs();
288    let months = (elapsed / SYNODIC_MONTH_DAYS).floor();
289    let remainder = elapsed - months * SYNODIC_MONTH_DAYS;
290    (months as u64, remainder)
291}
292
293impl core::fmt::Display for BabylonianDate {
294    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
295        write!(f, "{} {:?}, Year {} SE", self.day, self.month, self.year)
296    }
297}
298
299impl core::fmt::Display for BabylonianMonth {
300    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
301        let name = match self {
302            Self::Nisannu => "Nisannu",
303            Self::Ayaru => "Ayaru",
304            Self::Simanu => "Simanu",
305            Self::Dumuzu => "Dumuzu",
306            Self::Abu => "Abu",
307            Self::Ululu => "Ululu",
308            Self::Tashritu => "Tashritu",
309            Self::Arahsamna => "Arahsamna",
310            Self::Kislimu => "Kislimu",
311            Self::Tebetu => "Tebetu",
312            Self::Shabatu => "Shabatu",
313            Self::Addaru => "Addaru",
314        };
315        write!(f, "{name}")
316    }
317}
318
319// ---------------------------------------------------------------------------
320// Reciprocal tables (regular numbers)
321// ---------------------------------------------------------------------------
322
323/// Generate the Babylonian reciprocal table for regular numbers up to 81.
324///
325/// A "regular number" in base 60 is one whose only prime factors are
326/// 2, 3, and 5 (the prime factors of 60). These numbers have finite
327/// sexagesimal reciprocals.
328///
329/// Returns a map from regular number to its sexagesimal reciprocal
330/// (as a vector of base-60 digits representing the fraction).
331///
332/// For example: 2 -> \[30\] (meaning 30/60 = 1/2),
333///              3 -> \[20\] (meaning 20/60 = 1/3),
334///              4 -> \[15\] (meaning 15/60 = 1/4).
335#[must_use]
336pub fn reciprocal_table() -> BTreeMap<u64, Vec<u8>> {
337    // Known Babylonian reciprocal pairs.
338    // The reciprocal of n is computed as the sexagesimal representation of 60/n
339    // (or 3600/n for two-digit reciprocals, etc.)
340    let pairs: &[(u64, &[u8])] = &[
341        (2, &[30]),             // 1/2 = 0;30
342        (3, &[20]),             // 1/3 = 0;20
343        (4, &[15]),             // 1/4 = 0;15
344        (5, &[12]),             // 1/5 = 0;12
345        (6, &[10]),             // 1/6 = 0;10
346        (8, &[7, 30]),          // 1/8 = 0;7,30
347        (9, &[6, 40]),          // 1/9 = 0;6,40
348        (10, &[6]),             // 1/10 = 0;6
349        (12, &[5]),             // 1/12 = 0;5
350        (15, &[4]),             // 1/15 = 0;4
351        (16, &[3, 45]),         // 1/16 = 0;3,45
352        (18, &[3, 20]),         // 1/18 = 0;3,20
353        (20, &[3]),             // 1/20 = 0;3
354        (24, &[2, 30]),         // 1/24 = 0;2,30
355        (25, &[2, 24]),         // 1/25 = 0;2,24
356        (27, &[2, 13, 20]),     // 1/27 = 0;2,13,20
357        (30, &[2]),             // 1/30 = 0;2
358        (32, &[1, 52, 30]),     // 1/32 = 0;1,52,30
359        (36, &[1, 40]),         // 1/36 = 0;1,40
360        (40, &[1, 30]),         // 1/40 = 0;1,30
361        (45, &[1, 20]),         // 1/45 = 0;1,20
362        (48, &[1, 15]),         // 1/48 = 0;1,15
363        (50, &[1, 12]),         // 1/50 = 0;1,12
364        (54, &[1, 6, 40]),      // 1/54 = 0;1,6,40
365        (60, &[1]),             // 1/60 = 0;1
366        (64, &[0, 56, 15]),     // 1/64 = 0;0,56,15
367        (72, &[0, 50]),         // 1/72 = 0;0,50
368        (80, &[0, 45]),         // 1/80 = 0;0,45
369        (81, &[0, 44, 26, 40]), // 1/81 = 0;0,44,26,40
370    ];
371
372    let mut table = BTreeMap::new();
373    for &(n, recip) in pairs {
374        table.insert(n, recip.to_vec());
375    }
376    table
377}
378
379// ---------------------------------------------------------------------------
380// Plimpton 322 Pythagorean triples
381// ---------------------------------------------------------------------------
382
383/// Generate the 15 Pythagorean triples from the Plimpton 322 tablet.
384///
385/// Plimpton 322 is a Babylonian clay tablet (c. 1800 BCE) containing
386/// a table of Pythagorean triples. The tablet lists the short side (b),
387/// the diagonal (c = hypotenuse), and a ratio column. The triples are
388/// returned as (a, b, c) where a^2 + b^2 = c^2.
389///
390/// These triples were generated using the parametric form:
391/// a = p^2 - q^2, b = 2pq, c = p^2 + q^2 for appropriate p, q values.
392#[must_use]
393pub fn generate_plimpton_triples() -> Vec<(u64, u64, u64)> {
394    // The 15 rows of Plimpton 322, reconstructed as (a, b, c).
395    // Row ordering follows the tablet (sorted by decreasing angle).
396    vec![
397        (119, 120, 169),
398        (3367, 3456, 4825),
399        (4601, 4800, 6649),
400        (12709, 13500, 18541),
401        (65, 72, 97),
402        (319, 360, 481),
403        (2291, 2700, 3541),
404        (799, 960, 1249),
405        (481, 600, 769),
406        (4961, 6480, 8161),
407        (45, 60, 75),
408        (1679, 2400, 2929),
409        (161, 240, 289),
410        (1771, 2700, 3229),
411        (56, 90, 106),
412    ]
413}
414
415// ---------------------------------------------------------------------------
416// Babylonian square root (Heron's method)
417// ---------------------------------------------------------------------------
418
419/// Compute the square root using the Babylonian/Heron's method.
420///
421/// This iterative method was known to the Babylonians as early as
422/// 1700 BCE (the YBC 7289 tablet shows sqrt(2) accurate to 6 decimal places).
423///
424/// The algorithm: starting with an initial guess x, repeatedly compute
425/// x = (x + n/x) / 2 until convergence.
426///
427/// # Errors
428///
429/// Returns [`SankhyaError::ComputationError`] if `n` is negative.
430/// Returns [`SankhyaError::InvalidBase`] if `iterations` is zero.
431#[must_use = "returns the square root or an error"]
432pub fn babylonian_sqrt(n: f64, iterations: u32) -> Result<f64> {
433    tracing::debug!(n, iterations, "Babylonian/Heron sqrt");
434    if n.is_nan() || n.is_infinite() || n < 0.0 {
435        return Err(SankhyaError::ComputationError(
436            "cannot compute square root of negative, NaN, or infinite number".into(),
437        ));
438    }
439    if n == 0.0 {
440        return Ok(0.0);
441    }
442    if iterations == 0 {
443        return Err(SankhyaError::InvalidBase(
444            "iterations must be at least 1".into(),
445        ));
446    }
447    // Initial guess: n/2 (or 1 if n < 2)
448    let mut x = if n < 2.0 { 1.0 } else { n / 2.0 };
449    for _ in 0..iterations {
450        x = (x + n / x) / 2.0;
451    }
452    Ok(x)
453}
454
455// ---------------------------------------------------------------------------
456// Cuneiform display (requires varna)
457// ---------------------------------------------------------------------------
458
459/// Render a sexagesimal digit (0-59) in cuneiform notation.
460///
461/// Uses the Babylonian cuneiform numeral system from varna: 𒐕 (diš) for
462/// units 1-9, 𒌋/𒌋𒌋/𒌍 for tens 10/20/30. Digits above 30 are composed
463/// additively (e.g., 42 = 𒌍 + 𒐖 + 𒌋 = "𒌍𒌋𒐖").
464///
465/// Returns a space `" "` for zero (Babylonians had no zero symbol in
466/// early periods).
467///
468/// Requires the `varna` feature.
469///
470/// # Errors
471///
472/// Returns [`SankhyaError::InvalidBase`] if `digit` >= 60.
473#[cfg(feature = "varna")]
474#[must_use = "returns the cuneiform string or an error"]
475pub fn cuneiform_digit(digit: u8) -> Result<String> {
476    if digit >= 60 {
477        return Err(SankhyaError::InvalidBase(format!(
478            "cuneiform digit {digit} out of range 0..60"
479        )));
480    }
481    if digit == 0 {
482        return Ok(" ".into());
483    }
484
485    let system = varna::script::numerals::babylonian_sexagesimal();
486    let tens = digit / 10;
487    let units = digit % 10;
488    let mut result = String::new();
489
490    // Tens: use the highest available symbol, then compose
491    if tens > 0 {
492        // Available tens symbols: 10, 20, 30
493        let mut remaining_tens = tens;
494        for &val in &[30u8, 20, 10] {
495            if remaining_tens * 10 >= val
496                && let Some(ch) = system.char_for(u32::from(val))
497            {
498                result.push_str(ch);
499                remaining_tens -= val / 10;
500            }
501            if remaining_tens == 0 {
502                break;
503            }
504        }
505    }
506
507    if units > 0
508        && let Some(ch) = system.char_for(u32::from(units))
509    {
510        result.push_str(ch);
511    }
512
513    Ok(result)
514}
515
516/// Render a full number in cuneiform sexagesimal notation.
517///
518/// Digits are separated by a middle dot `·` for readability,
519/// matching the modern convention for displaying sexagesimal.
520///
521/// Requires the `varna` feature.
522///
523/// # Errors
524///
525/// Returns [`SankhyaError::InvalidBase`] if any internal digit is invalid.
526#[cfg(feature = "varna")]
527#[must_use = "returns the cuneiform string or an error"]
528pub fn to_cuneiform(n: u64) -> Result<String> {
529    let digits = to_sexagesimal(n);
530    let mut parts = Vec::with_capacity(digits.len());
531    for &d in &digits {
532        parts.push(cuneiform_digit(d)?);
533    }
534    Ok(parts.join("·"))
535}
536
537#[cfg(test)]
538mod tests {
539    use super::*;
540
541    #[test]
542    fn sexagesimal_zero() {
543        assert_eq!(to_sexagesimal(0), vec![0]);
544        assert_eq!(from_sexagesimal(&[0]).unwrap(), 0);
545    }
546
547    #[test]
548    fn sexagesimal_roundtrip() {
549        for n in [1, 59, 60, 3599, 3600, 216_000, 1_000_000] {
550            let digits = to_sexagesimal(n);
551            assert_eq!(from_sexagesimal(&digits).unwrap(), n, "failed for {n}");
552        }
553    }
554
555    #[test]
556    fn babylonian_numeral_value() {
557        let n = BabylonianNumeral::from_value(42).unwrap();
558        assert_eq!(n.tens, 4);
559        assert_eq!(n.units, 2);
560        assert_eq!(n.value(), 42);
561    }
562
563    #[test]
564    fn plimpton_triples_valid() {
565        let triples = generate_plimpton_triples();
566        assert_eq!(triples.len(), 15);
567        for (a, b, c) in &triples {
568            assert_eq!(a * a + b * b, c * c, "invalid triple: ({a}, {b}, {c})");
569        }
570    }
571
572    #[test]
573    fn sqrt_2_accuracy() {
574        let result = babylonian_sqrt(2.0, 10).unwrap();
575        assert!((result - std::f64::consts::SQRT_2).abs() < 1e-15);
576    }
577
578    #[test]
579    fn saros_cycle_test() {
580        let next = saros_cycle(2451545.0); // J2000.0
581        assert!((next - (2451545.0 + SAROS_DAYS)).abs() < 1e-10);
582    }
583
584    // -- Lunar calendar --
585
586    #[test]
587    fn babylonian_epoch_roundtrip() {
588        let date = jdn_to_babylonian(BABYLONIAN_EPOCH_JDN);
589        assert_eq!(date.year, 1);
590        assert_eq!(date.month, BabylonianMonth::Nisannu);
591        assert_eq!(date.day, 1);
592
593        let jdn = babylonian_to_jdn(&date).unwrap();
594        assert!((jdn - BABYLONIAN_EPOCH_JDN).abs() < 0.5);
595    }
596
597    #[test]
598    fn babylonian_year_is_354() {
599        let total: u16 = BABYLONIAN_MONTH_DAYS.iter().map(|&d| u16::from(d)).sum();
600        assert_eq!(total, BABYLONIAN_YEAR_DAYS);
601    }
602
603    #[test]
604    fn babylonian_month_alternates() {
605        // Odd months (0-indexed: 0,2,4...) have 30, even have 29
606        for (i, &d) in BABYLONIAN_MONTH_DAYS.iter().enumerate() {
607            if i % 2 == 0 {
608                assert_eq!(d, 30);
609            } else {
610                assert_eq!(d, 29);
611            }
612        }
613    }
614
615    #[test]
616    fn babylonian_to_jdn_invalid_day() {
617        let date = BabylonianDate {
618            year: 1,
619            month: BabylonianMonth::Ayaru, // 29-day month
620            day: 30,
621        };
622        assert!(babylonian_to_jdn(&date).is_err());
623    }
624
625    #[test]
626    fn synodic_months_one_year() {
627        // ~12.37 synodic months in a year
628        let (months, _rem) = synodic_months_between(0.0, 365.25);
629        assert_eq!(months, 12);
630    }
631
632    #[test]
633    fn serde_roundtrip_babylonian_date() {
634        let date = jdn_to_babylonian(BABYLONIAN_EPOCH_JDN + 500.0);
635        let json = serde_json::to_string(&date).unwrap();
636        let back: BabylonianDate = serde_json::from_str(&json).unwrap();
637        assert_eq!(date, back);
638    }
639
640    #[cfg(feature = "varna")]
641    mod cuneiform_tests {
642        use super::*;
643
644        #[test]
645        fn cuneiform_digit_units() {
646            let s = cuneiform_digit(1).unwrap();
647            assert_eq!(s, "𒐕");
648            let s = cuneiform_digit(9).unwrap();
649            assert_eq!(s, "𒐝");
650        }
651
652        #[test]
653        fn cuneiform_digit_tens() {
654            let s = cuneiform_digit(10).unwrap();
655            assert_eq!(s, "𒌋");
656            let s = cuneiform_digit(30).unwrap();
657            assert_eq!(s, "𒌍");
658        }
659
660        #[test]
661        fn cuneiform_digit_composite() {
662            // 42 = 30 + 10 + 2 = 𒌍𒌋𒐖
663            let s = cuneiform_digit(42).unwrap();
664            assert!(s.contains("𒌍"));
665            assert!(s.contains("𒐖"));
666        }
667
668        #[test]
669        fn cuneiform_digit_zero() {
670            assert_eq!(cuneiform_digit(0).unwrap(), " ");
671        }
672
673        #[test]
674        fn cuneiform_digit_out_of_range() {
675            assert!(cuneiform_digit(60).is_err());
676        }
677
678        #[test]
679        fn to_cuneiform_basic() {
680            // 60 = [1, 0] in sexagesimal
681            let s = to_cuneiform(60).unwrap();
682            assert!(s.contains('·'));
683            assert!(s.contains("𒐕"));
684        }
685    }
686}