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}