diff --git a/Cargo.lock b/Cargo.lock index 2e9e648..90a5b1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "chrono" -version = "0.4.43" +version = "0.4.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" dependencies = [ "iana-time-zone", "js-sys", @@ -382,9 +382,9 @@ dependencies = [ [[package]] name = "qtty" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec18b86dcb4c777f979a171699a5f552675bbbfd7406d3d002403a26d7bd3215" +checksum = "b42948ee9aba0ed2217fd0d89a666df2ab020e3e25ef9b0328150e6d6c0aaee3" dependencies = [ "qtty-core", "qtty-derive", @@ -392,9 +392,9 @@ dependencies = [ [[package]] name = "qtty-core" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02271e8e4889de336896d2b690ad40ef811e68a81726885a7ddfbccf7e35b19c" +checksum = "283c549662e44f8a2848964e9639f2924f7a442f94294d508fc70c531a9717b3" dependencies = [ "libm", "qtty-derive", @@ -403,15 +403,29 @@ dependencies = [ [[package]] name = "qtty-derive" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6b1435ce1b31534c420c9b7c08dc7cf1d4a39f18748e7c7f29b71296d558a51" +checksum = "5edb55d59461af47a984baae7fafc2374dac2a79d41434a77ee7807f0f592757" dependencies = [ "proc-macro2", "quote", "syn", ] +[[package]] +name = "qtty-ffi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93de7d3db9abe1006b8a8b635de51f6d967636be144d06c28f66c3b8a49bd17" +dependencies = [ + "cbindgen", + "qtty", + "quote", + "serde", + "serde_json", + "syn", +] + [[package]] name = "quote" version = "1.0.44" @@ -567,6 +581,7 @@ dependencies = [ "cbindgen", "chrono", "qtty", + "qtty-ffi", "serde_json", "tempoch", ] diff --git a/tempoch-core/Cargo.toml b/tempoch-core/Cargo.toml index 83366ea..13dce94 100644 --- a/tempoch-core/Cargo.toml +++ b/tempoch-core/Cargo.toml @@ -17,7 +17,7 @@ path = "src/lib.rs" [dependencies] chrono = "0.4.43" -qtty = "0.3.0" +qtty = "0.3.1" serde = { version = "1.0", default-features = false, features = ["derive"], optional = true } [dev-dependencies] diff --git a/tempoch-ffi/Cargo.toml b/tempoch-ffi/Cargo.toml index 11abe86..d84e80a 100644 --- a/tempoch-ffi/Cargo.toml +++ b/tempoch-ffi/Cargo.toml @@ -17,8 +17,9 @@ serde = ["tempoch/serde", "dep:serde_json"] [dependencies] tempoch = { path = "../tempoch", version = "0.3.0" } -qtty = "0.3.0" -chrono = "0.4.43" +qtty = "0.3.1" +qtty-ffi = "0.3.1" +chrono = "0.4.44" serde_json = { version = "1.0", optional = true } [build-dependencies] diff --git a/tempoch-ffi/cbindgen.toml b/tempoch-ffi/cbindgen.toml index 10247b1..42c6ed0 100644 --- a/tempoch-ffi/cbindgen.toml +++ b/tempoch-ffi/cbindgen.toml @@ -4,16 +4,23 @@ autogen_warning = "/* Warning: this file is auto-generated by cbindgen. Do not m documentation_style = "c99" style = "both" cpp_compat = true +after_includes = """ +/* QttyQuantity and UnitId definitions come from qtty_ffi.h */ +#include "qtty_ffi.h" +""" [defines] "feature = serde" = "TEMPOCH_FFI_HAS_SERDE" [export] +# Exclude qtty-ffi types — they come from qtty_ffi.h (included via after_includes) +exclude = ["QttyQuantity", "UnitId", "DimensionId", "QttyDerivedQuantity"] [export.rename] "TempochStatus" = "tempoch_status_t" "TempochUtc" = "tempoch_utc_t" "TempochPeriodMjd" = "tempoch_period_mjd_t" +"QttyQuantity" = "qtty_quantity_t" [enum] rename_variants = "ScreamingSnakeCase" diff --git a/tempoch-ffi/include/tempoch_ffi.h b/tempoch-ffi/include/tempoch_ffi.h index c8a8674..db6e225 100644 --- a/tempoch-ffi/include/tempoch_ffi.h +++ b/tempoch-ffi/include/tempoch_ffi.h @@ -7,6 +7,9 @@ #include #include #include +/* QttyQuantity and UnitId definitions come from qtty_ffi.h */ +#include "qtty_ffi.h" + // Status codes returned by tempoch-ffi functions. // @@ -131,6 +134,32 @@ tempoch_status_t tempoch_period_mjd_intersection(struct tempoch_period_mjd_t a, // Compute Julian centuries since J2000 for a given Julian Date. double tempoch_jd_julian_centuries(double jd); +// Compute the difference between two Julian Dates as a `QttyQuantity` in days. + qtty_quantity_t tempoch_jd_difference_qty(double jd1, double jd2); + +// Add a `QttyQuantity` duration (must be time-compatible) to a Julian Date. +// The quantity is converted to days internally. +// +// # Safety +// `out` must be a valid, writable pointer to `f64`. + tempoch_status_t tempoch_jd_add_qty(double jd, qtty_quantity_t duration, double *out); + +// Compute the difference between two Modified Julian Dates as a `QttyQuantity` in days. + qtty_quantity_t tempoch_mjd_difference_qty(double mjd1, double mjd2); + +// Add a `QttyQuantity` duration (must be time-compatible) to a Modified Julian Date. +// The quantity is converted to days internally. +// +// # Safety +// `out` must be a valid, writable pointer to `f64`. + tempoch_status_t tempoch_mjd_add_qty(double mjd, qtty_quantity_t duration, double *out); + +// Compute Julian centuries since J2000 as a `QttyQuantity`. + qtty_quantity_t tempoch_jd_julian_centuries_qty(double jd); + +// Compute the duration of a period as a `QttyQuantity` in days. + qtty_quantity_t tempoch_period_mjd_duration_qty(struct tempoch_period_mjd_t period); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/tempoch-ffi/src/lib.rs b/tempoch-ffi/src/lib.rs index e9b1460..b61e3d2 100644 --- a/tempoch-ffi/src/lib.rs +++ b/tempoch-ffi/src/lib.rs @@ -5,6 +5,9 @@ //! //! This crate exposes a flat C-compatible API for creating and manipulating //! Julian Dates, Modified Julian Dates, time periods, and UTC conversions. +//! +//! Duration-related functions return [`QttyQuantity`] from qtty-ffi, providing +//! type-safe unit information alongside numeric values. mod error; mod period; @@ -14,6 +17,20 @@ pub use error::*; pub use period::*; pub use time::*; +// Re-export qtty-ffi types used in our public FFI surface +pub use qtty_ffi::{QttyQuantity, UnitId}; + +/// Catches any panic and returns an error value instead of unwinding across FFI. +macro_rules! catch_panic { + ($default:expr, $body:expr) => {{ + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| $body)) { + Ok(result) => result, + Err(_) => $default, + } + }}; +} +pub(crate) use catch_panic; + /// Returns the tempoch-ffi ABI version (semver-encoded: major*10000 + minor*100 + patch). #[allow(clippy::erasing_op, clippy::identity_op)] #[no_mangle] diff --git a/tempoch-ffi/src/period.rs b/tempoch-ffi/src/period.rs index eb3a51f..1142985 100644 --- a/tempoch-ffi/src/period.rs +++ b/tempoch-ffi/src/period.rs @@ -3,6 +3,7 @@ //! FFI bindings for tempoch period/interval types. +use crate::catch_panic; use crate::error::TempochStatus; use tempoch::{Interval, ModifiedJulianDate, Period, MJD}; @@ -50,17 +51,18 @@ pub unsafe extern "C" fn tempoch_period_mjd_new( end_mjd: f64, out: *mut TempochPeriodMjd, ) -> TempochStatus { - if out.is_null() { - return TempochStatus::NullPointer; - } - if start_mjd > end_mjd { - return TempochStatus::InvalidPeriod; - } - // SAFETY: `out` was checked for null and the caller guarantees it points to writable memory. - unsafe { - *out = TempochPeriodMjd { start_mjd, end_mjd }; - } - TempochStatus::Ok + catch_panic!(TempochStatus::InvalidPeriod, { + if out.is_null() { + return TempochStatus::NullPointer; + } + if start_mjd > end_mjd { + return TempochStatus::InvalidPeriod; + } + unsafe { + *out = TempochPeriodMjd { start_mjd, end_mjd }; + } + TempochStatus::Ok + }) } /// Compute the duration of a period in days. @@ -80,17 +82,18 @@ pub unsafe extern "C" fn tempoch_period_mjd_intersection( b: TempochPeriodMjd, out: *mut TempochPeriodMjd, ) -> TempochStatus { - if out.is_null() { - return TempochStatus::NullPointer; - } - let pa = a.to_period(); - let pb = b.to_period(); - match pa.intersection(&pb) { - Some(result) => { - // SAFETY: `out` was checked for null and the caller guarantees it points to writable memory. - unsafe { *out = TempochPeriodMjd::from_period(&result) }; - TempochStatus::Ok + catch_panic!(TempochStatus::NoIntersection, { + if out.is_null() { + return TempochStatus::NullPointer; } - None => TempochStatus::NoIntersection, - } + let pa = a.to_period(); + let pb = b.to_period(); + match pa.intersection(&pb) { + Some(result) => { + unsafe { *out = TempochPeriodMjd::from_period(&result) }; + TempochStatus::Ok + } + None => TempochStatus::NoIntersection, + } + }) } diff --git a/tempoch-ffi/src/time.rs b/tempoch-ffi/src/time.rs index e6e3929..4bdad8c 100644 --- a/tempoch-ffi/src/time.rs +++ b/tempoch-ffi/src/time.rs @@ -4,9 +4,11 @@ //! FFI bindings for tempoch time types: JulianDate, ModifiedJulianDate, //! and UTC conversions. +use crate::catch_panic; use crate::error::TempochStatus; use chrono::{DateTime, NaiveDate, Utc}; use qtty::Days; +use qtty_ffi::{QttyQuantity, UnitId}; use tempoch::{JulianDate, ModifiedJulianDate, TimeInstant, JD, MJD}; // ═══════════════════════════════════════════════════════════════════════════ @@ -80,18 +82,19 @@ pub extern "C" fn tempoch_jd_to_mjd(jd: f64) -> f64 { /// `out` must be a valid, writable pointer to `f64`. #[no_mangle] pub unsafe extern "C" fn tempoch_jd_from_utc(utc: TempochUtc, out: *mut f64) -> TempochStatus { - if out.is_null() { - return TempochStatus::NullPointer; - } - match utc.into_chrono() { - Some(dt) => { - let jd = JulianDate::from_utc(dt); - // SAFETY: `out` was checked for null and the caller guarantees writable memory. - unsafe { *out = jd.value() }; - TempochStatus::Ok + catch_panic!(TempochStatus::UtcConversionFailed, { + if out.is_null() { + return TempochStatus::NullPointer; } - None => TempochStatus::UtcConversionFailed, - } + match utc.into_chrono() { + Some(dt) => { + let jd = JulianDate::from_utc(dt); + unsafe { *out = jd.value() }; + TempochStatus::Ok + } + None => TempochStatus::UtcConversionFailed, + } + }) } /// Convert a Julian Date to UTC. Returns Ok on success, @@ -101,17 +104,18 @@ pub unsafe extern "C" fn tempoch_jd_from_utc(utc: TempochUtc, out: *mut f64) -> /// `out` must be a valid, writable pointer to `TempochUtc`. #[no_mangle] pub unsafe extern "C" fn tempoch_jd_to_utc(jd: f64, out: *mut TempochUtc) -> TempochStatus { - if out.is_null() { - return TempochStatus::NullPointer; - } - match JulianDate::new(jd).to_utc() { - Some(dt) => { - // SAFETY: `out` was checked for null and the caller guarantees writable memory. - unsafe { *out = TempochUtc::from_chrono(&dt) }; - TempochStatus::Ok + catch_panic!(TempochStatus::UtcConversionFailed, { + if out.is_null() { + return TempochStatus::NullPointer; } - None => TempochStatus::UtcConversionFailed, - } + match JulianDate::new(jd).to_utc() { + Some(dt) => { + unsafe { *out = TempochUtc::from_chrono(&dt) }; + TempochStatus::Ok + } + None => TempochStatus::UtcConversionFailed, + } + }) } // ═══════════════════════════════════════════════════════════════════════════ @@ -136,18 +140,19 @@ pub extern "C" fn tempoch_mjd_to_jd(mjd: f64) -> f64 { /// `out` must be a valid, writable pointer to `f64`. #[no_mangle] pub unsafe extern "C" fn tempoch_mjd_from_utc(utc: TempochUtc, out: *mut f64) -> TempochStatus { - if out.is_null() { - return TempochStatus::NullPointer; - } - match utc.into_chrono() { - Some(dt) => { - let mjd = ModifiedJulianDate::from_utc(dt); - // SAFETY: `out` was checked for null and the caller guarantees writable memory. - unsafe { *out = mjd.value() }; - TempochStatus::Ok + catch_panic!(TempochStatus::UtcConversionFailed, { + if out.is_null() { + return TempochStatus::NullPointer; } - None => TempochStatus::UtcConversionFailed, - } + match utc.into_chrono() { + Some(dt) => { + let mjd = ModifiedJulianDate::from_utc(dt); + unsafe { *out = mjd.value() }; + TempochStatus::Ok + } + None => TempochStatus::UtcConversionFailed, + } + }) } /// Convert a Modified Julian Date to UTC. @@ -156,19 +161,24 @@ pub unsafe extern "C" fn tempoch_mjd_from_utc(utc: TempochUtc, out: *mut f64) -> /// `out` must be a valid, writable pointer to `TempochUtc`. #[no_mangle] pub unsafe extern "C" fn tempoch_mjd_to_utc(mjd: f64, out: *mut TempochUtc) -> TempochStatus { - if out.is_null() { - return TempochStatus::NullPointer; - } - match ModifiedJulianDate::new(mjd).to_utc() { - Some(dt) => { - // SAFETY: `out` was checked for null and the caller guarantees writable memory. - unsafe { *out = TempochUtc::from_chrono(&dt) }; - TempochStatus::Ok + catch_panic!(TempochStatus::UtcConversionFailed, { + if out.is_null() { + return TempochStatus::NullPointer; } - None => TempochStatus::UtcConversionFailed, - } + match ModifiedJulianDate::new(mjd).to_utc() { + Some(dt) => { + unsafe { *out = TempochUtc::from_chrono(&dt) }; + TempochStatus::Ok + } + None => TempochStatus::UtcConversionFailed, + } + }) } +// ═══════════════════════════════════════════════════════════════════════════ +// Duration / Difference (raw f64 — backward compatible) +// ═══════════════════════════════════════════════════════════════════════════ + /// Compute the difference between two Julian Dates in days. #[no_mangle] pub extern "C" fn tempoch_jd_difference(jd1: f64, jd2: f64) -> f64 { @@ -205,6 +215,98 @@ pub extern "C" fn tempoch_jd_julian_centuries(jd: f64) -> f64 { JulianDate::new(jd).julian_centuries().value() } +// ═══════════════════════════════════════════════════════════════════════════ +// Duration / Difference (QttyQuantity — typed) +// ═══════════════════════════════════════════════════════════════════════════ +// These functions return `QttyQuantity` values with proper unit metadata, +// enabling type-safe conversions via the qtty-ffi API. + +/// Compute the difference between two Julian Dates as a `QttyQuantity` in days. +#[no_mangle] +pub extern "C" fn tempoch_jd_difference_qty(jd1: f64, jd2: f64) -> QttyQuantity { + let t1 = JulianDate::new(jd1); + let t2 = JulianDate::new(jd2); + QttyQuantity::new(t1.difference(&t2).value(), UnitId::Day) +} + +/// Add a `QttyQuantity` duration (must be time-compatible) to a Julian Date. +/// The quantity is converted to days internally. +/// +/// # Safety +/// `out` must be a valid, writable pointer to `f64`. +#[no_mangle] +pub unsafe extern "C" fn tempoch_jd_add_qty( + jd: f64, + duration: QttyQuantity, + out: *mut f64, +) -> TempochStatus { + catch_panic!(TempochStatus::UtcConversionFailed, { + if out.is_null() { + return TempochStatus::NullPointer; + } + // Convert quantity to days via qtty-ffi registry + let days_val = match duration.convert_to(UnitId::Day) { + Some(q) => q.value, + None => return TempochStatus::UtcConversionFailed, + }; + let result = JulianDate::new(jd) + .add_duration(Days::new(days_val)) + .value(); + unsafe { *out = result }; + TempochStatus::Ok + }) +} + +/// Compute the difference between two Modified Julian Dates as a `QttyQuantity` in days. +#[no_mangle] +pub extern "C" fn tempoch_mjd_difference_qty(mjd1: f64, mjd2: f64) -> QttyQuantity { + let t1 = ModifiedJulianDate::new(mjd1); + let t2 = ModifiedJulianDate::new(mjd2); + QttyQuantity::new(t1.difference(&t2).value(), UnitId::Day) +} + +/// Add a `QttyQuantity` duration (must be time-compatible) to a Modified Julian Date. +/// The quantity is converted to days internally. +/// +/// # Safety +/// `out` must be a valid, writable pointer to `f64`. +#[no_mangle] +pub unsafe extern "C" fn tempoch_mjd_add_qty( + mjd: f64, + duration: QttyQuantity, + out: *mut f64, +) -> TempochStatus { + catch_panic!(TempochStatus::UtcConversionFailed, { + if out.is_null() { + return TempochStatus::NullPointer; + } + let days_val = match duration.convert_to(UnitId::Day) { + Some(q) => q.value, + None => return TempochStatus::UtcConversionFailed, + }; + let result = ModifiedJulianDate::new(mjd) + .add_duration(Days::new(days_val)) + .value(); + unsafe { *out = result }; + TempochStatus::Ok + }) +} + +/// Compute Julian centuries since J2000 as a `QttyQuantity`. +#[no_mangle] +pub extern "C" fn tempoch_jd_julian_centuries_qty(jd: f64) -> QttyQuantity { + QttyQuantity::new( + JulianDate::new(jd).julian_centuries().value(), + UnitId::JulianCentury, + ) +} + +/// Compute the duration of a period as a `QttyQuantity` in days. +#[no_mangle] +pub extern "C" fn tempoch_period_mjd_duration_qty(period: crate::TempochPeriodMjd) -> QttyQuantity { + QttyQuantity::new(period.end_mjd - period.start_mjd, UnitId::Day) +} + #[cfg(test)] mod tests { use super::*; diff --git a/tempoch/Cargo.toml b/tempoch/Cargo.toml index 347bbf3..026bf43 100644 --- a/tempoch/Cargo.toml +++ b/tempoch/Cargo.toml @@ -22,7 +22,7 @@ serde = ["tempoch-core/serde"] tempoch-core = { path = "../tempoch-core", version = "0.3.0" } [dev-dependencies] -chrono = "0.4.43" -qtty = "0.3.0" +chrono = "0.4.44" +qtty = "0.3.1" serde_json = "1.0"