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}