Skip to main content

calendar_core/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![warn(missing_docs)]
3
4//! # Nusantara Calendar Core
5//!
6//! This crate provides the foundational types, traits, and utilities for the
7//! nusantara-calendar workspace, which implements traditional Indonesian
8//! calendar systems with modern Rust ergonomics.
9//!
10//! ## Julian Day Number (JDN) Pivot
11//!
12//! This crate uses the Julian Day Number (JDN) system as the central pivot for
13//! all calendar conversions. JDN provides a continuous count of days since
14//! noon Universal Time on January 1, 4713 BCE (Julian calendar), making it
15//! ideal for converting between different calendar systems.
16//!
17//! The JDN system allows us to:
18//! - Convert any date to/from Gregorian calendar
19//! - Implement bidirectional conversions between calendar systems
20//! - Perform date arithmetic with consistent results
21//! - Handle historical dates across different calendar reforms
22//!
23//! ## Algorithm Source
24//!
25//! The Gregorian to JDN conversion algorithm implemented in this crate follows
26//! the standard formula from:
27//!
28//! **Meeus, Jean.** *Astronomical Algorithms*, 2nd Edition.
29//! Willmann-Blohm, 1998. Chapter 7: "Julian Day".
30//!
31//! This reference implementation provides accurate conversions for the full
32//! range of historical dates supported by the JDN system (approximately
33//! 262,000 BCE to 262,000 CE).
34//!
35//! ## Core Components
36//!
37//! ### Types
38//! - [`JDN`] - Julian Day Number type alias (`i64`)
39//! - [`CycleYear`] - Type for cycle-year fields (`u32`)
40//! - [`SubYearPosition`] - Type for sub-year positions (`u8`)
41//!
42//! ### Traits
43//! - [`CalendarDate`] - Core interface for calendar implementations
44//! - [`CalendarMetadata`] - Access to calendar metadata and cultural context
45//! - [`HasAuspiciousness`] - Auspiciousness calculations for Indonesian calendars
46//!
47//! ### Error Handling
48//! - [`CalendarError`] - Comprehensive error types with detailed context
49//! - [`stub!`] macro for marking unimplemented features
50//!
51//! ### Cultural Features
52//! - [`Activity`] - Indonesian cultural activities for auspiciousness evaluation
53//! - [`AuspiciousnessLevel`] - Favorability levels for activities and dates
54//!
55//! ## Platform Support
56//!
57//! This crate supports multiple compilation targets:
58//! - **std**: Standard library with full functionality
59//! - **`no_std`**: Embedded systems with `alloc` support
60//! - **WASM**: WebAssembly targets for browser usage
61//!
62//! ## Example Usage
63//!
64//! ```rust
65//! use calendar_core::{gregorian_to_jdn, jdn_to_gregorian, CalendarDate};
66//!
67//! // Convert Gregorian to JDN
68//! let jdn = gregorian_to_jdn(2024, 3, 15);
69//!
70//! // Convert back to Gregorian
71//! let (year, month, day) = jdn_to_gregorian(jdn);
72//!
73//! // Use with CalendarDate trait implementations
74//! // let calendar_date = MyCalendar::from_gregorian(2024, 3, 15)?;
75//! ```
76//!
77//! ## Indonesian Calendar Context
78//!
79//! This crate is specifically designed to support the rich diversity of
80//! Indonesian calendar systems, including:
81//! - Javanese calendar (Saka and Islamic integration)
82//! - Balinese calendar (Pawukon cycle)
83//! - Hijri/Islamic calendar
84//! - Chinese calendar integration
85//! - Various regional ethnic calendars
86//!
87//! Each calendar system can implement the core traits while maintaining
88//! cultural authenticity and computational accuracy.
89
90extern crate alloc;
91
92// Re-export commonly used items
93pub use thiserror::Error;
94
95use alloc::string::String;
96
97// Modules
98pub mod auspiciousness;
99
100// Re-export auspiciousness types
101pub use auspiciousness::{Activity, AuspiciousnessLevel};
102
103/// Julian Day Number type alias
104///
105/// Uses i64 to support the full range of historical dates
106/// from approximately 262,000 BCE to 262,000 CE
107pub type JDN = i64;
108
109/// Type for cycle-year fields (e.g., year in a 60-year cycle)
110///
111/// Uses u32 to support large cycles while remaining efficient
112pub type CycleYear = u32;
113
114/// Type for sub-year positions (e.g., month, day, weekday)
115///
116/// Uses u8 for compact storage of values 0-255
117pub type SubYearPosition = u8;
118
119/// Core error types for calendar operations
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub enum CalendarError {
122    /// Date is out of supported range
123    OutOfRange(String),
124    /// Invalid calendar parameters
125    InvalidParameters(String),
126    /// Feature not yet implemented
127    NotImplemented(String),
128    /// Arithmetic error
129    ArithmeticError(String),
130}
131
132impl core::fmt::Display for CalendarError {
133    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134        match self {
135            Self::OutOfRange(msg) => write!(f, "Date out of supported range: {msg}"),
136            Self::InvalidParameters(msg) => write!(f, "Invalid calendar parameters: {msg}"),
137            Self::NotImplemented(msg) => write!(f, "Feature not yet implemented: {msg}"),
138            Self::ArithmeticError(msg) => write!(f, "Arithmetic error: {msg}"),
139        }
140    }
141}
142
143#[cfg(feature = "std")]
144impl std::error::Error for CalendarError {}
145
146/// Trait for calendar date implementations
147///
148/// This trait defines the core interface that all calendar systems
149/// in the nusantara-calendar workspace must implement. It provides
150/// bidirectional conversion with Julian Day Numbers and Gregorian dates,
151/// serving as the foundation for all calendar implementations.
152///
153/// # Required Implementations
154///
155/// Calendar types must implement:
156/// - [`Self::from_jdn()`] - Convert from JDN to calendar date
157/// - [`Self::to_jdn()`] - Convert from calendar date to JDN
158/// - [`Self::calendar_name()`] - Return the calendar system name
159/// - [`Self::validate_range()`] - Check if date is within supported range
160///
161/// # Provided Implementations
162///
163/// Default implementations are provided for:
164/// - [`Self::from_gregorian()`] - Convert via JDN intermediate
165/// - [`Self::to_gregorian()`] - Convert via JDN intermediate
166///
167/// # Implementation Guidelines
168///
169/// When implementing this trait:
170///
171/// 1. **Use JDN as the canonical representation** - All conversions should
172///    go through JDN to ensure consistency across calendar systems
173///
174/// 2. **Validate date ranges** - Each calendar should define reasonable
175///    bounds and return appropriate errors for out-of-range dates
176///
177/// 3. **Handle cultural specifics** - Account for calendar-specific
178///    rules like leap months, intercalary days, etc.
179///
180/// 4. **Provide clear error messages** - Use descriptive error messages
181///    that help users understand validation failures
182///
183/// # Example Implementation
184///
185/// ```rust
186/// use calendar_core::{CalendarDate, CalendarError, JDN};
187///
188/// #[derive(Debug, Clone, PartialEq, Eq)]
189/// struct MyCalendarDate {
190///     year: i32,
191///     month: u8,
192///     day: u8,
193/// }
194///
195/// impl CalendarDate for MyCalendarDate {
196///     fn from_jdn(jdn: JDN) -> Result<Self, CalendarError> {
197///         // Convert JDN to calendar date
198///         // Implementation depends on calendar rules
199///         todo!("Implement JDN to calendar conversion")
200///     }
201///
202///     fn to_jdn(&self) -> JDN {
203///         // Convert calendar date to JDN
204///         // Implementation depends on calendar rules
205///         todo!("Implement calendar to JDN conversion")
206///     }
207///
208///     fn calendar_name() -> &'static str {
209///         "My Calendar System"
210///     }
211///
212///     fn validate_range(&self) -> Result<(), CalendarError> {
213///         // Check if date is within supported range
214///         if self.year < 1 || self.year > 9999 {
215///             return Err(CalendarError::OutOfRange(
216///                 "Year must be between 1 and 9999".to_string()
217///             ));
218///         }
219///         Ok(())
220///     }
221/// }
222/// ```
223///
224/// # Performance Considerations
225///
226/// - JDN conversions are the most computationally expensive operations
227/// - Cache results when performing repeated conversions
228/// - Use the provided Gregorian conversion methods for convenience
229/// - Consider lazy evaluation for complex calendar calculations
230pub trait CalendarDate: Clone + PartialEq + Eq + core::fmt::Debug {
231    /// Convert from Julian Day Number to this calendar's date
232    ///
233    /// This is the core conversion method that all calendar implementations
234    /// must provide. It should handle the specific rules and calculations
235    /// required for the calendar system.
236    ///
237    /// # Arguments
238    /// * `jdn` - Julian Day Number to convert
239    ///
240    /// # Returns
241    /// - `Ok(date)` - Successfully converted calendar date
242    /// - `Err(CalendarError)` - Conversion failed (invalid JDN, out of range, etc.)
243    ///
244    /// # Errors
245    /// Returns `CalendarError` if the JDN is invalid or outside the supported range.
246    ///
247    /// # Implementation Notes
248    ///
249    /// - Should handle edge cases like epoch dates, leap years, etc.
250    /// - Must return appropriate errors for invalid inputs
251    /// - Should be the inverse of [`Self::to_jdn()`] for valid dates
252    fn from_jdn(jdn: JDN) -> Result<Self, CalendarError>
253    where
254        Self: Sized;
255
256    /// Convert from this calendar's date to Julian Day Number
257    ///
258    /// This method should provide the exact inverse of [`Self::from_jdn()`]
259    /// for all valid dates in the calendar's supported range.
260    ///
261    /// # Returns
262    /// Julian Day Number representing this calendar date
263    ///
264    /// # Implementation Notes
265    ///
266    /// - Must handle calendar-specific rules (leap months, etc.)
267    /// - Should return consistent results for the same date
268    /// - Round-trip with [`Self::from_jdn()`] should preserve the original date
269    fn to_jdn(&self) -> JDN;
270
271    /// Get the calendar system name
272    ///
273    /// Returns a human-readable name for the calendar system,
274    /// suitable for display in user interfaces and documentation.
275    ///
276    /// # Returns
277    /// String slice containing the calendar name
278    ///
279    /// # Examples
280    ///
281    /// - "Gregorian"
282    /// - "Javanese Saka"
283    /// - "Islamic Hijri"
284    /// - "Chinese Lunisolar"
285    fn calendar_name() -> &'static str;
286
287    /// Validate that this date is within the supported range
288    ///
289    /// Each calendar system should define reasonable bounds for
290    /// valid dates and return appropriate errors for out-of-range values.
291    ///
292    /// # Returns
293    /// - `Ok(())` - Date is valid and within range
294    /// - `Err(CalendarError)` - Date is invalid or out of supported range
295    ///
296    /// # Errors
297    /// Returns `CalendarError` if the date is outside the supported range.
298    ///
299    /// # Implementation Guidelines
300    ///
301    /// - Check year ranges based on historical accuracy
302    /// - Validate month and day values for the specific calendar
303    /// - Consider astronomical constraints (e.g., full moon dates)
304    /// - Provide clear error messages for validation failures
305    fn validate_range(&self) -> Result<(), CalendarError>;
306
307    /// Convert from Gregorian date to this calendar's date
308    ///
309    /// Default implementation uses JDN as intermediate format:
310    /// Gregorian → JDN → Calendar Date
311    ///
312    /// # Arguments
313    /// * `year` - Gregorian year (CE, can be negative for BCE)
314    /// * `month` - Gregorian month (1-12)
315    /// * `day` - Gregorian day (1-31, depending on month)
316    ///
317    /// # Returns
318    /// - `Ok(date)` - Successfully converted calendar date
319    /// - `Err(CalendarError)` - Conversion failed
320    ///
321    /// # Errors
322    /// Returns `CalendarError` if the conversion fails.
323    ///
324    /// # Performance
325    ///
326    /// This involves two conversions: Gregorian→JDN and JDN→Calendar.
327    /// For performance-critical applications, consider implementing
328    /// direct Gregorian→Calendar conversion if the calendar system
329    /// has a known relationship with the Gregorian calendar.
330    fn from_gregorian(year: i32, month: u8, day: u8) -> Result<Self, CalendarError>
331    where
332        Self: Sized,
333    {
334        let jdn = gregorian_to_jdn(year, month, day);
335        Self::from_jdn(jdn)
336    }
337
338    /// Convert from this calendar's date to Gregorian date
339    ///
340    /// Default implementation uses JDN as intermediate format:
341    /// Calendar Date → JDN → Gregorian
342    ///
343    /// # Returns
344    /// Tuple of (year, month, day) in Gregorian calendar
345    ///
346    /// # Performance
347    ///
348    /// This involves two conversions: Calendar→JDN and JDN→Gregorian.
349    /// For performance-critical applications, consider implementing
350    /// direct Calendar→Gregorian conversion if possible.
351    fn to_gregorian(&self) -> (i32, u8, u8) {
352        jdn_to_gregorian(self.to_jdn())
353    }
354}
355
356/// Trait for calendar metadata and information
357///
358/// This trait provides access to calendar-specific metadata such as
359/// epoch information, cycle information, and cultural context. Implementers
360/// should provide accurate historical and cultural information about their
361/// calendar systems.
362///
363/// # Implementation Requirements
364///
365/// Calendar implementations must:
366/// - Provide accurate epoch dates with historical sources
367/// - Document cycle lengths with cultural references
368/// - Include cultural origin information
369/// - Optionally provide reference sources for verification
370///
371/// # Reference Sources Contract
372///
373/// Implementers of this trait should document their reference sources for:
374/// - Epoch dates and historical accuracy
375/// - Cycle calculations and astronomical basis
376/// - Cultural practices and calendar rules
377/// - Regional variations and historical changes
378///
379/// Recommended reference sources include:
380/// - Historical astronomical records
381/// - Cultural and religious texts
382/// - Academic research on calendar systems
383/// - Government or institutional standards
384///
385/// # Example
386///
387/// ```rust
388/// use calendar_core::{CalendarMetadata, JDN};
389///
390/// struct MyCalendar;
391///
392/// impl CalendarMetadata for MyCalendar {
393///     fn epoch() -> JDN {
394///         // Epoch based on historical records
395///         1948439 // Example: March 22, 2024 CE
396///     }
397///
398///     fn cycle_length() -> Option<u32> {
399///         Some(60) // 60-year cycle common in Indonesian calendars
400///     }
401///
402///     fn description() -> &'static str {
403///         "Traditional Indonesian calendar with 60-year cycle"
404///     }
405///
406///     fn cultural_origin() -> &'static str {
407///         "Javanese court calendar system, integrated with Islamic calendar"
408///     }
409/// }
410/// ```
411pub trait CalendarMetadata {
412    /// Get the epoch (starting date) for this calendar
413    ///
414    /// The epoch represents the starting point of the calendar system,
415    /// typically corresponding to a historically significant date.
416    ///
417    /// # Returns
418    /// Julian Day Number of the calendar's epoch
419    ///
420    /// # Historical Context
421    ///
422    /// Implementers should base epoch dates on reliable historical sources
423    /// and document any uncertainties or variations in historical records.
424    fn epoch() -> JDN;
425
426    /// Get the cycle length if this calendar uses cycles
427    ///
428    /// Many Indonesian calendars use cyclical systems (e.g., 60-year cycles).
429    /// This method returns the length of such cycles if applicable.
430    ///
431    /// # Returns
432    /// - `Some(length)` - Number of years in the cycle
433    /// - `None` - Calendar doesn't use year cycles
434    ///
435    /// # Examples
436    ///
437    /// - Javanese calendar: 60-year cycle
438    /// - Chinese calendar: 60-year cycle
439    /// - Gregorian calendar: No cycle (returns `None`)
440    #[must_use]
441    fn cycle_length() -> Option<CycleYear> {
442        None
443    }
444
445    /// Get a description of this calendar system
446    ///
447    /// Provides a concise description of the calendar system, including
448    /// its main characteristics and usage context.
449    ///
450    /// # Returns
451    /// String slice describing the calendar system
452    fn description() -> &'static str;
453
454    /// Get the cultural/ethnic origin of this calendar
455    ///
456    /// Identifies the cultural, ethnic, or religious group that
457    /// developed and primarily uses this calendar system.
458    ///
459    /// # Returns
460    /// String slice identifying the cultural origin
461    ///
462    /// # Examples
463    ///
464    /// - "Javanese court calendar"
465    /// - "Balinese Pawukon system"
466    /// - "Islamic Hijri calendar"
467    /// - "Chinese lunisolar calendar"
468    fn cultural_origin() -> &'static str;
469}
470
471/// Trait for calendars that have auspiciousness calculations
472///
473/// This trait enables Indonesian calendar systems to provide cultural
474/// auspiciousness evaluations for various activities. Many Indonesian
475/// traditional calendars include concepts of auspicious and inauspicious
476/// days that influence important life events and activities.
477///
478/// # Cultural Context
479///
480/// Indonesian calendar systems often incorporate auspiciousness concepts
481/// from various cultural and religious traditions:
482///
483/// - **Javanese calendar**: Balancing elements, market days, and spiritual aspects
484/// - **Balinese calendar**: Complex system of auspicious/inauspicious days
485/// - **Chinese integration**: Feng shui and zodiac considerations
486/// - **Islamic integration**: Religious observances and favorable timing
487///
488/// # Implementation Requirements
489///
490/// Calendar systems implementing this trait should:
491///
492/// 1. **Define Activity Types**: Specify which cultural activities are relevant
493/// 2. **Provide Auspiciousness Levels**: Use the standard 5-level system
494/// 3. **Implement Cultural Logic**: Apply authentic cultural rules and calculations
495/// 4. **Document Sources**: Reference cultural texts or expert knowledge
496///
497/// # Example Implementation
498///
499/// ```rust
500/// use calendar_core::{HasAuspiciousness, Activity, AuspiciousnessLevel};
501///
502/// struct MyCalendarDate {
503///     // Calendar date fields
504/// }
505///
506/// impl HasAuspiciousness for MyCalendarDate {
507///     type Activity = Activity;
508///     type AuspiciousnessLevel = AuspiciousnessLevel;
509///
510///     fn auspiciousness_for(&self, activity: &Activity) -> Self::AuspiciousnessLevel {
511///         match activity {
512///             Activity::Marriage => {
513///                 // Check if this date is auspicious for marriage
514///                 // Based on cultural rules and calculations
515///                 // Implementation would go here
516///                 AuspiciousnessLevel::Auspicious // Placeholder
517///             }
518///             Activity::Building => {
519///                 // Check auspiciousness for construction
520///                 AuspiciousnessLevel::Neutral // Placeholder
521///             }
522///             // Handle other activities...
523///             _ => AuspiciousnessLevel::Neutral,
524///         }
525///     }
526///
527///     fn is_auspicious_day(&self) -> bool {
528///         // General auspiciousness for the day
529///         // Implementation would calculate daily auspiciousness
530///         true // Placeholder
531///     }
532/// }
533/// ```
534///
535/// # Performance Considerations
536///
537/// - Auspiciousness calculations can be computationally intensive
538/// - Consider caching results for frequently accessed dates
539/// - Some calculations may depend on complex astronomical data
540/// - Balance accuracy with performance for real-time applications
541pub trait HasAuspiciousness {
542    /// Activity types that can be evaluated for auspiciousness
543    ///
544    /// This associated type defines which activities the calendar system
545    /// can evaluate for auspiciousness. Most implementations will use
546    /// the standard [`Activity`] enum, but custom calendars may define
547    /// their own activity types.
548    ///
549    /// # Recommended Types
550    ///
551    /// - Use the standard [`Activity`] enum for common Indonesian activities
552    /// - Extend with custom activities for specific calendar systems
553    /// - Consider cultural relevance when defining activity sets
554    type Activity;
555
556    /// Auspiciousness levels
557    ///
558    /// This associated type defines the levels of auspiciousness that
559    /// the calendar system can return. Most implementations should use
560    /// the standard [`AuspiciousnessLevel`] enum for consistency.
561    ///
562    /// # Standard Levels
563    ///
564    /// - `VeryAuspicious` - Extremely favorable for the activity
565    /// - `Auspicious` - Favorable for the activity
566    /// - `Neutral` - Neither favorable nor unfavorable
567    /// - `Inauspicious` - Unfavorable for the activity
568    /// - `VeryInauspicious` - Extremely unfavorable for the activity
569    type AuspiciousnessLevel;
570
571    /// Determine the auspiciousness level for a given activity
572    ///
573    /// This method evaluates how auspicious a specific date is for
574    /// performing a particular activity according to the calendar system's
575    /// cultural rules and traditions.
576    ///
577    /// # Arguments
578    /// * `activity` - The activity to evaluate for auspiciousness
579    ///
580    /// # Returns
581    /// Auspiciousness level for the given activity on this date
582    ///
583    /// # Implementation Guidelines
584    ///
585    /// - Apply authentic cultural rules and calculations
586    /// - Consider multiple factors (planetary positions, market days, etc.)
587    /// - Handle edge cases (conflicting auspiciousness indicators)
588    /// - Document the cultural basis for the calculations
589    ///
590    /// # Cultural Factors to Consider
591    ///
592    /// - **Astronomical**: Planetary positions, lunar phases
593    /// - **Cyclical**: Market days, elemental cycles
594    /// - **Religious**: Holy days, prayer times
595    /// - **Cultural**: Traditional beliefs, regional variations
596    fn auspiciousness_for(&self, activity: &Self::Activity) -> Self::AuspiciousnessLevel;
597
598    /// Check if a day is generally auspicious
599    ///
600    /// Provides a quick assessment of whether the day is generally
601    /// considered auspicious for most activities. This is useful for
602    /// applications that need a simple good/bad day assessment.
603    ///
604    /// # Returns
605    /// `true` if the day is generally auspicious, `false` otherwise
606    ///
607    /// # Implementation Notes
608    ///
609    /// - Should consider the overall auspiciousness of the day
610    /// - May combine multiple factors into a single assessment
611    /// - Useful for calendar displays and quick filters
612    /// - Should be consistent with detailed activity-specific evaluations
613    ///
614    /// # Default Logic
615    ///
616    /// A common approach is to consider a day auspicious if:
617    /// - It's neutral or better for most common activities
618    /// - It doesn't have major inauspicious indicators
619    /// - It aligns with favorable astronomical or cultural conditions
620    fn is_auspicious_day(&self) -> bool;
621}
622
623/// Macro for stub implementations
624///
625/// Used to mark features that are not yet implemented but
626/// have defined interfaces.
627#[macro_export]
628macro_rules! stub {
629    ($msg:expr) => {
630        return Err(CalendarError::NotImplemented($msg.to_string()))
631    };
632}
633
634/// Basic Gregorian to Julian Day Number conversion
635///
636/// Implements the standard Gregorian calendar algorithm
637/// from the Julian Day Number Wikipedia page and astronomical sources.
638///
639/// # Arguments
640/// * `year` - Gregorian year (CE, can be negative for BCE)
641/// * `month` - Gregorian month (1-12)
642/// * `day` - Gregorian day (1-31, depending on month)
643///
644/// # Returns
645/// Julian Day Number for the given Gregorian date
646#[must_use]
647pub fn gregorian_to_jdn(year: i32, month: u8, day: u8) -> JDN {
648    let (y, m) = if month <= 2 {
649        (year - 1, month + 12)
650    } else {
651        (year, month)
652    };
653
654    // Algorithm from: <https://en.wikipedia.org/wiki/Julian_day#Calculation>
655    // Cast m and day to i32 to avoid type issues
656    let m_i32 = i32::from(m);
657    let day_i32 = i32::from(day);
658
659    let jdn = (1461 * (y + 4800 + (m_i32 - 14) / 12)) / 4
660        + (367 * (m_i32 - 2 - 12 * ((m_i32 - 14) / 12))) / 12
661        - (3 * ((y + 4900 + (m_i32 - 14) / 12) / 100)) / 4
662        + day_i32
663        - 32075;
664
665    JDN::from(jdn)
666}
667
668/// Basic Julian Day Number to Gregorian conversion
669///
670/// Implements the canonical Fliegel & van Flandern (1968) algorithm
671/// from the U.S. Naval Observatory, which is the authoritative inverse
672/// of the Gregorian to JDN conversion.
673///
674/// # Arguments
675/// * `jdn` - Julian Day Number
676///
677/// # Returns
678/// Tuple of (year, month, day) in Gregorian calendar
679///
680/// # Panics
681/// Panics if JDN is outside the supported range for Gregorian conversion
682/// (approximately -2,147,483,648 to 2,147,483,647)
683///
684/// # Reference
685/// Fliegel, H. F. & van Flandern, T. C. 1968, "A Machine Algorithm for
686/// Processing Calendar Dates", Communications of the ACM, 11, 657.
687/// [https://aa.usno.navy.mil/faq/JD_formula](https://aa.usno.navy.mil/faq/JD_formula)
688#[must_use]
689pub fn jdn_to_gregorian(jdn: JDN) -> (i32, u8, u8) {
690    // Validate JDN is within i32 range to prevent overflow
691    assert!(
692        jdn >= JDN::from(i32::MIN) && jdn <= JDN::from(i32::MAX),
693        "JDN {jdn} is outside supported range for Gregorian conversion ({} to {})",
694        i32::MIN,
695        i32::MAX
696    );
697
698    // Fliegel & van Flandern algorithm (1968)
699    // Reference: U.S. Naval Observatory
700    #[allow(clippy::cast_possible_truncation)]
701    let jd = jdn as i32;
702    let l = jd + 68_569;
703    let n = (4 * l) / 146_097;
704    let l = l - (146_097 * n + 3) / 4;
705    let i = (4_000 * (l + 1)) / 1_461_001;
706    let l = l - (1_461 * i) / 4 + 31;
707    let j = (80 * l) / 2_447;
708    let day = l - (2_447 * j) / 80;
709    let l = j / 11;
710    let month = j + 2 - 12 * l;
711    let year = 100 * (n - 49) + i + l;
712
713    // Validate calculated values fit in u8 range
714    debug_assert!((1..=31).contains(&day), "Invalid day calculation: {day}");
715    debug_assert!(
716        (1..=12).contains(&month),
717        "Invalid month calculation: {month}"
718    );
719
720    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
721    {
722        (year, month as u8, day as u8)
723    }
724}
725
726#[cfg(test)]
727mod tests {
728    use super::*;
729    use alloc::string::ToString;
730
731    #[test]
732    fn test_gregorian_to_jdn_reform_anchor() {
733        // Gregorian reform anchor: October 15, 1582
734        let jdn = gregorian_to_jdn(1582, 10, 15);
735        assert_eq!(jdn, 2_299_161);
736    }
737
738    #[test]
739    fn test_gregorian_to_jdn_sultan_agung_epoch() {
740        // Sultan Agung epoch: July 8, 1633
741        let jdn = gregorian_to_jdn(1633, 7, 8);
742        assert_eq!(jdn, 2_317_690);
743    }
744
745    #[test]
746    fn test_jdn_to_gregorian_reform_anchor() {
747        // Test inverse of reform anchor
748        let (year, month, day) = jdn_to_gregorian(2_299_161);
749        assert_eq!((year, month, day), (1582, 10, 15));
750    }
751
752    #[test]
753    fn test_jdn_to_gregorian_sultan_agung_epoch() {
754        // Test inverse of Sultan Agung epoch
755        let (year, month, day) = jdn_to_gregorian(2_317_690);
756        assert_eq!((year, month, day), (1633, 7, 8));
757    }
758
759    #[test]
760    fn test_round_trip_conversions() {
761        // Test round-trip property for various dates
762        let test_dates = [
763            (2000, 1, 1),    // Y2K
764            (2024, 2, 29),   // Leap day
765            (1900, 3, 1),    // Non-leap year century
766            (1600, 1, 1),    // Leap year century
767            (1582, 10, 4),   // Last Julian day
768            (1582, 10, 15),  // First Gregorian day
769            (1, 1, 1),       // Early date
770            (-4713, 11, 24), // JDN epoch
771        ];
772
773        for (year, month, day) in test_dates {
774            let jdn = gregorian_to_jdn(year, month, day);
775            let (year2, month2, day2) = jdn_to_gregorian(jdn);
776            assert_eq!(
777                (year, month, day),
778                (year2, month2, day2),
779                "Round-trip failed for {year}-{month}-{day}"
780            );
781        }
782    }
783
784    #[test]
785    fn test_stub_macro() {
786        // Test that stub! returns the correct error
787        fn test_function() -> Result<(), CalendarError> {
788            stub!("test message");
789        }
790
791        let result = test_function();
792        assert!(result.is_err());
793        match result.unwrap_err() {
794            CalendarError::NotImplemented(msg) => {
795                assert_eq!(msg, "test message");
796            }
797            _ => panic!("Expected NotImplemented error"),
798        }
799    }
800
801    #[test]
802    fn test_calendar_error_display() {
803        let error = CalendarError::OutOfRange("test range".to_string());
804        assert_eq!(error.to_string(), "Date out of supported range: test range");
805
806        let error = CalendarError::InvalidParameters("test params".to_string());
807        assert_eq!(
808            error.to_string(),
809            "Invalid calendar parameters: test params"
810        );
811
812        let error = CalendarError::NotImplemented("test feature".to_string());
813        assert_eq!(
814            error.to_string(),
815            "Feature not yet implemented: test feature"
816        );
817
818        let error = CalendarError::ArithmeticError("test math".to_string());
819        assert_eq!(error.to_string(), "Arithmetic error: test math");
820    }
821
822    #[test]
823    fn test_activity_enum() {
824        let activity = Activity::Marriage;
825        assert!(activity.description().contains("Marriage"));
826
827        let custom = Activity::Custom("Custom activity".to_string());
828        assert_eq!(custom.description(), "Custom activity");
829    }
830
831    #[test]
832    fn test_auspiciousness_level_enum() {
833        let level = AuspiciousnessLevel::Auspicious;
834        assert!(level.is_auspicious());
835        assert!(!level.is_very_auspicious());
836        assert!(!level.is_inauspicious());
837        assert!(!level.is_very_inauspicious());
838        assert!(!level.is_neutral());
839
840        let level = AuspiciousnessLevel::VeryAuspicious;
841        assert!(level.is_auspicious());
842        assert!(level.is_very_auspicious());
843        assert!(!level.is_inauspicious());
844        assert!(!level.is_neutral());
845
846        let level = AuspiciousnessLevel::Neutral;
847        assert!(!level.is_auspicious());
848        assert!(!level.is_inauspicious());
849        assert!(!level.is_very_auspicious());
850        assert!(!level.is_very_inauspicious());
851        assert!(level.is_neutral());
852
853        let level = AuspiciousnessLevel::Inauspicious;
854        assert!(!level.is_auspicious());
855        assert!(level.is_inauspicious());
856        assert!(!level.is_very_inauspicious());
857        assert!(!level.is_neutral());
858
859        let level = AuspiciousnessLevel::VeryInauspicious;
860        assert!(!level.is_auspicious());
861        assert!(level.is_inauspicious());
862        assert!(level.is_very_inauspicious());
863        assert!(!level.is_neutral());
864    }
865
866    #[test]
867    fn test_type_definitions() {
868        // Test that type aliases work correctly
869        let jdn: JDN = 2_451_545;
870        let cycle_year: CycleYear = 60;
871        let sub_year: SubYearPosition = 12;
872
873        assert_eq!(jdn, 2_451_545);
874        assert_eq!(cycle_year, 60);
875        assert_eq!(sub_year, 12);
876    }
877}