1#[cfg(test)]
2use std::ffi::CStr;
3use std::ffi::{c_char, c_double, c_int};
4use std::sync::OnceLock;
5
6use xalen_ayanamsa::Ayanamsa;
7use xalen_coords::obliquity::mean_obliquity;
8use xalen_ephem::{Almanac, Body};
9use xalen_houses::{GeoLocation, HouseSystem, compute_houses};
10use xalen_time::{JdUT1, JulianDay};
11
12static ALMANAC: OnceLock<Almanac> = OnceLock::new();
13
14fn get_almanac() -> &'static Almanac {
15 ALMANAC.get_or_init(Almanac::default_vedic)
16}
17
18#[repr(C)]
19pub struct XalenPosition {
20 pub longitude_deg: c_double,
21 pub latitude_deg: c_double,
22 pub distance_au: c_double,
23 pub status: c_int,
24}
25
26#[repr(C)]
33pub struct XalenPositionFull {
34 pub longitude_deg: c_double,
35 pub latitude_deg: c_double,
36 pub distance_au: c_double,
37 pub lon_speed_deg: c_double,
38 pub lat_speed_deg: c_double,
39 pub dist_speed_au: c_double,
40 pub is_retrograde: c_int,
41 pub status: c_int,
42}
43
44#[repr(C)]
45pub struct XalenHouses {
46 pub cusps: [c_double; 12],
47 pub ascendant_deg: c_double,
48 pub mc_deg: c_double,
49 pub status: c_int,
50}
51
52const XALEN_FFI_PANIC: c_int = -99;
57const XALEN_FFI_PANIC_F64: c_double = -99.0;
61
62fn guard_int(f: impl FnOnce() -> c_int + std::panic::UnwindSafe, sentinel: c_int) -> c_int {
65 std::panic::catch_unwind(f).unwrap_or(sentinel)
66}
67
68fn guard_f64(
70 f: impl FnOnce() -> c_double + std::panic::UnwindSafe,
71 sentinel: c_double,
72) -> c_double {
73 std::panic::catch_unwind(f).unwrap_or(sentinel)
74}
75
76#[unsafe(no_mangle)]
80pub extern "C" fn xalen_init() -> c_int {
81 guard_int(
82 || {
83 let _ = get_almanac();
84 0
85 },
86 XALEN_FFI_PANIC,
87 )
88}
89
90fn body_from_id(body_id: c_int) -> Option<Body> {
96 match body_id {
97 0 => Some(Body::Sun),
98 1 => Some(Body::Moon),
99 2 => Some(Body::Mercury),
100 3 => Some(Body::Venus),
101 4 => Some(Body::Mars),
102 5 => Some(Body::Jupiter),
103 6 => Some(Body::Saturn),
104 7 => Some(Body::Uranus),
105 8 => Some(Body::Neptune),
106 9 => Some(Body::MeanNode),
107 10 => Some(Body::TrueNode),
108 11 => Some(Body::Pluto),
109 12 => Some(Body::Chiron),
110 _ => None,
112 }
113}
114
115fn ayanamsa_from_id(id: c_int) -> Option<Ayanamsa> {
122 match id {
123 0 => Some(Ayanamsa::Lahiri),
124 1 => Some(Ayanamsa::KPKrishnamurti),
125 2 => Some(Ayanamsa::Raman),
126 3 => Some(Ayanamsa::FaganBradley),
127 4 => Some(Ayanamsa::TrueChitra),
128 5 => Some(Ayanamsa::TrueRevati),
129 6 => Some(Ayanamsa::SuryaSiddhanta),
130 7 => Some(Ayanamsa::YukteswarSriSS),
131 8 => Some(Ayanamsa::JNBhasin),
132 9 => Some(Ayanamsa::DeLuce),
133 10 => Some(Ayanamsa::Ushashashi),
134 11 => Some(Ayanamsa::PushyaPaksha),
135 12 => Some(Ayanamsa::GalacticCenter0Sag),
136 13 => Some(Ayanamsa::LahiriICRC),
137 14 => Some(Ayanamsa::KPStraightLine),
138 15 => Some(Ayanamsa::Hipparchos),
139 16 => Some(Ayanamsa::LahiriVP285),
140 _ => None,
141 }
142}
143
144fn house_system_from_id(id: c_int) -> Option<HouseSystem> {
150 match id {
151 0 => Some(HouseSystem::WholeSign),
152 1 => Some(HouseSystem::Equal),
153 2 => Some(HouseSystem::Placidus),
154 3 => Some(HouseSystem::Koch),
155 4 => Some(HouseSystem::Porphyry),
156 5 => Some(HouseSystem::Regiomontanus),
157 6 => Some(HouseSystem::Campanus),
158 7 => Some(HouseSystem::Morinus),
159 8 => Some(HouseSystem::Alcabitius),
160 9 => Some(HouseSystem::Topocentric),
161 10 => Some(HouseSystem::Sripati),
162 11 => Some(HouseSystem::Vehlow),
163 12 => Some(HouseSystem::Meridian),
164 13 => Some(HouseSystem::KrusinskiPisa),
165 _ => None,
166 }
167}
168
169#[unsafe(no_mangle)]
178pub unsafe extern "C" fn xalen_planet_position(
179 jd_ut1: c_double,
180 body_id: c_int,
181 out: *mut XalenPosition,
182) -> c_int {
183 if out.is_null() {
184 return -1;
185 }
186
187 unsafe {
189 std::ptr::write_bytes(out, 0, 1);
190 }
191
192 let out_guard = std::panic::AssertUnwindSafe(out);
197 let result = std::panic::catch_unwind(move || {
198 let out = out_guard.0;
199 if !jd_ut1.is_finite() {
201 unsafe {
202 (*out).status = -2;
203 }
204 return -2;
205 }
206
207 if body_id == 13 {
212 let almanac = get_almanac();
213 return match almanac.geocentric_ecliptic(Body::MeanNode, JdUT1(jd_ut1)) {
214 Ok(pos) => {
215 unsafe {
216 (*out).longitude_deg =
217 (pos.longitude.to_degrees() + 180.0).rem_euclid(360.0);
218 (*out).latitude_deg = -pos.latitude.to_degrees();
219 (*out).distance_au = pos.distance;
220 (*out).status = 0;
221 }
222 0
223 }
224 Err(_) => {
225 unsafe {
226 (*out).status = -3;
227 }
228 -3
229 }
230 };
231 }
232
233 let body = match body_from_id(body_id) {
234 Some(b) => b,
235 None => {
236 unsafe {
237 (*out).status = -2;
238 }
239 return -2;
240 }
241 };
242
243 let almanac = get_almanac();
244 match almanac.geocentric_ecliptic(body, JdUT1(jd_ut1)) {
245 Ok(pos) => {
246 unsafe {
247 (*out).longitude_deg = pos.longitude.to_degrees().rem_euclid(360.0);
248 (*out).latitude_deg = pos.latitude.to_degrees();
249 (*out).distance_au = pos.distance;
250 (*out).status = 0;
251 }
252 0
253 }
254 Err(_) => {
255 unsafe {
256 (*out).status = -3;
257 }
258 -3
259 }
260 }
261 });
262
263 match result {
264 Ok(code) => code,
265 Err(_) => {
266 unsafe {
268 (*out).status = XALEN_FFI_PANIC;
269 }
270 XALEN_FFI_PANIC
271 }
272 }
273}
274
275#[unsafe(no_mangle)]
289pub unsafe extern "C" fn xalen_planet_position_full(
290 jd_ut1: c_double,
291 body_id: c_int,
292 out: *mut XalenPositionFull,
293) -> c_int {
294 if out.is_null() {
295 return -1;
296 }
297
298 unsafe {
300 std::ptr::write_bytes(out, 0, 1);
301 }
302
303 let out_guard = std::panic::AssertUnwindSafe(out);
304 let result = std::panic::catch_unwind(move || {
305 let out = out_guard.0;
306 if !jd_ut1.is_finite() {
307 unsafe {
308 (*out).status = -2;
309 }
310 return -2;
311 }
312
313 let (body, ketu_shift) = if body_id == 13 {
315 (Body::MeanNode, true)
316 } else {
317 match body_from_id(body_id) {
318 Some(b) => (b, false),
319 None => {
320 unsafe {
321 (*out).status = -2;
322 }
323 return -2;
324 }
325 }
326 };
327
328 let almanac = get_almanac();
329 let jd = JdUT1(jd_ut1);
330 let pos = match almanac.geocentric_ecliptic(body, jd) {
331 Ok(p) => p,
332 Err(_) => {
333 unsafe {
334 (*out).status = -3;
335 }
336 return -3;
337 }
338 };
339 let speed = match almanac.geocentric_speed(body, jd) {
340 Ok(s) => s,
341 Err(_) => {
342 unsafe {
343 (*out).status = -3;
344 }
345 return -3;
346 }
347 };
348
349 let mut longitude = pos.longitude.to_degrees().rem_euclid(360.0);
350 let mut latitude = pos.latitude.to_degrees();
351 if ketu_shift {
352 longitude = (longitude + 180.0).rem_euclid(360.0);
353 latitude = -latitude; }
355
356 unsafe {
357 (*out).longitude_deg = longitude;
358 (*out).latitude_deg = latitude;
359 (*out).distance_au = pos.distance;
360 (*out).lon_speed_deg = speed.longitude_deg_per_day();
361 (*out).lat_speed_deg = speed.latitude_deg_per_day();
362 (*out).dist_speed_au = speed.distance;
363 (*out).is_retrograde = c_int::from(speed.longitude < 0.0);
364 (*out).status = 0;
365 }
366 0
367 });
368
369 match result {
370 Ok(code) => code,
371 Err(_) => {
372 unsafe {
373 (*out).status = XALEN_FFI_PANIC;
374 }
375 XALEN_FFI_PANIC
376 }
377 }
378}
379
380#[unsafe(no_mangle)]
385pub extern "C" fn xalen_sidereal_longitude(
386 jd_ut1: c_double,
387 body_id: c_int,
388 ayanamsa_id: c_int,
389) -> c_double {
390 guard_f64(
391 move || {
392 if !jd_ut1.is_finite() {
394 return -2.0;
395 }
396
397 let ayanamsa = match ayanamsa_from_id(ayanamsa_id) {
398 Some(a) => a,
399 None => return -2.0,
400 };
401
402 let almanac = get_almanac();
403 let jd = JdUT1(jd_ut1);
404 let tt = jd.to_tt(&xalen_time::DeltaTModel::StephensonMorrisonHohenkerk2016);
405 let aya_deg = ayanamsa.compute_deg(tt.as_f64());
406
407 if body_id == 13 {
409 return match almanac.sidereal_longitude_deg(Body::MeanNode, jd, aya_deg) {
410 Ok(lon) => (lon + 180.0).rem_euclid(360.0),
411 Err(_) => -1.0,
412 };
413 }
414
415 let body = match body_from_id(body_id) {
416 Some(b) => b,
417 None => return -1.0,
418 };
419
420 almanac
421 .sidereal_longitude_deg(body, jd, aya_deg)
422 .unwrap_or(-1.0)
423 },
424 XALEN_FFI_PANIC_F64,
425 )
426}
427
428#[unsafe(no_mangle)]
438pub unsafe extern "C" fn xalen_houses(
439 jd_ut1: c_double,
440 latitude_deg: c_double,
441 longitude_deg: c_double,
442 system_id: c_int,
443 out: *mut XalenHouses,
444) -> c_int {
445 if out.is_null() {
446 return -1;
447 }
448
449 unsafe {
451 std::ptr::write_bytes(out, 0, 1);
452 }
453
454 let out_guard = std::panic::AssertUnwindSafe(out);
457 let result = std::panic::catch_unwind(move || {
458 let out = out_guard.0;
459 if !jd_ut1.is_finite()
463 || !latitude_deg.is_finite()
464 || !longitude_deg.is_finite()
465 || !(-90.0..=90.0).contains(&latitude_deg)
466 {
467 unsafe {
468 (*out).status = -2;
469 }
470 return -2;
471 }
472
473 let system = match house_system_from_id(system_id) {
474 Some(s) => s,
475 None => {
476 unsafe {
477 (*out).status = -2;
478 }
479 return -2;
480 }
481 };
482
483 let loc = GeoLocation::new(latitude_deg, longitude_deg);
484 let t = (jd_ut1 - 2_451_545.0) / 36525.0;
485 let epsilon = mean_obliquity(t);
486 let houses = compute_houses(jd_ut1, &loc, epsilon, system);
487
488 unsafe {
489 for i in 0..12 {
490 (*out).cusps[i] = houses.cusp_deg(i);
491 }
492 (*out).ascendant_deg = houses.ascendant.to_degrees().rem_euclid(360.0);
493 (*out).mc_deg = houses.mc.to_degrees().rem_euclid(360.0);
494 (*out).status = 0;
495 }
496 0
497 });
498
499 match result {
500 Ok(code) => code,
501 Err(_) => {
502 unsafe {
503 (*out).status = XALEN_FFI_PANIC;
504 }
505 XALEN_FFI_PANIC
506 }
507 }
508}
509
510#[unsafe(no_mangle)]
517pub extern "C" fn xalen_version() -> *const c_char {
518 let ptr = std::panic::catch_unwind(|| {
521 concat!("XALEN Ephemeris ", env!("CARGO_PKG_VERSION"), "\0").as_ptr() as usize
522 })
523 .unwrap_or(0);
524 ptr as *const c_char
525}
526
527#[unsafe(no_mangle)]
530pub extern "C" fn xalen_ayanamsa(jd_ut1: c_double, ayanamsa_id: c_int) -> c_double {
531 guard_f64(
532 move || {
533 let ayanamsa = match ayanamsa_from_id(ayanamsa_id) {
534 Some(a) => a,
535 None => return -1.0,
536 };
537 if !jd_ut1.is_finite() {
540 return -1.0;
541 }
542 let jd = JdUT1(jd_ut1);
543 let tt = jd.to_tt(&xalen_time::DeltaTModel::StephensonMorrisonHohenkerk2016);
544 ayanamsa.compute_deg(tt.as_f64())
545 },
546 XALEN_FFI_PANIC_F64,
547 )
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 #[test]
555 fn init_succeeds() {
556 assert_eq!(xalen_init(), 0);
557 }
558
559 #[test]
560 fn planet_position_sun() {
561 xalen_init();
562 let mut pos = XalenPosition {
563 longitude_deg: 0.0,
564 latitude_deg: 0.0,
565 distance_au: 0.0,
566 status: -1,
567 };
568 let ret = unsafe { xalen_planet_position(2451545.0, 0, &mut pos) };
569 assert_eq!(ret, 0);
570 assert_eq!(pos.status, 0);
571 assert!(pos.longitude_deg >= 0.0 && pos.longitude_deg < 360.0);
572 }
573
574 #[test]
575 fn all_body_ids_valid() {
576 xalen_init();
577 for id in 0..=12 {
579 let mut pos = XalenPosition {
580 longitude_deg: 0.0,
581 latitude_deg: 0.0,
582 distance_au: 0.0,
583 status: -1,
584 };
585 let ret = unsafe { xalen_planet_position(2451545.0, id, &mut pos) };
586 assert_eq!(ret, 0, "Body ID {id} should succeed");
587 assert!(
588 pos.longitude_deg >= 0.0 && pos.longitude_deg < 360.0,
589 "Body {id} longitude should be [0,360), got {}",
590 pos.longitude_deg
591 );
592 }
593 }
594
595 #[test]
596 fn ketu_body_id_13() {
597 xalen_init();
598 let mut rahu_pos = XalenPosition {
599 longitude_deg: 0.0,
600 latitude_deg: 0.0,
601 distance_au: 0.0,
602 status: -1,
603 };
604 unsafe { xalen_planet_position(2451545.0, 9, &mut rahu_pos) }; let mut ketu_pos = XalenPosition {
606 longitude_deg: 0.0,
607 latitude_deg: 0.0,
608 distance_au: 0.0,
609 status: -1,
610 };
611 let ret = unsafe { xalen_planet_position(2451545.0, 13, &mut ketu_pos) }; assert_eq!(ret, 0, "Ketu (body 13) should succeed");
613 let expected = (rahu_pos.longitude_deg + 180.0).rem_euclid(360.0);
614 assert!(
615 (ketu_pos.longitude_deg - expected).abs() < 1e-10,
616 "Ketu should be Rahu+180: expected {expected}, got {}",
617 ketu_pos.longitude_deg
618 );
619 assert!(
623 (ketu_pos.latitude_deg + rahu_pos.latitude_deg).abs() < 1e-10,
624 "Ketu latitude {} should be −Rahu latitude {}",
625 ketu_pos.latitude_deg,
626 rahu_pos.latitude_deg
627 );
628 }
629
630 #[test]
631 fn planet_position_full_six_tuple_and_speed() {
632 xalen_init();
633 let mut p = XalenPositionFull {
634 longitude_deg: 0.0,
635 latitude_deg: 0.0,
636 distance_au: 0.0,
637 lon_speed_deg: 0.0,
638 lat_speed_deg: 0.0,
639 dist_speed_au: 0.0,
640 is_retrograde: -1,
641 status: -1,
642 };
643 let ret = unsafe { xalen_planet_position_full(2451545.0, 0, &mut p) }; assert_eq!(ret, 0);
645 assert_eq!(p.status, 0);
646 assert!(p.longitude_deg >= 0.0 && p.longitude_deg < 360.0);
647 assert!(
650 (p.lon_speed_deg - 1.019432).abs() < 0.01,
651 "Sun lon_speed {} vs pyswisseph 1.019432",
652 p.lon_speed_deg
653 );
654 assert!(
655 (p.distance_au - 0.98332764).abs() < 1e-3,
656 "Sun distance {} vs pyswisseph 0.98332764",
657 p.distance_au
658 );
659 assert_eq!(p.is_retrograde, 0, "Sun is never retrograde");
660 }
661
662 #[test]
663 fn planet_position_full_node_retrograde_and_ketu() {
664 xalen_init();
665 let mut rahu = XalenPositionFull {
667 longitude_deg: 0.0,
668 latitude_deg: 0.0,
669 distance_au: 0.0,
670 lon_speed_deg: 0.0,
671 lat_speed_deg: 0.0,
672 dist_speed_au: 0.0,
673 is_retrograde: -1,
674 status: -1,
675 };
676 let ret = unsafe { xalen_planet_position_full(2451545.0, 9, &mut rahu) };
677 assert_eq!(ret, 0);
678 assert_eq!(rahu.is_retrograde, 1, "mean node is always retrograde");
679 assert!(
680 rahu.lon_speed_deg < 0.0,
681 "node lon_speed {} should be < 0",
682 rahu.lon_speed_deg
683 );
684
685 let mut ketu = XalenPositionFull {
687 longitude_deg: 0.0,
688 latitude_deg: 0.0,
689 distance_au: 0.0,
690 lon_speed_deg: 0.0,
691 lat_speed_deg: 0.0,
692 dist_speed_au: 0.0,
693 is_retrograde: -1,
694 status: -1,
695 };
696 let ret = unsafe { xalen_planet_position_full(2451545.0, 13, &mut ketu) };
697 assert_eq!(ret, 0);
698 let expected = (rahu.longitude_deg + 180.0).rem_euclid(360.0);
699 assert!(
700 (ketu.longitude_deg - expected).abs() < 1e-9,
701 "Ketu lon {} != Rahu+180 {}",
702 ketu.longitude_deg,
703 expected
704 );
705 assert!(
708 (ketu.latitude_deg + rahu.latitude_deg).abs() < 1e-9,
709 "Ketu latitude {} should be −Rahu latitude {}",
710 ketu.latitude_deg,
711 rahu.latitude_deg
712 );
713 assert_eq!(
714 ketu.is_retrograde, rahu.is_retrograde,
715 "Ketu shares Rahu retrograde"
716 );
717 }
718
719 #[test]
720 fn planet_position_full_rejects_bad_input() {
721 xalen_init();
722 let ret = unsafe { xalen_planet_position_full(2451545.0, 0, std::ptr::null_mut()) };
724 assert_eq!(ret, -1);
725 let mut p = XalenPositionFull {
727 longitude_deg: 0.0,
728 latitude_deg: 0.0,
729 distance_au: 0.0,
730 lon_speed_deg: 0.0,
731 lat_speed_deg: 0.0,
732 dist_speed_au: 0.0,
733 is_retrograde: -1,
734 status: 0,
735 };
736 assert_eq!(
737 unsafe { xalen_planet_position_full(f64::NAN, 0, &mut p) },
738 -2
739 );
740 assert_eq!(p.status, -2);
741 assert_eq!(
742 unsafe { xalen_planet_position_full(2451545.0, 99, &mut p) },
743 -2
744 );
745 assert_eq!(p.status, -2);
746 }
747
748 #[test]
749 fn sidereal_longitude_reasonable() {
750 xalen_init();
751 let lon = xalen_sidereal_longitude(2451545.0, 0, 0); assert!(
753 lon >= 0.0 && lon < 360.0,
754 "Sidereal Sun should be 0-360°, got {lon}°"
755 );
756 }
757
758 #[test]
759 fn sidereal_all_ayanamsa_ids_valid() {
760 xalen_init();
761 for id in 0..=16 {
762 let lon = xalen_sidereal_longitude(2451545.0, 0, id); assert!(
764 lon >= 0.0 && lon < 360.0,
765 "Ayanamsa ID {id} should produce valid sidereal lon, got {lon}"
766 );
767 }
768 }
769
770 #[test]
771 fn sidereal_invalid_ayanamsa_returns_error() {
772 let lon = xalen_sidereal_longitude(2451545.0, 0, 99);
773 assert!(
774 lon < 0.0,
775 "Invalid ayanamsa should return negative, got {lon}"
776 );
777 }
778
779 #[test]
780 fn sidereal_ketu_body_13() {
781 xalen_init();
782 let rahu_lon = xalen_sidereal_longitude(2451545.0, 9, 0); let ketu_lon = xalen_sidereal_longitude(2451545.0, 13, 0); assert!(
785 ketu_lon >= 0.0 && ketu_lon < 360.0,
786 "Ketu sidereal should be [0,360), got {ketu_lon}"
787 );
788 let expected = (rahu_lon + 180.0).rem_euclid(360.0);
789 assert!(
790 (ketu_lon - expected).abs() < 1e-10,
791 "Ketu sidereal should be Rahu+180: expected {expected}, got {ketu_lon}"
792 );
793 }
794
795 #[test]
796 fn houses_compute() {
797 xalen_init();
798 let mut h = XalenHouses {
799 cusps: [0.0; 12],
800 ascendant_deg: 0.0,
801 mc_deg: 0.0,
802 status: -1,
803 };
804 let ret = unsafe { xalen_houses(2451545.0, 18.52, 73.85, 0, &mut h) };
805 assert_eq!(ret, 0);
806 assert_eq!(h.status, 0);
807 assert!(h.ascendant_deg >= 0.0 && h.ascendant_deg < 360.0);
808 }
809
810 #[test]
811 fn houses_all_system_ids_valid() {
812 xalen_init();
813 for id in 0..=13 {
814 let mut h = XalenHouses {
815 cusps: [0.0; 12],
816 ascendant_deg: 0.0,
817 mc_deg: 0.0,
818 status: -1,
819 };
820 let ret = unsafe { xalen_houses(2451545.0, 18.52, 73.85, id, &mut h) };
821 assert_eq!(ret, 0, "House system ID {id} should succeed");
822 assert!(
823 h.ascendant_deg >= 0.0 && h.ascendant_deg < 360.0,
824 "House system {id} should produce valid ascendant"
825 );
826 }
827 }
828
829 #[test]
830 fn houses_invalid_system_returns_error() {
831 let mut h = XalenHouses {
832 cusps: [0.0; 12],
833 ascendant_deg: 0.0,
834 mc_deg: 0.0,
835 status: 0,
836 };
837 let ret = unsafe { xalen_houses(2451545.0, 18.52, 73.85, 99, &mut h) };
838 assert_eq!(ret, -2, "Invalid house system should return -2");
839 }
840
841 #[test]
842 fn invalid_body_returns_error() {
843 let mut pos = XalenPosition {
844 longitude_deg: 0.0,
845 latitude_deg: 0.0,
846 distance_au: 0.0,
847 status: 0,
848 };
849 let ret = unsafe { xalen_planet_position(2451545.0, 99, &mut pos) };
850 assert_eq!(ret, -2);
851 }
852
853 #[test]
854 fn null_pointer_returns_error() {
855 let ret = unsafe { xalen_planet_position(2451545.0, 0, std::ptr::null_mut()) };
856 assert_eq!(ret, -1);
857 }
858
859 #[test]
860 fn ayanamsa_at_j2000() {
861 let aya = xalen_ayanamsa(2451545.0, 0);
862 assert!(
863 (aya - 23.85).abs() < 0.1,
864 "Lahiri at J2000 should be ~23.85°, got {aya}°"
865 );
866 }
867
868 #[test]
869 fn ayanamsa_all_ids_valid() {
870 for id in 0..=16 {
871 let aya = xalen_ayanamsa(2451545.0, id);
872 assert!(
873 aya.is_finite() && aya > 0.0,
874 "Ayanamsa ID {id} should produce finite positive value, got {aya}"
875 );
876 }
877 }
878
879 #[test]
880 fn ayanamsa_invalid_id_returns_error() {
881 let aya = xalen_ayanamsa(2451545.0, 99);
882 assert!(
883 aya < 0.0,
884 "Invalid ayanamsa ID should return negative, got {aya}"
885 );
886 }
887
888 #[test]
889 fn ayanamsa_non_finite_jd_returns_error() {
890 assert_eq!(xalen_ayanamsa(f64::NAN, 0), -1.0);
893 assert_eq!(xalen_ayanamsa(f64::INFINITY, 0), -1.0);
894 }
895
896 #[test]
897 fn houses_reject_non_finite_and_out_of_range_lat() {
898 xalen_init();
899 let bad_inputs: [(c_double, c_double, c_double); 9] = [
903 (f64::NAN, 18.52, 73.85), (f64::INFINITY, 18.52, 73.85), (2451545.0, f64::NAN, 73.85), (2451545.0, f64::INFINITY, 73.85), (2451545.0, 18.52, f64::NAN), (2451545.0, 18.52, f64::INFINITY), (2451545.0, 18.52, f64::NEG_INFINITY), (2451545.0, 91.0, 73.85), (2451545.0, -90.5, 73.85), ];
913 for (jd, lat, lon) in bad_inputs {
914 let mut h = XalenHouses {
915 cusps: [0.0; 12],
916 ascendant_deg: 0.0,
917 mc_deg: 0.0,
918 status: 0,
919 };
920 let ret = unsafe { xalen_houses(jd, lat, lon, 0, &mut h) };
921 assert_eq!(
922 ret, -2,
923 "jd={jd} lat={lat} lon={lon} should be rejected with -2"
924 );
925 assert_eq!(
926 h.status, -2,
927 "status should be -2 for jd={jd} lat={lat} lon={lon}"
928 );
929 }
930
931 let mut h = XalenHouses {
933 cusps: [0.0; 12],
934 ascendant_deg: 0.0,
935 mc_deg: 0.0,
936 status: -1,
937 };
938 let ret = unsafe { xalen_houses(2451545.0, 18.52, 540.0, 0, &mut h) };
939 assert_eq!(ret, 0, "Periodic longitude 540.0 should be accepted");
940 assert_eq!(h.status, 0);
941 }
942
943 #[test]
944 fn planet_position_rejects_non_finite_jd() {
945 xalen_init();
946 for jd in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
947 let mut pos = XalenPosition {
948 longitude_deg: 0.0,
949 latitude_deg: 0.0,
950 distance_au: 0.0,
951 status: 0,
952 };
953 let ret = unsafe { xalen_planet_position(jd, 0, &mut pos) };
954 assert_eq!(ret, -2, "Non-finite jd={jd} should return -2");
955 assert_eq!(pos.status, -2, "status should be -2 for jd={jd}");
956 }
957 let mut pos = XalenPosition {
959 longitude_deg: 0.0,
960 latitude_deg: 0.0,
961 distance_au: 0.0,
962 status: 0,
963 };
964 let ret = unsafe { xalen_planet_position(f64::NAN, 13, &mut pos) };
965 assert_eq!(ret, -2, "Non-finite jd on Ketu path should return -2");
966 assert_eq!(pos.status, -2);
967 }
968
969 #[test]
970 fn sidereal_longitude_rejects_non_finite_jd() {
971 xalen_init();
972 for jd in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
973 let lon = xalen_sidereal_longitude(jd, 0, 0);
974 assert_eq!(lon, -2.0, "Non-finite jd={jd} should return -2.0");
975 }
976 }
977
978 #[test]
979 fn panic_sentinels_are_distinct_from_normal_errors() {
980 assert_ne!(XALEN_FFI_PANIC, -1);
984 assert_ne!(XALEN_FFI_PANIC, -2);
985 assert_ne!(XALEN_FFI_PANIC, -3);
986 assert!((XALEN_FFI_PANIC_F64 - (-1.0)).abs() > 0.5);
988 assert!((XALEN_FFI_PANIC_F64 - (-2.0)).abs() > 0.5);
989 }
990
991 #[test]
992 fn catch_unwind_wrappers_preserve_normal_returns() {
993 assert_eq!(xalen_init(), 0);
996 let mut pos = XalenPosition {
997 longitude_deg: 0.0,
998 latitude_deg: 0.0,
999 distance_au: 0.0,
1000 status: -1,
1001 };
1002 assert_eq!(unsafe { xalen_planet_position(2451545.0, 0, &mut pos) }, 0);
1003 assert_eq!(pos.status, 0);
1004 let aya = xalen_ayanamsa(2451545.0, 0);
1005 assert!(aya.is_finite() && aya > 0.0);
1006 let lon = xalen_sidereal_longitude(2451545.0, 0, 0);
1007 assert!(lon >= 0.0 && lon < 360.0);
1008 }
1009
1010 #[test]
1011 fn version_string() {
1012 let v = xalen_version();
1013 assert!(!v.is_null());
1014 let s = unsafe { CStr::from_ptr(v) }.to_str().unwrap();
1015 assert!(
1016 s.contains("XALEN"),
1017 "Version should contain XALEN, got '{s}'"
1018 );
1019 assert!(
1021 s.contains(env!("CARGO_PKG_VERSION")),
1022 "Version should contain the crate version {}, got '{s}'",
1023 env!("CARGO_PKG_VERSION")
1024 );
1025 }
1026}