diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 26b619621..da510c385 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -93,8 +93,10 @@ jobs: distro: ${{ matrix.distro }} install: | apt-get update -q -y - apt-get install -q -y make gcc gfortran - run: make test + apt-get install -q -y make gcc g++ gfortran + run: | + export ENABLE_CPP=1 + make test test-macos: name: Test on MacOS diff --git a/CHANGELOG.md b/CHANGELOG.md index f187961d0..a07318d2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] -Upcoming maintenance release, possibly around 1 August 2026. +Upcoming feature release, possibly around 1 August 2026. ### Added @@ -17,8 +17,19 @@ Upcoming maintenance release, possibly around 1 August 2026. `offset_by()` function. Added `novas_offset_by()`, `novas_equ_offset_by()` functions; and `Equatorial::offset()`, `Ecliptic::offset()`, `Galactic::offset()`, and `Horizontal::offset()` methods. (thanks to aleberti) + - #318: New functions to check for effective equality of data structures, within typical tolerances of the library. + The functions are in `cmp.c`, and have names `novas_equals_[...]()`. + + - #318: `CatalogEntry`, `OrbitalSystem`, `Orbital`, `Source`, `Observer`, and `Frame` now have `equals()` methods + as well as overridden `==` and `!=` operators, thanks to the new C99 comparison functions. + ### Changed + - #317: Avoid `memcmp()` in testing structs for equality, using new comparison functions instead. + + - #318: `Vector::equals()` to use the new `novas_equals_vector()` for consitent implementation between the C99 and + C++ APIs. + - Improved CMake installation of examples (no unintended files, C++ examples only if `ENABLE_CPP` option is used). - Adjust C++ testing precision on tests that can trip up, depending on the platform. @@ -32,6 +43,7 @@ Upcoming maintenance release, possibly around 1 August 2026. - `examples/Makefile` to work standalone, without `config.mk`. - Fix wrong argument types in error traces of `Source` and `Ecliptic` (found by CodeQL). + ## [1.6.0] - 2026-04-27 diff --git a/config.mk b/config.mk index 3ebee3e3c..a16ce51b5 100644 --- a/config.mk +++ b/config.mk @@ -170,7 +170,7 @@ endif SOURCES = target.c observer.c earth.c equator.c system.c transform.c cio.c \ orbital.c spectral.c grav.c nutation.c timescale.c frames.c place.c \ calendar.c refract.c naif.c parse.c util.c planets.c itrf.c \ - ephemeris.c solsys3.c solsys-ephem.c moon.c + ephemeris.c solsys3.c solsys-ephem.c moon.c cmp.c # Generate a list of object (obj/*.o) files from the input sources OBJECTS := $(addprefix $(OBJ)/,$(subst .c,.o,$(SOURCES))) diff --git a/include/novas.h b/include/novas.h index adafaed38..8c491c4f0 100644 --- a/include/novas.h +++ b/include/novas.h @@ -3384,7 +3384,7 @@ int novas_icrs_to_sys(const double *in, double jd_tdb, enum novas_accuracy accur int novas_sys_to_icrs(enum novas_reference_system sys, const double *in, double jd_tdb, enum novas_accuracy accuracy, double *out); -// ---------------------- Added in 1.6.0 ------------------------- +// ---------------------- Added in 1.7.0 ------------------------- // in util.c /// @c_util @@ -3393,6 +3393,44 @@ int novas_offset_by(double lon, double lat, double direction, double distance, d /// @c_util int novas_equ_offset_by(double ra, double dec, double direction, double distance, double *restrict out_ra, double *restrict out_dec); +// in cmp.c +/// @c_util +int novas_equals_vector(const double *a, const double *b, double tol); + +/// @c_time +int novas_equals_timespec(const novas_timespec *a, const novas_timespec *b); + +/// @c_observer +int novas_equals_on_surface(const on_surface *a, const on_surface *b); + +/// @c_observer +int novas_equals_near_earth(const in_space *a, const in_space *b); + +/// @c_observer +int novas_equals_ssb_posvel(const in_space *a, const in_space *b); + +/// @c_observer +int novas_equals_observer(const observer *a, const observer *b); + +/// @c_source +int novas_equals_cat_entry(const cat_entry *a, const cat_entry *b); + +/// @c_source +int novas_equals_orbsys(const novas_orbital_system *a, const novas_orbital_system *b); + +/// @c_source +int novas_equals_orbital(const novas_orbital *a, const novas_orbital *b); + +/// @c_source +int novas_equals_object(const object *a, const object *b); + +/// @c_apparent +int novas_equals_sky_pos(const sky_pos *a, const sky_pos *b); + +int novas_equals_planet_bundle(const novas_planet_bundle *a, const novas_planet_bundle *b); + +/// @c_frame +int novas_equals_frame(const novas_frame *a, const novas_frame *b); // <================= END of SuperNOVAS API =====================> diff --git a/include/supernovas.h b/include/supernovas.h index 86f72cb10..8fd9154be 100644 --- a/include/supernovas.h +++ b/include/supernovas.h @@ -1483,6 +1483,12 @@ class Observer : public Validating { virtual bool is_geocentric() const; + virtual bool equals(const Observer& other) const; + + bool operator==(const Observer& other) const; + + bool operator!=(const Observer& other) const; + /// @ingroup frame Frame frame_at(const Time& time, enum novas_accuracy accuracy = NOVAS_FULL_ACCURACY) const; @@ -1547,6 +1553,8 @@ class GeodeticObserver : public Observer { bool is_geodetic() const override; + bool equals(const Observer& other) const override; + Site site() const; Velocity itrs_velocity() const; @@ -1932,6 +1940,12 @@ class Frame : public Validating { Frame& operator=(const Frame& frame); + bool equals(const Frame& other) const; + + bool operator==(const Frame& other) const; + + bool operator!=(const Frame& other) const; + const novas_frame* _novas_frame() const; enum novas_accuracy accuracy() const; @@ -1997,6 +2011,12 @@ class Source : public Validating { enum novas_object_type type() const; + bool equals(const Source& other) const; + + bool operator==(const Source &other) const; + + bool operator!=(const Source &other) const; + /// @ingroup apparent Apparent apparent_in(const Frame &frame) const; @@ -2054,7 +2074,7 @@ class Source : public Validating { class CatalogEntry : public Validating { private: cat_entry _entry = {}; ///< stored catalog entry - Equinox _sys; ///< stored catalog system + Equinox _sys; ///< stored catalog system void validate(const char *loc); @@ -2069,6 +2089,12 @@ class CatalogEntry : public Validating { const cat_entry* _cat_entry() const; + bool equals(const CatalogEntry& other) const; + + bool operator==(const CatalogEntry& other) const; + + bool operator!=(const CatalogEntry& other) const; + const Equinox& system() const; std::string name() const; @@ -2291,6 +2317,12 @@ class OrbitalSystem : public Validating { public: const novas_orbital_system * _novas_orbital_system() const; + bool equals(const OrbitalSystem& other) const; + + bool operator==(const OrbitalSystem& other) const; + + bool operator!=(const OrbitalSystem& other) const; + Planet center() const; Angle obliquity() const; @@ -2361,6 +2393,12 @@ class Orbital : public Validating { static Orbital from_mean_motion(const OrbitalSystem& system, const Time& ref_time, const Coordinate& semi_major, const Angle& mean_anomaly, double rad_per_s); + bool equals(const Orbital& other) const; + + bool operator==(const Orbital& other) const; + + bool operator!=(const Orbital& other) const; + const novas_orbital * _novas_orbital() const; OrbitalSystem system() const; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 626df6d4c..207c9c5b4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -33,6 +33,7 @@ set(SUPERNOVAS_CORE_SOURCES solsys3.c solsys-ephem.c moon.c + cmp.c ) diff --git a/src/c99/cmp.c b/src/c99/cmp.c new file mode 100644 index 000000000..1f40ce0ce --- /dev/null +++ b/src/c99/cmp.c @@ -0,0 +1,619 @@ +/** + * @file + * + * @date Created on May 8, 2026 + * @author Attila Kovacs + * @since 1.7 + * + * Set of functions to check for effective equality between SuperNOVAS data sturctures within + * typical tolerances. + */ + +#include +#include + +/// \cond PRIVATE +#define __NOVAS_INTERNAL_API__ ///< Use definitions meant for internal use by SuperNOVAS only +/// \endcond + +#include "novas.h" + +/// \cond PRIVATE +#define CMP_DEG (1e-6 / 3600.0) ///< [deg] Compare degrees to 1 μas precison. +#define CMP_RAD ( CMP_DEG * DEGREE ) ///< [rad] Compare radians to 1 μas precison. +/// \endcond + + +static int novas_equals_double(double a, double b, double tol) { + return fabs(a - b) <= fabs(tol); +} + +static int novas_equals_deg(double a, double b) { + return fabs(remainder(a - b, DEG360)) <= fabs(CMP_DEG); +} + + +/** + * Checks if two 3D vectors are effectively the same, within the specified absolute tolerance. Two + * vectors are equal if the distance between them is less than or equal to the magnitude of the + * specified tolerance. + * + * NOTES: + * + * - Two NULL (undefined) vectors are considered not equal. + * + * - A vector may not equal itself if it contains NAN or infinite components, since + * `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one of the vectors + * @param b the other vector + * @param tol absolute tolerance (sign is ignored). + * @return TRUE (1) if all the two vectors match within the specified tolerance, or else FALSE + * (0) + * + * @since 1.7 + * @author Attila Kovacs + */ +int novas_equals_vector(const double *a, const double *b, double tol) { + double d2 = 0.0; + int i; + + if(!a || !b) + return 0; + + for(i = 0; i < 3; i++) { + double d = a[i] - b[i]; + d2 += d * d; + } + + return d2 <= tol * tol; +} + +/** + * Checks if two time specifications are the same within 10-7 days (~100 μs). + * + * NOTES: + * + * - Two NULL (undefined) time specifications are considered not equal. + * + * - A time specification may not equal itself if it contains NAN or infinite components, + * since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one of the time specs + * @param b the other time spec + * @return TRUE (1) if the two time specifications match to within 100 μs, or else FALSE + * (0). + * + * @since 1.7 + * @author Attila Kovacs + */ +int novas_equals_timespec(const novas_timespec *a, const novas_timespec *b) { + if(!a || !b) + return 0; + + if(!novas_time_equals(remainder(a->fjd_tt, 1.0), remainder(b->fjd_tt, 1.0))) // [0.1 * us] + return 0; + if(!novas_time_equals(a->ijd_tt + a->fjd_tt, b->ijd_tt + b->fjd_tt)) + return 0; + if(!novas_equals_double(a->ut1_to_tt, b->ut1_to_tt, 1e-7)) // [0.1 * us] + return 0; + if(!novas_equals_double(a->dut1, b->dut1, 1e-7)) // [0.1 * us] + return 0; + if(!novas_equals_double(a->tt2tdb, b->tt2tdb, 1e-7)) // [0.1 * us] + return 0; + + return 1; +} + +/** + * Checks if two geodetic locations, and the weather parameters defined for them, match. For two + * `on_surface` structures to be equal, they must have matching: + * + * - longitude and latitude angles to within 1 μas. + * - altitudes within 1 mm. + * - temperatures within 1 mK. + * - pressures within 1 μbar + * - humidities within 0.001 % + * + * NOTES: + * + * - Two NULL (undefined) structures are considered not equal. + * + * - A geodetic location + local weather structure may not equal itself if it contains + * NAN or infinite components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one geodetic location (with weather) + * @param b another geodetic location (with weather) + * @return TRUE (1) if the two locations and their weather parameters match within the + * described tolerances, or else FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + * + * @sa novas_equals_observer() + */ +int novas_equals_on_surface(const on_surface *a, const on_surface *b) { + if(!a || !b) + return 0; + + if(!novas_equals_deg(a->longitude, b->longitude)) + return 0; + if(!novas_equals_double(a->latitude, b->latitude, CMP_DEG)) + return 0; + if(!novas_equals_double(a->height, b->height, 1e-3)) + return 0; + if(!novas_equals_double(a->temperature, b->temperature, 1e-3)) + return 0; + if(!novas_equals_double(a->pressure, b->pressure, 1e-3)) + return 0; + if(!novas_equals_double(a->humidity, b->humidity, 1e-3)) + return 0; + + return 1; +} + +/** + * Checks if two near-Earth locations and motions match within 1 mm and 1 mm/s, respectively. Such + * near-Earth data structures are used by airborne observers or observers in Earth orbit. + * + * NOTES: + * + * - Two NULL (undefined) structures are considered not equal. + * + * - A near-Earth position/velocity structure may not equal itself if it contains NAN or + * infinite components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one near-Earth position/velocity data structure + * @param b another near-Earth position/velocity data structure + * @return TRUE (1) if the two near-Earth positions and velocities match within 1 mm and + * 1 mm/s, respectively. + * + * @since 1.7 + * @author Attila Kovacs + * + * @sa novas_equals_ssb_posvel(), novas_equals_observer() + */ +int novas_equals_near_earth(const in_space *a, const in_space *b) { + if(!a || !b) + return 0; + + if(!novas_equals_vector(a->sc_pos, b->sc_pos, 1e-6)) // [mm] + return 0; + if(!novas_equals_vector(a->sc_vel, b->sc_vel, 1e-6)) // [mm/s] + return 0; + + return 1; +} + +/** + * Checks if two Solar-system locations and motions match within 1 m and ~1 mm/s, respectively. + * Such near-Earth data structures are used by observers defined relative to the Solar-System + * Barycenter (SSB). + * + * NOTES: + * + * - Two NULL (undefined) structures are considered not equal. + * + * - A Solar-system position / velocity structure may not equal itself if it contains NAN + * or infinite components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one Solar-system position/velocity data structure + * @param b another Solar-system position/velocity data structure + * @return TRUE (1) if the two SSB positions and velocities match within 1 m and ~1 mm/s, + * respectively. + * + * @since 1.7 + * @author Attila Kovacs + * + * @sa novas_equals_near_earth(), novas_equals_observer() + */ +int novas_equals_ssb_posvel(const in_space *a, const in_space *b) { + if(!a || !b) + return 0; + + if(!novas_equals_vector(a->sc_pos, b->sc_pos, 1.0 / NOVAS_AU)) // [m] + return 0; + if(!novas_equals_vector(a->sc_vel, b->sc_vel, 10.0 / NOVAS_AU)) // [~mm/s] 1e-4 km/s * 1e5 s / AU[m] + return 0; + + return 1; +} + +/** + * Checks if two observers are essentially the same within the tolerances associated to their + * defining components. + * + * NOTES: + * + * - Two NULL (undefined) observers are considered not equal. + * + * - An observer structure may not equal itself if it contains NAN or infinite + * components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a an observer data structure + * @param b another observer data structure + * @return TRUE (1) if the two data structures describe effectively the same observer location, + * within the typical tolerances associated to them, or else FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + */ +int novas_equals_observer(const observer *a, const observer *b) { + if(!a || !b) + return 0; + + if(a->where != b->where) + return 0; + + switch(a->where) { + case NOVAS_OBSERVER_AT_GEOCENTER: + return 1; + case NOVAS_OBSERVER_ON_EARTH: + return novas_equals_on_surface(&a->on_surf, &b->on_surf); + case NOVAS_AIRBORNE_OBSERVER: + return novas_equals_on_surface(&a->on_surf, &b->on_surf) && novas_equals_near_earth(&a->near_earth, &b->near_earth); + case NOVAS_OBSERVER_IN_EARTH_ORBIT: + return novas_equals_near_earth(&a->near_earth, &b->near_earth); + case NOVAS_SOLAR_SYSTEM_OBSERVER: + return novas_equals_ssb_posvel(&a->near_earth, &b->near_earth); + default: + return 0; + } + + return 1; +} + +/** + * Checks if two catalog entries define the same parameters, within the typical tolerances + * associated to these. For two catalog entries to be equals, they must have matching + * + * - names (case sensitive) + * - catalog IDs (case sensitive) + * - numerical IDs + * - coordinates within 1 μas. + * - proper motions to within 1 μas / century. + * - parallaxes to within 1 ppm of their geometric mean. + * - radial velocities to withing 1 mm/s. + * + * NOTES: + * + * - Two NULL (undefined) catalog entries are considered not equal. + * + * - A catalog entry structure may not equal itself if it contains NAN or infinite + * components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one of the catalog entries + * @param b another catalog entry + * @return TRUE (1) if the two catalog entries define the same sidereal source effectively, + * or else FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + * + * @sa novas_equals_object() + */ +int novas_equals_cat_entry(const cat_entry *a, const cat_entry *b) { + if(!a || !b) + return 0; + + if(strncmp(a->starname, b->starname, SIZE_OF_OBJ_NAME)) + return 0; + if(strncmp(a->catalog, b->catalog, SIZE_OF_CAT_NAME)) + return 0; + if(a->starnumber != b->starnumber) + return 0; + if(!novas_equals_deg(15.0 * a->ra, 15.0 * b->ra)) + return 0; + if(!novas_equals_double(a->dec, b->dec, CMP_DEG)) + return 0; + if(!novas_equals_double(a->promora, b->promora, 1e-6)) // [uas / cy] + return 0; + if(!novas_equals_double(a->promodec, b->promodec, 1e-6)) // [uas / cy] + return 0; + if(!novas_equals_double(a->parallax, b->parallax, 1e-6 * sqrt(fabs(a->parallax * b->parallax)))) // 1 ppm + return 0; + if(!novas_equals_double(a->radialvelocity, b->radialvelocity, 1e-6)) // [mm/s] + return 0; + + return 1; +} + +/** + * Checks if two orbital systems match within typical tolerances. Two orbital system are + * considered equal, if they are defined with respect to the same reference plane, around the same + * major planet (or Solar-system position), and have the obliquity and ascending node (if + * obliquity is non-zero) to within 1 μas. + * + * NOTES: + * + * - Two NULL (undefined) orbital systems are considered not equal. + * + * - An orbital system structure may not equal itself if it contains NAN or infinite + * components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one of the orbital systems + * @param b another orbital system + * @return TRUE (1) if the two systems are effectively the same, within tolerances, + * or else FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + * + * @sa novas_equals_orbital(), novas_equals_object() + */ +int novas_equals_orbsys(const novas_orbital_system *a, const novas_orbital_system *b) { + if(!a || !b) + return 0; + + if(a->center != b->center) + return 0; + if(a->plane != b->plane) + return 0; + if(a->type != b->type) + return 0; + if(!novas_equals_deg(a->obl, b->obl)) + return 0; + if(sqrt(fabs(a->obl * b->obl)) > CMP_DEG && !novas_equals_deg(a->Omega, b->Omega)) + return 0; + + return 1; +} + +/** + * Checks if two Keplerian orbitals match, within typical tolerances. For two orbitals to be + * considered equal, they must have matching orbital systems, have the same reference dates + * to within ~10 ms (see `novas_time_equals()`), and: + * + * - angular parameters must match to within 1 μas. + * - semi-major axis must match to within 1 m. + * - eccentricity must match to within 10-12 time the geometric mean of the two + * semi-major axes. + * - mean motion must match to within 1 μas / cy. + * - apsis and node periods must match to within ~10 ms (see `novas_time_equals()`) + * + * NOTES: + * + * - Two NULL (undefined) orbitals are considered not equal. + * + * - Sn orbital elements structure may not equal itself if it contains NAN or infinite + * components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a one set of Keplerian orbital parameters + * @param b another set of Keplerian orbital parameters + * @return TRUE (1) if the two Keplerian orbitals are effectively the same, within + * tolerances, or else FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + * + * @sa novas_equals_object() + */ +int novas_equals_orbital(const novas_orbital *a, const novas_orbital *b) { + if(!a || !b) + return 0; + + if(!novas_equals_orbsys(&a->system, &b->system)) + return 0; + if(!novas_time_equals(a->jd_tdb, b->jd_tdb)) + return 0; + if(!novas_equals_double(a->a, b->a, 1.0 / NOVAS_AU)) // [m] + return 0; + if(!novas_equals_deg(a->M0, b->M0)) + return 0; + if(!novas_equals_double(a->n, b->n, 1e-3 * CMP_DEG)) // [< uas/yr] + return 0; + if(!novas_equals_double(a->e, b->e, 1e-12 * sqrt(a->a * b->a))) + return 0; + if(sqrt(fabs(a->e * b->e)) > 1e-12 && !novas_equals_deg(a->omega, b->omega)) + return 0; + if(!novas_equals_deg(a->i, b->i)) + return 0; + if(sqrt(fabs(a->i * b->i)) > CMP_DEG && !novas_equals_deg(a->Omega, b->Omega)) + return 0; + if(!novas_time_equals(a->apsis_period, b->apsis_period)) + return 0; + if(!novas_time_equals(a->node_period, b->node_period)) + return 0; + + return 1; +} + +/** + * Checks if two astronomical targets are the same within typical tolerances. Two targets are + * considered equals only if their types, names (case sensitive), numerical IDs, and defining + * parameters match within typical tolerances. + * + * NOTES: + * + * - Two NULL (undefined) objects are considered not equal. + * + * - An astronomical target structure may not equal itself if it contains NAN or infinite + * components, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a an astronomical target + * @param b another astronomical target + * @return TRUE (1) if the two targets are effectively the same, within tolerances, or else + * FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + */ +int novas_equals_object(const object *a, const object *b) { + if(!a || !b) + return 0; + + if(a->type != b->type) + return 0; + + if(strncmp(a->name, b->name, SIZE_OF_OBJ_NAME)) + return 0; + + if(a->number != b->number) + return 0; + + switch(a->type) { + case NOVAS_PLANET: + case NOVAS_EPHEM_OBJECT: + return 1; + case NOVAS_CATALOG_OBJECT: + return novas_equals_cat_entry(&a->star, &b->star); + case NOVAS_ORBITAL_OBJECT: + return novas_equals_orbital(&a->orbit, &b->orbit); + default: + return 0; + } + + return 1; +} + +/** + * Checks if two apparent positions on the observer's sky are the same within typical tolerances. + * Two sky positions are considered to be equal if their: + * + * - coordinates match within 1 μas, + * - distances match within 1 ppm. + * - radial velocities match within 1 mm/s. + * - unit vectors match to 10-12 (~1 μas). + * + * NOTES: + * + * - Two NULL (undefined) sky positions are considered not equal. + * + * - A sky position may not equal itself if it contains NAN or infinite components, since + * `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a a position on the observer's sky + * @param b another sky position + * @return TRUE (1) if the two positions are essentially the same within typical tolerances, + * or else FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + */ +int novas_equals_sky_pos(const sky_pos *a, const sky_pos *b) { + if(!a || !b) + return 0; + + if(!novas_equals_deg(15.0 * a->ra, 15.0 * b->ra)) + return 0; + if(!novas_equals_double(a->dec, b->dec, CMP_DEG)) + return 0; + if(!novas_equals_double(a->dis, b->dis, 1e-6 * sqrt(fabs(a->dis * b->dis)))) // [1 ppm] + return 0; + if(!novas_equals_double(a->rv, b->rv, 1e-6)) // [1 mm/s] + return 0; + if(!novas_equals_vector(a->r_hat, b->r_hat, 1e-12)) // [~1 μas] + return 0; + + return 1; +} + +/** + * Checks if two bundles containing Solar-system baricentric planet positions and velocities are + * effectively the same. The two bundles are considered equals if: + * + * - they contain data for the same set of planets. + * - the planets with data have positions matching within 1 m. + * - the planets with data have velocities matching within 1 mm/s. + * + * NOTES: + * + * - Two NULL (undefined) planet bundles are considered not equal. + * + * - A planet bundle may not equal itself if it contains NAN or infinite components for the + * planets they are supposed to have data for, since `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a a planet position / velocity bundle + * @param b another planet position / velocity bundle + * @return TRUE (1) if the two bundles are essentially the same within tolerances, or else + * FALSE (0). + * + * @since 1.7 + * @author Attila Kovacs + */ +int novas_equals_planet_bundle(const novas_planet_bundle *a, const novas_planet_bundle *b) { + int i; + + if(!a || !b) + return 0; + + for(i = 0; i < NOVAS_PLANETS; i++) { + if((a->mask & (1 << i)) != (b->mask & (1 << i))) + return 0; + } + + for(i = 0; i < NOVAS_PLANETS; i++) + if(a->mask & (1 << i)) { + if(!novas_equals_vector(&a->pos[i][0], &b->pos[i][0], 1.0 / NOVAS_AU)) // [m] + return 0; + if(!novas_equals_vector(&a->vel[i][0], &b->vel[i][0], 10.0 / NOVAS_AU)) // [~mm/s] 1e-4 km/s * 1e5 s / AU[m] + return 0; + } + + return 1; +} + +static int is_geodetic(const observer *obs) { + return obs->where == NOVAS_OBSERVER_ON_EARTH || obs->where == NOVAS_AIRBORNE_OBSERVER; +} + +/** + * Checks if two observing frames are essentially the same, within typical tolerances. Two frames + * are considered equal, if they are both: + * + * - initialized, + * - represent the same accuracy, + * - for the same observer, + * - at the same time, + * - same polar offsets (for geodetic observers or if defined), + * - and contain the same planetary ephemeris data + * + * all within the typical tolerances associated to these. For non-geodetic observers two frames + * are considered equal also if both have all NAN (undefined) polar offsets -- since polar offsets + * are not generally required for non-geodetic observing frames. + * + * NOTES: + * + * - this function does not check the calculated fields of the observing frames, which are + * assumed to have been left untouched after the called to `novas_make_frame()`. If the user + * modifies the calculated fields, they may need additional checks to ensure 'equality' + * accordingly. + * + * - Two NULL (undefined) observing frames are considered not equal. + * + * - An observing frame may not equal itself if it contains NAN or infinite components, since + * `NAN != NAN` and `INFINITE != INFINITE`. + * + * @param a an observing frame + * @param b another observing frame + * @return TRUE (1) if the two observing frames are effectively the same, given the + * tolerances, or else false. + * + * @since 1.7 + * @author Attila Kovacs + */ +int novas_equals_frame(const novas_frame *a, const novas_frame *b) { + if(!a || !b) + return 0; + + if(!novas_frame_is_initialized(a) || !novas_frame_is_initialized(b)) + return 0; + if(a->accuracy != b->accuracy) + return 0; + if(!novas_equals_timespec(&a->time, &b->time)) + return 0; + if(!novas_equals_observer(&a->observer, &b->observer)) + return 0; + if(!novas_equals_planet_bundle(&a->planets, &b->planets)) + return 0; + + // Allow all NAN dx/dy for non-geodetic observers + if(is_geodetic(&a->observer) || !isnan(a->dx) || !isnan(b->dx) || !isnan(a->dy) || !isnan(b->dy)) { + if(!novas_equals_double(a->dx, b->dx, 1e-3)) // [uas] + return 0; + if(!novas_equals_double(a->dy, b->dy, 1e-3)) // [uas] + return 0; + } + + return 1; +} diff --git a/src/cpp/CatalogEntry.cpp b/src/cpp/CatalogEntry.cpp index 7e1da931e..b3db98a47 100644 --- a/src/cpp/CatalogEntry.cpp +++ b/src/cpp/CatalogEntry.cpp @@ -154,6 +154,65 @@ CatalogEntry::CatalogEntry(cat_entry e, const Equinox& system) validate("CatalogEntry()"); } +/** + * Checks if this catalog entry matches another within the standard tolerances. For two catalog + * entries to be equals, they must have matching: + * + * - names (case sensitive) + * - catalog IDs (case sensitive) + * - numerical IDs + * - coordinates within 1 μas. + * - proper motions to within 1 μas / century. + * - parallaxes to within 10-4 % of their geometric mean. + * - radial velocities to withing 1 mm/s. + * + * Note, that a catalog entry may not equal itself if it contains NAN or infinite components. + * + * @param other the other catalog entry. + * @return `true` if both catalog entries essentially describe the same source, otherwise + * `false`. + * + * @since 1.7 + * + * @sa operator==(), operator!=() + */ +bool CatalogEntry::equals(const CatalogEntry& other) const { + return (_sys == other._sys) && novas_equals_cat_entry(&_entry, &other._entry); +} + +/** + * Checks if this catalog entry matches another within the standard tolerances. See `equals()` + * for details. + * + * @param other the other catalog entry. + * @return `true` if both catalog entries essentially describe the same source, otherwise + * `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool CatalogEntry::operator==(const CatalogEntry& other) const { + return equals(other); +} + +/** + * Checks if this catalog entry differs from another within. Same as `!equals()`. See `equals()` + * for details of the comparisons performed. + * + * @param other the other catalog entry. + * @return `true` if this catalog entry and the argument describe two distinct sources, + * otherwise `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool CatalogEntry::operator!=(const CatalogEntry& other) const { + return !equals(other); +} + + /** * Returns the equatorial coordinate system in which this catalog entry is defined. * diff --git a/src/cpp/Frame.cpp b/src/cpp/Frame.cpp index b1c089ef0..8a3230037 100644 --- a/src/cpp/Frame.cpp +++ b/src/cpp/Frame.cpp @@ -107,10 +107,68 @@ Frame& Frame::operator=(const Frame& frame) { } /** - * Returns the pointer to the underlying NOVAS C `novas_frame` data structure of this observing - * frame. + * Checks if this observing frame is essentially the same as another, given typical tolerances. See + * `Observer::equals()` and `Time::equals()` for details. Beyond the matching the observers and + * times, mathing frames must also match: * - * @return pointer to the underlying NOVAS C data structure. + * - accuracy setting + * - polar offsets (for geodetic observers, or else if defined) to 1 μas. + * - stored planet position and velocity data to 1 m and 1 mm/s, respectively. + * + * Note, that an observing frame may not equal itself if it contains NAN or infinite components. + * The exception is polar offsets for non-geodetic observing frames, where NAN values are + * considered 'normal', and hence don't spoil the effective equality so long as both frames + * leave these undefined. + * + * @param other the other observing frame + * @return `true` if this frame and the argument describe essentially the same observing + * frame, within the typical tolerances. + * + * @since 1.7 + * + * @sa operator==(), operator!=() + */ +bool Frame::equals(const Frame& other) const { + return (_time == other._time) + && (_observer == other._observer) + && novas_equals_frame(&_frame, &other._frame); +} + +/** + * Checks if this observing frame is essentially the same as another, given typical tolerances. + * Same as `equals()`. + * + * @param other the other observing frame + * @return `true` if this frame and the argument describe essentially the same observing + * frame, within the typical tolerances. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool Frame::operator==(const Frame& other) const { + return equals(other); +} + +/** + * Checks if this observing frame differes from another, given typical tolerances. Same as `!equals()`. + * + * @param other the other observing frame + * @return `true` if this frame and the argument describe essentially the same observing + * frame, within the typical tolerances. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool Frame::operator!=(const Frame& other) const { + return !equals(other); +} + +/** + * Returns the pointer to the underlying C data structure of this observing frame. + * + * @return pointer to the underlying C data structure. * * @since 1.6 */ diff --git a/src/cpp/Observer.cpp b/src/cpp/Observer.cpp index f7c3d3427..4f4145a2e 100644 --- a/src/cpp/Observer.cpp +++ b/src/cpp/Observer.cpp @@ -91,6 +91,55 @@ enum novas_observer_place Observer::type() const { return _observer.where; } +/** + * Checks if this observer is essentially the same as the other, within the tolerances associated + * to their defining components. Note, that an observer may not equal itself if it contains NAN or + * infinite components. + * + * @param other the other observer + * @return `true` if both observers effectively describe the same observing location, + * within tolerances, or else `false`. + * + * @since 1.7 + * + * @sa operator==(), operator!=() + */ +bool Observer::equals(const Observer& other) const { + return novas_equals_observer(&_observer, &other._observer); +} + +/** + * Checks if two observers are essentially the same within the tolerances associated to their + * defining components. Same as `equals()`. + * + * @param other the other observer + * @return `true` if both observers effectively describe the same observing location, + * within tolerances, or else `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool Observer::operator==(const Observer& other) const { + return equals(other); +} + +/** + * Checks if two observers differ given standard tolerances associated to their defining + * components. Same as `!equals()`. + * + * @param other the other observer + * @return `true` if this and the argument describe distinct observing locations, + * or else `false`. + * + * @since 1.7 + * + * @sa equals(), operator==() + */ +bool Observer::operator!=(const Observer& other) const { + return !equals(other); +} + /** * Returns an observing frame for this observer at the specified time and optionally with a * specified accuracy. Full accuracy frames (default) require that a high-precision planet @@ -711,6 +760,20 @@ bool GeodeticObserver::is_geodetic() const { return true; } +bool GeodeticObserver::equals(const Observer& other) const { + if(!other.is_geodetic()) + return 0; + + const GeodeticObserver& go = dynamic_cast(other); + + if(fabs(_eop.xp().uas() - go._eop.xp().uas()) > 1.0) + return false; + if(fabs(_eop.yp().uas() - go._eop.yp().uas()) > 1.0) + return false; + + return Observer::equals(other); +} + /** * Returns the fixed or momentary observing site for this observer. * diff --git a/src/cpp/Orbital.cpp b/src/cpp/Orbital.cpp index 1a555a36b..c06253e81 100644 --- a/src/cpp/Orbital.cpp +++ b/src/cpp/Orbital.cpp @@ -45,6 +45,56 @@ OrbitalSystem::OrbitalSystem(const novas_orbital_system *system) { _valid = (errno == 0); } +/** + * Checks if this orbital system matches another within typical tolerances. Two orbital system are + * considered equal, if they are defined with respect to the same reference plane, around the same + * major planet (or Solar-system position), and have the obliquity and ascending node (if + * obliquity is non-zero) to within 1 μas. + * + * Note, that an orbital system may not equal itself if it contains NAN or infinite components. + * + * @param other the other orbital system + * @return `true` if this orbital system and the other describe essentially the same system, + * within tolerances, or else `false`. + * + * @since 1.7 + * + * @sa operator==(), operator!=() + */ +bool OrbitalSystem::equals(const OrbitalSystem& other) const { + return novas_equals_orbsys(&_system, &other._system); +} + +/** + * Checks if this orbital system matches another within typical tolerances. Same as `equals()`. + * + * @param other the other orbital system + * @return `true` if this orbital system and the other describe essentially the same system, + * within tolerances, or else `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool OrbitalSystem::operator==(const OrbitalSystem& other) const { + return equals(other); +} + +/** + * Checks if this orbital system differs from another, given typical tolerances. Same as `!equals()`. + * + * @param other the other orbital system + * @return `true` if this orbital system and the other descrive distinct systems, given typical + * tolerances, or else `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool OrbitalSystem::operator!=(const OrbitalSystem& other) const { + return !equals(other); +} + /** * (_primarily for internal use_) Returns the underlying NOVAS C data structure, which * defines the orbital system. @@ -435,6 +485,63 @@ Orbital::Orbital(const OrbitalSystem& system, const Time& ref_time, const Coordi : Orbital(system, ref_time.jd(NOVAS_TDB), semi_major.m(), mean_anomaly.rad(), period.seconds()) {} +/** + * Checks if this Keplerian orbital matches another within typical tolerances. For two orbitals to + * be considered equal, they must have matching orbital systems, have the same reference dates + * to within ~10 ms (see `novas_time_equals()`), and: + * + * - angular parameters must match to within 1 μas. + * - semi-major axis must match to within 1 m. + * - eccentricity must match to within 10-12 time the geometric mean of the two semi-major axes. + * - mean motion must match to within 1 μas / cy. + * - apsis and node periods must match to within ~10 ms (see `novas_time_equals()`) + * + * Note, that an orbital may not equal itself if it contains NAN or infinite components. + * + * @param other the other orbital + * @return `true` if this orbital and the argument essentially describe the same Keplerian + * orbital, within the typical tolerances, or else `false`. + * + * @since 1.7 + * + * @sa operator==(), operator!=() + */ +bool Orbital::equals(const Orbital& other) const { + return novas_equals_orbital(&_orbit, &other._orbit); +} + +/** + * Checks if this Keplerian orbital matches another within typical tolerances. Same as `equals()`. + * See `equals()` for details. + * + * @param other the other orbital + * @return `true` if this orbital and the argument essentially describe the same Keplerian + * orbital, within the typical tolerances, or else `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool Orbital::operator==(const Orbital& other) const { + return equals(other); +} + +/** + * Checks if this Keplerian orbital differs from another given typical tolerances. Same as + * `!equals()`. See `equals()` for details. + * + * @param other the other orbital + * @return `true` if this orbital and the argument describe distinct Keplerian orbitals, + * given the typical tolerances, or else `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool Orbital::operator!=(const Orbital& other) const { + return !equals(other); +} + /** * (_for internal use_) Returns the underlying NOVAS C data structure containing the orbital * parameters. diff --git a/src/cpp/Source.cpp b/src/cpp/Source.cpp index bf59a4453..07ce53dfb 100644 --- a/src/cpp/Source.cpp +++ b/src/cpp/Source.cpp @@ -28,6 +28,60 @@ const struct novas_object *Source::_novas_object() const { return &_object; } +/** + * Checks if this astronomical source is the same as another within typical tolerances. Two + * sources are considered equals only if their types, names (case sensitive), and defining + * parameters all match within tolerances. + * + * Note, that a source instance may not equal itself if it contains NAN or infinite + * components. + * + * @param other the other source + * @return `true` if this source and the other source describe essentially the same + * astronomical object/position within tolerances, otherwise `false`. + * + * @since 1.7 + * + * @sa operator==(), operator!=() + */ +bool Source::equals(const Source& other) const { + return novas_equals_object(&_object, &other._object); +} + +/** + * Checks if this astronomical source is the same as another within typical tolerances. Two + * sources are considered equals only if their types, names (case sensitive), and defining + * parameters all match within tolerances. Same as `equals()`. + * + * @param other the other source + * @return `true` if this source and the other source describe essentially the same + * astronomical object/position within tolerances, otherwise `false`. + * + * @since 1.7 + * + * @sa equals(), operator!=() + */ +bool Source::operator==(const Source& other) const { + return equals(other); +} + +/** + * Checks if this astronomical source differs another given typical tolerances. + * Same as `!equals()`. + * + * @param other the other source + * @return `true` if this source and the other source describe two distinct + * astronomical objects/positions given the tolerances, otherwise + * `false`. + * + * @since 1.7 + * + * @sa equals(), operator==() + */ +bool Source::operator!=(const Source& other) const { + return !equals(other); +} + /** * Returns the name given to this source at instantiation. It may be lower-case unless the * `set_case_sensitive(true)` was called before instantiating the source. diff --git a/src/cpp/Vector.cpp b/src/cpp/Vector.cpp index 777f0698d..88a691e69 100644 --- a/src/cpp/Vector.cpp +++ b/src/cpp/Vector.cpp @@ -78,7 +78,8 @@ bool Vector::is_zero() const { } /** - * Checks if this vector is the same as another vector, within the specified precision. + * Checks if this vector is the same as another vector, within the specified precision. Note, + * that a vector may not equal itself if it contains NAN or infinite components. * * @param v the reference vector * @param precision precision for the equality test @@ -88,12 +89,7 @@ bool Vector::is_zero() const { * @since 1.6 */ bool Vector::equals(const Vector& v, double precision) const { - double d2 = 0.0; - for(int i = 0; i < 3; i++) { - double d = _component[i] - v._component[i]; - d2 += d * d; - } - return d2 < precision * precision; + return novas_equals_vector(_component, v._component, precision); } /** diff --git a/test/c99/test-super.c b/test/c99/test-super.c index 5785c7bb4..104595540 100644 --- a/test/c99/test-super.c +++ b/test/c99/test-super.c @@ -1903,7 +1903,7 @@ static int test_make_cat_object() { make_cat_entry("test", "FK4", 123, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, &star); if(!is_ok("make_cat_object", make_cat_object(&star, &source))) return 1; - if(!is_ok("make_cat_object:check", memcmp(&source.star, &star, sizeof(star)))) return 1; + if(!is_ok("make_cat_object:check", !novas_equals_cat_entry(&source.star, &star))) return 1; return 0; } @@ -1917,8 +1917,8 @@ static int test_airborne_observer() { if(!is_ok("airborne_observer:make_on_surface", make_on_surface(1.0, 2.0, 3.0, 4.0, 5.0, &loc))) return 1; if(!is_ok("airborne_observer:make", make_airborne_observer(&loc, vel, &obs))) return 1; - if(!is_ok("airborne_observer:check:on_surf", memcmp(&obs.on_surf, &loc, sizeof(loc)))) return 1; - if(!is_ok("airborne_observer:check:vel", memcmp(&obs.near_earth.sc_vel, &vel, sizeof(vel)))) return 1; + if(!is_ok("airborne_observer:check:on_surf", !novas_equals_on_surface(&obs.on_surf, &loc))) return 1; + if(!is_ok("airborne_observer:check:vel", !novas_equals_vector(obs.near_earth.sc_vel, vel, 1e-6))) return 1; if(!is_ok("airborne_observer:make_observer_at_geocenter", make_observer_at_geocenter(&gc))) return 1; if(!is_ok("airborne_observer:geo_posvel:gc", geo_posvel(tdb, ut12tt, NOVAS_REDUCED_ACCURACY, &gc, epos, evel))) return 1; @@ -1945,8 +1945,8 @@ static int test_solar_system_observer() { int i; if(!is_ok("solar_system_observer:make", make_solar_system_observer(pos, vel, &obs))) return 1; - if(!is_ok("solar_system_observer:check:pos", memcmp(&obs.near_earth.sc_pos, &pos, sizeof(pos)))) return 1; - if(!is_ok("solar_system_observer:check:vel", memcmp(&obs.near_earth.sc_vel, &vel, sizeof(vel)))) return 1; + if(!is_ok("solar_system_observer:check:pos", !novas_equals_vector(obs.near_earth.sc_pos, pos, 1e-3 / NOVAS_AU))) return 1; + if(!is_ok("solar_system_observer:check:vel", !novas_equals_vector(obs.near_earth.sc_vel, vel, 1e2 / NOVAS_AU))) return 1; if(!is_ok("solar_system_observer:make_observer_at_geocenter", make_observer_at_geocenter(&gc))) return 1; if(!is_ok("solar_system_observer:obs_posvel", obs_posvel(tdb, ut12tt, NOVAS_REDUCED_ACCURACY, &obs, NULL, NULL, opos, ovel))) return 1; @@ -2370,8 +2370,8 @@ static int test_make_frame() { if(!is_ok("make_frame", novas_make_frame(NOVAS_REDUCED_ACCURACY, &obs, &ts, 1.0, 2.0, &frame))) return 1; - if(!is_ok("make_frame:time", memcmp(&frame.time, &ts, sizeof(ts)))) return 1; - if(!is_ok("make_frame:obs", memcmp(&frame.observer, &obs, sizeof(obs)))) return 1; + if(!is_ok("make_frame:time", !novas_equals_timespec(&frame.time, &ts))) return 1; + if(!is_ok("make_frame:obs", !novas_equals_observer(&frame.observer, &obs))) return 1; if(!is_ok("make_frame:dx", frame.dx != 1.0)) return 1; if(!is_ok("make_frame:dy", frame.dy != 2.0)) return 1; @@ -2390,10 +2390,10 @@ static int test_change_observer() { make_observer_on_surface(1.0, 2.0, 3.0, 4.0, 1001.0, &obs); if(!is_ok("change_observer", novas_change_observer(&frame, &obs, &out))) return 1; - if(!is_ok("change_observer:check", memcmp(&out.observer, &obs, sizeof(obs)))) return 1; + if(!is_ok("change_observer:check", !novas_equals_observer(&out.observer, &obs))) return 1; if(!is_ok("change_observer:same", novas_change_observer(&frame, &obs, &frame))) return 1; - if(!is_ok("change_observer:same:check", memcmp(&frame.observer, &obs, sizeof(obs)))) return 1; + if(!is_ok("change_observer:same:check", !novas_equals_observer(&frame.observer, &obs))) return 1; return 0; } @@ -5257,11 +5257,506 @@ static int test_equ_offset_by() { return n; } +static int test_equals_vector() { + int n = 0; + + double a[3] = {0.0, 1.0, 2.0}, b[3]; + int i; + + memcpy(b, a, sizeof(b)); + if(!is_ok("equals_vector:null", novas_equals_vector(NULL, NULL, 1.0) != 0)) n++; + if(!is_ok("equals_vector:null:a", novas_equals_vector(NULL, a, 1.0) != 0)) n++; + if(!is_ok("equals_vector:null:b", novas_equals_vector(a, NULL, 1.0) != 0)) n++; + if(!is_ok("equals_vector", novas_equals_vector(a, b, 1e-15) == 0)) n++; + if(!is_ok("equals_vector:tol=0", novas_equals_vector(a, b, 0.0) == 0)) n++; + + for(i = 0; i < 3; i++) { + char label[40] = {'\0'}; + + sprintf(label, "equals_vector:%d", i); + b[i] += 1e-12; + if(!is_ok(label, novas_equals_vector(a, b, 1e-15) != 0)) n++; + b[i] = a[i]; + } + + return n; +} + +static int test_equals_timespec() { + int n = 0; + novas_timespec a = {}, b; + + novas_set_time(NOVAS_TT, NOVAS_JD_J2000, 32, 0.1, &a); + b = a; + + if(!is_ok("equals_timespec:null", novas_equals_timespec(NULL, NULL) != 0)) n++; + if(!is_ok("equals_timespec:null:a", novas_equals_timespec(NULL, &a) != 0)) n++; + if(!is_ok("equals_timespec:null:b", novas_equals_timespec(&a, NULL) != 0)) n++; + if(!is_ok("equals_timespec", novas_equals_timespec(&a, &b) == 0)) n++; + + b.ijd_tt++; + if(!is_ok("equals_timespec:ijd", novas_equals_timespec(&a, &b) != 0)) n++; + b = a; b.fjd_tt += 2e-7; + if(!is_ok("equals_timespec:fjd:fine", novas_equals_timespec(&a, &b) != 0)) n++; + b = a; b.dut1 += 1e-5; + if(!is_ok("equals_timespec:dut1", novas_equals_timespec(&a, &b) != 0)) n++; + b = a; b.ut1_to_tt += 1e-5; + if(!is_ok("equals_timespec:ut1_to_tt", novas_equals_timespec(&a, &b) != 0)) n++; + b = a; b.tt2tdb += 1e-5; + if(!is_ok("equals_timespec:tt2tdb", novas_equals_timespec(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_on_surface() { + int n = 0; + on_surface a = {}, b; + + make_on_surface(10.0, 20.0, 30.0, 40.0, 1000.0, &a); + a.humidity = 50.0; + + b = a; + + if(!is_ok("equals_on_surface:null", novas_equals_on_surface(NULL, NULL) != 0)) n++; + if(!is_ok("equals_on_surface:null:a", novas_equals_on_surface(NULL, &a) != 0)) n++; + if(!is_ok("equals_on_surface:null:b", novas_equals_on_surface(&a, NULL) != 0)) n++; + if(!is_ok("equals_on_surface", novas_equals_on_surface(&a, &b) == 0)) n++; + + b.longitude += 2.0e-6 / 3600.0; + if(!is_ok("equals_on_surface:longitude", novas_equals_on_surface(&a, &b) != 0)) n++; + + b = a; b.longitude += 360.0; + if(!is_ok("equals_on_surface:longitude:wrap", novas_equals_on_surface(&a, &b) == 0)) n++; + + b = a; b.latitude += 2.0e-6 / 3600.0; + if(!is_ok("equals_on_surface:latitude", novas_equals_on_surface(&a, &b) != 0)) n++; + + b = a; b.height += 2.0e-3; + if(!is_ok("equals_on_surface:height", novas_equals_on_surface(&a, &b) != 0)) n++; + + b = a; b.temperature += 2.0e-3; + if(!is_ok("equals_on_surface:temperature", novas_equals_on_surface(&a, &b) != 0)) n++; + + b = a; b.pressure += 2.0e-3; + if(!is_ok("equals_on_surface:pressure", novas_equals_on_surface(&a, &b) != 0)) n++; + + b = a; b.humidity += 2.0e-3; + if(!is_ok("equals_on_surface:humidity", novas_equals_on_surface(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_near_earth() { + int n = 0; + + double p[3] = {100.0, -200.0, 300.0}, v[3] = {-1.0, 2.0, 3.0}; + in_space a = {}, b; + + make_in_space(p, v, &a); + b = a; + + if(!is_ok("equals_near_earth:null", novas_equals_near_earth(NULL, NULL) != 0)) n++; + if(!is_ok("equals_near_earth:null:a", novas_equals_near_earth(NULL, &a) != 0)) n++; + if(!is_ok("equals_near_earth:null:b", novas_equals_near_earth(&a, NULL) != 0)) n++; + if(!is_ok("equals_near_earth", novas_equals_near_earth(&a, &b) == 0)) n++; + + b.sc_pos[0] += 2e-6; + if(!is_ok("equals_near_earth:pos", novas_equals_near_earth(&a, &b) != 0)) n++; + + b = a; b.sc_vel[0] += 2e-6; + if(!is_ok("equals_near_earth:vel", novas_equals_near_earth(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_ssb_posvel() { + int n = 0; + + double p[3] = {10.0, -20.0, 30.0}, v[3] = {-0.01, 0.02, 0.03}; + in_space a = {}, b; + + make_in_space(p, v, &a); + b = a; + + if(!is_ok("equals_ssb_posvel:null", novas_equals_ssb_posvel(NULL, NULL) != 0)) n++; + if(!is_ok("equals_ssb_posvel:null:a", novas_equals_ssb_posvel(NULL, &a) != 0)) n++; + if(!is_ok("equals_ssb_posvel:null:b", novas_equals_ssb_posvel(&a, NULL) != 0)) n++; + if(!is_ok("equals_ssb_posvel", novas_equals_ssb_posvel(&a, &b) == 0)) n++; + + b.sc_pos[0] += 2.0 / NOVAS_AU; + if(!is_ok("equals_ssb_posvel:pos", novas_equals_ssb_posvel(&a, &b) != 0)) n++; + + b = a; b.sc_vel[0] += 20.0 / NOVAS_AU; + if(!is_ok("equals_ssb_posvel:vel", novas_equals_ssb_posvel(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_observer() { + int n = 0; + observer a = {}, b, c; + on_surface surf = {}; + double p[3] = {10.0, -20.0, 30.0}, v[3] = {-1.0, 2.0, 3.0}; + + if(!is_ok("equals_observer:null", novas_equals_observer(NULL, NULL) != 0)) n++; + if(!is_ok("equals_observer:null:a", novas_equals_observer(NULL, &a) != 0)) n++; + if(!is_ok("equals_observer:null:b", novas_equals_observer(&a, NULL) != 0)) n++; + + make_observer_at_geocenter(&a); + b = a; + if(!is_ok("equals_observer:gc==gc", novas_equals_observer(&a, &b) == 0)) n++; + + make_observer_on_surface(10.0, 20.0, 30.0, 40.0, 1000.0, &b); + if(!is_ok("equals_observer:gc!=earth", novas_equals_observer(&a, &b) != 0)) n++; + c = b; + if(!is_ok("equals_observer:earth", novas_equals_observer(&b, &c) == 0)) n++; + + surf = b.on_surf; + make_airborne_observer(&surf, v, &b); + if(!is_ok("equals_observer:gc!=air", novas_equals_observer(&a, &b) != 0)) n++; + c = b; + if(!is_ok("equals_observer:air", novas_equals_observer(&b, &c) == 0)) n++; + + make_observer_in_space(p, v, &b); + if(!is_ok("equals_observer:gc!=orbit", novas_equals_observer(&a, &b) != 0)) n++; + c = b; + if(!is_ok("equals_observer:orbit", novas_equals_observer(&b, &c) == 0)) n++; + + make_solar_system_observer(p, v, &b); + if(!is_ok("equals_observer:gc!=ss", novas_equals_observer(&a, &b) != 0)) n++; + c = b; + if(!is_ok("equals_observer:ss", novas_equals_observer(&b, &c) == 0)) n++; + + a.where = (enum novas_observer_place) -1; + b = a; + if(!is_ok("equals_observer:where:-1", novas_equals_observer(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_cat_entry() { + int n = 0; + cat_entry a = {}, b; + + make_cat_entry("test", "TST", 123, 10.0, 20.0, -0.1, -0.2, 1e-3, 100.0, &a); + b = a; + + if(!is_ok("equals_cat_entry:null", novas_equals_cat_entry(NULL, NULL) != 0)) n++; + if(!is_ok("equals_cat_entry:null:a", novas_equals_cat_entry(NULL, &a) != 0)) n++; + if(!is_ok("equals_cat_entry:null:b", novas_equals_cat_entry(&a, NULL) != 0)) n++; + if(!is_ok("equals_cat_entry", novas_equals_cat_entry(&a, &b) == 0)) n++; + + b.starname[0] = 'X'; + if(!is_ok("equals_cat_entry:starname", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.catalog[0] = 'X'; + if(!is_ok("equals_cat_entry:catalog", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.starnumber++; + if(!is_ok("equals_cat_entry:starnumber", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.ra += 1e-7; + if(!is_ok("equals_cat_entry:ra", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.ra += 24.0; + if(!is_ok("equals_cat_entry:ra:wrap", novas_equals_cat_entry(&a, &b) == 0)) n++; + + b = a; b.dec += 1e-6; + if(!is_ok("equals_cat_entry:dec", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.promora += 1e-5; + if(!is_ok("equals_cat_entry:promora", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.promodec += 1e-5; + if(!is_ok("equals_cat_entry:promodec", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.parallax += 1e-6; + if(!is_ok("equals_cat_entry:parallax", novas_equals_cat_entry(&a, &b) != 0)) n++; + + b = a; b.radialvelocity += 1e-5; + if(!is_ok("equals_cat_entry:radialvelocity", novas_equals_cat_entry(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_orbsys() { + int n = 0; + novas_orbital_system a = {}, b; + + a.center = NOVAS_JUPITER; + a.plane = NOVAS_EQUATORIAL_PLANE; + a.type = NOVAS_ICRS; + a.obl = 0.0; + a.Omega = 133.0; + + b = a; + + if(!is_ok("equals_orbsys:null", novas_equals_orbsys(NULL, NULL) != 0)) n++; + if(!is_ok("equals_orbsys:null:a", novas_equals_orbsys(NULL, &a) != 0)) n++; + if(!is_ok("equals_orbsys:null:b", novas_equals_orbsys(&a, NULL) != 0)) n++; + if(!is_ok("equals_orbsys", novas_equals_orbsys(&a, &b) == 0)) n++; + + b.Omega -= 90.0; + if(!is_ok("equals_orbsys:Omega:no-obl", novas_equals_orbsys(&a, &b) == 0)) n++; + + a.obl = b.obl = 1.0; + if(!is_ok("equals_orbsys:Omega:obl", novas_equals_orbsys(&a, &b) != 0)) n++; + + b = a; b.center = NOVAS_SATURN; + if(!is_ok("equals_orbsys:center", novas_equals_orbsys(&a, &b) != 0)) n++; + + b = a; b.plane = NOVAS_ECLIPTIC_PLANE; + if(!is_ok("equals_orbsys:plane", novas_equals_orbsys(&a, &b) != 0)) n++; + + b = a; b.type = NOVAS_GCRS; + if(!is_ok("equals_orbsys:type", novas_equals_orbsys(&a, &b) != 0)) n++; + + b = a; b.obl += 1e-6; + if(!is_ok("equals_orbsys:obl", novas_equals_orbsys(&a, &b) != 0)) n++; + + b = a; b.Omega += 360.0; + if(!is_ok("equals_orbsys:Omega:wrap", novas_equals_orbsys(&a, &b) == 0)) n++; + + return n; +} + + +static int test_equals_orbital() { + int n = 0; + novas_orbital_system sys = NOVAS_ORBITAL_SYSTEM_INIT; + novas_orbital a = {}, b; + + a.M0 = 10.0; + a.n = 0.1; + a.a = 2.0; + a.system = sys; + + b = a; + if(!is_ok("equals_orbital:null", novas_equals_orbital(NULL, NULL) != 0)) n++; + if(!is_ok("equals_orbital:null:a", novas_equals_orbital(NULL, &a) != 0)) n++; + if(!is_ok("equals_orbital:null:b", novas_equals_orbital(&a, NULL) != 0)) n++; + if(!is_ok("equals_orbital", novas_equals_orbital(&a, &b) == 0)) n++; + + b = a; b.system.obl += 0.1; + if(!is_ok("equals_orbital:system", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.jd_tdb += 1e-6; + if(!is_ok("equals_orbital:jd_tdb", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.omega -= 90.0; + if(!is_ok("equals_orbital:omega:no-e", novas_equals_orbital(&a, &b) == 0)) n++; + + a.e = b.e = 1e-5; + if(!is_ok("equals_orbital:omega:e", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.omega += 360.0; + if(!is_ok("equals_orbital:omega:wrap", novas_equals_orbital(&a, &b) == 0)) n++; + + b = a; b.Omega -= 90.0; + if(!is_ok("equals_orbital:Omega:no-i", novas_equals_orbital(&a, &b) == 0)) n++; + + a.i = b.i = 0.1; + if(!is_ok("equals_orbital:Omega:i", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.Omega += 360.0; + if(!is_ok("equals_orbital:Omega:wrap", novas_equals_orbital(&a, &b) == 0)) n++; + + b = a; b.a += 2.0 / NOVAS_AU; + if(!is_ok("equals_orbital:a", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.M0 += 1e-6; + if(!is_ok("equals_orbital:M0", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.M0 += 360.0; + if(!is_ok("equals_orbital:M0:wrap", novas_equals_orbital(&a, &b) == 0)) n++; + + b = a; b.n += 1e-9; + if(!is_ok("equals_orbital:n", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.e += 1e-11; + if(!is_ok("equals_orbital:e", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.i += 1e-6; + if(!is_ok("equals_orbital:i", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.apsis_period += 1e-6; + if(!is_ok("equals_orbital:apsis_period", novas_equals_orbital(&a, &b) != 0)) n++; + + b = a; b.node_period += 1e-6; + if(!is_ok("equals_orbital:node_period", novas_equals_orbital(&a, &b) != 0)) n++; + + return n; +} + + +static int test_equals_object() { + int n = 0; + + object a = {}, b, c; + cat_entry e = {}; + novas_orbital o = {}; + + make_planet(NOVAS_MOON, &a); + b = a; + + if(!is_ok("equals_object:null", novas_equals_object(NULL, NULL) != 0)) n++; + if(!is_ok("equals_object:null:a", novas_equals_object(NULL, &a) != 0)) n++; + if(!is_ok("equals_object:null:b", novas_equals_object(&a, NULL) != 0)) n++; + if(!is_ok("equals_object:planet", novas_equals_object(&a, &b) == 0)) n++; + + b = a; b.name[0] = 'X'; + if(!is_ok("equals_object:name", novas_equals_object(&a, &b) != 0)) n++; + + b = a; b.number++; + if(!is_ok("equals_object:number", novas_equals_object(&a, &b) != 0)) n++; + + b = a; b.type = a.type = (enum novas_object_type) -1; + if(!is_ok("equals_object:type:-1", novas_equals_object(&a, &b) != 0)) n++; + + make_cat_entry("test", "TST", 123, 10.0, 20.0, -0.1, -0.2, 1e-3, 100.0, &e); + make_cat_object(&e, &b); + if(!is_ok("equals_object:cat!=planet", novas_equals_object(&a, &b) != 0)) n++; + c = b; + if(!is_ok("equals_object:cat", novas_equals_object(&b, &c) == 0)) n++; + + make_ephem_object("Test", 123, &b); + if(!is_ok("equals_object:cat!=ephem", novas_equals_object(&a, &b) != 0)) n++; + c = b; + if(!is_ok("equals_object:ephem", novas_equals_object(&b, &c) == 0)) n++; + + make_orbital_object("Test", 123, &o, &b); + if(!is_ok("equals_object:orbit!=planet", novas_equals_object(&a, &b) != 0)) n++; + c = b; + if(!is_ok("equals_object:orbit", novas_equals_object(&b, &c) == 0)) n++; + + return n; +} + +static int test_equals_sky_pos() { + int n = 0; + sky_pos a = {}, b; + + a.ra = 10.0; + a.dec = -20.0; + a.dis = 1e9; + a.rv = -30.0; + radec2vector(a.ra, a.dec, 1.0, a.r_hat); + + b = a; + + if(!is_ok("equals_sky_pos:null", novas_equals_sky_pos(NULL, NULL) != 0)) n++; + if(!is_ok("equals_sky_pos:null:a", novas_equals_sky_pos(NULL, &a) != 0)) n++; + if(!is_ok("equals_sky_pos:null:b", novas_equals_sky_pos(&a, NULL) != 0)) n++; + if(!is_ok("equals_sky_pos", novas_equals_sky_pos(&a, &b) == 0)) n++; + + b = a; a.ra += 1e-8; + if(!is_ok("equals_sky_pos:ra", novas_equals_sky_pos(&a, &b) != 0)) n++; + + b = a; a.dec += 1e-9; + if(!is_ok("equals_sky_pos:dec", novas_equals_sky_pos(&a, &b) != 0)) n++; + + b = a; a.dis += 2e3; + if(!is_ok("equals_sky_pos:dis", novas_equals_sky_pos(&a, &b) != 0)) n++; + + b = a; a.rv += 2e-6; + if(!is_ok("equals_sky_pos:rv", novas_equals_sky_pos(&a, &b) != 0)) n++; + + b = a; a.r_hat[0] += 2e-12; + if(!is_ok("equals_sky_pos:r_hat", novas_equals_sky_pos(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_planet_bundle() { + int n = 0; + + novas_planet_bundle a = {}, b; + + b = a; + + if(!is_ok("equals_planet_bundle:null", novas_equals_planet_bundle(NULL, NULL) != 0)) n++; + if(!is_ok("equals_planet_bundle:null:a", novas_equals_planet_bundle(NULL, &a) != 0)) n++; + if(!is_ok("equals_planet_bundle:null:b", novas_equals_planet_bundle(&a, NULL) != 0)) n++; + if(!is_ok("equals_planet_bundle", novas_equals_planet_bundle(&a, &b) == 0)) n++; + + b = a; a.mask |= 1 << NOVAS_SUN; + if(!is_ok("equals_planet_bundle:mask", novas_equals_planet_bundle(&a, &b) != 0)) n++; + + b = a; b.pos[NOVAS_SUN][0] += 2.0 / NOVAS_AU; + if(!is_ok("equals_planet_bundle:mask", novas_equals_planet_bundle(&a, &b) != 0)) n++; + + b = a; b.vel[NOVAS_SUN][0] += 20.0 / NOVAS_AU; + if(!is_ok("equals_planet_bundle:mask", novas_equals_planet_bundle(&a, &b) != 0)) n++; + + return n; +} + +static int test_equals_frame() { + int n = 0; + observer obs = {}; + novas_timespec ts = {}; + novas_frame a = {}, b; + + make_observer_at_geocenter(&obs); + novas_set_time(NOVAS_TT, NOVAS_JD_J2000, 32, 0.0, &ts); + novas_make_frame(NOVAS_REDUCED_ACCURACY, &obs, &ts, 100.0, -200.0, &a); + + b = a; + + if(!is_ok("equals_frame:null", novas_equals_frame(NULL, NULL) != 0)) n++; + if(!is_ok("equals_frame:null:a", novas_equals_frame(NULL, &a) != 0)) n++; + if(!is_ok("equals_frame:null:b", novas_equals_frame(&a, NULL) != 0)) n++; + if(!is_ok("equals_frame", novas_equals_frame(&a, &b) == 0)) n++; + + b = a; b.accuracy = NOVAS_FULL_ACCURACY; + if(!is_ok("equals_frame:accuracy", novas_equals_frame(&a, &b) != 0)) n++; + + b = a; b.planets.mask = 0; + if(!is_ok("equals_frame:planets", novas_equals_frame(&a, &b) != 0)) n++; + + novas_set_time(NOVAS_TT, NOVAS_JD_J2000 + 2e-7, 32, 0.0, &ts); + novas_make_frame(NOVAS_REDUCED_ACCURACY, &obs, &ts, 100.0, -200.0, &b); + if(!is_ok("equals_frame:time", novas_equals_frame(&a, &b) != 0)) n++; + + // Geodetic.... + make_observer_on_surface(30.0, -40.0, 3000.0, 10.0, 900.0, &obs); + + novas_make_frame(NOVAS_REDUCED_ACCURACY, &obs, &ts, 100.0, -200.0, &a); + if(!is_ok("equals_frame:earth!=gc", novas_equals_frame(&a, &b) != 0)) n++; + + b = a; + if(!is_ok("equals_frame:earth:polar", novas_equals_frame(&a, &b) == 0)) n++; + + b = a; b.dx += 2e-3; + if(!is_ok("equals_frame:earth:dx", novas_equals_frame(&a, &b) != 0)) n++; + + b = a; b.dy += 2e-3; + if(!is_ok("equals_frame:earth:dy", novas_equals_frame(&a, &b) != 0)) n++; + + b = a; b.dx = NAN; + if(!is_ok("equals_frame:earth:dx:NAN", novas_equals_frame(&a, &b) != 0)) n++; + + b = a; b.dy = NAN; + if(!is_ok("equals_frame:earth:dy:NAN", novas_equals_frame(&a, &b) != 0)) n++; + + novas_make_frame(NOVAS_REDUCED_ACCURACY, &obs, &ts, NAN, NAN, &a); + b = a; + if(!is_ok("equals_frame:earth:no-polar", novas_equals_frame(&a, &b) != 0)) n++; + + b = a; b.dx = 100.0; + if(!is_ok("equals_frame:earth:dx:!nan", novas_equals_frame(&a, &b) != 0)) n++; + + b = a; b.dy = -200.0; + if(!is_ok("equals_frame:earth:dy:!nan", novas_equals_frame(&a, &b) != 0)) n++; + + return n; +} + int main(int argc, char *argv[]) { int n = 0; if(argc > 1) - dataPath = argv[1]; + dataPath = argv[1]; novas_debug(NOVAS_DEBUG_ON); enable_earth_sun_hp(1); @@ -5431,6 +5926,20 @@ int main(int argc, char *argv[]) { if(test_offset_by()) n++; if(test_equ_offset_by()) n++; + if(test_equals_vector()) n++; + if(test_equals_timespec()) n++; + if(test_equals_on_surface()) n++; + if(test_equals_near_earth()) n++; + if(test_equals_ssb_posvel()) n++; + if(test_equals_observer()) n++; + if(test_equals_cat_entry()) n++; + if(test_equals_orbsys()) n++; + if(test_equals_orbital()) n++; + if(test_equals_object()) n++; + if(test_equals_sky_pos()) n++; + if(test_equals_planet_bundle()) n++; + if(test_equals_frame()) n++; + n += test_dates(); if(n) fprintf(stderr, " -- FAILED %d tests\n", n); diff --git a/test/cpp/CatalogEntryTest.cpp b/test/cpp/CatalogEntryTest.cpp index 60ddd29a8..c9bc2f4e7 100644 --- a/test/cpp/CatalogEntryTest.cpp +++ b/test/cpp/CatalogEntryTest.cpp @@ -33,6 +33,8 @@ int main() { if(!test.check("parallax(invalid)", !x.parallax().is_valid())) n++; if(!test.check("equatorial(invalid)", !x.equatorial().is_valid())) n++; if(!test.check("to_source(invalid)", !x.to_source().is_valid())) n++; + if(!test.check("operator==(invalid)", !(x == x))) n++; + if(!test.check("operator!=(invalid)", x != x)) n++; char longName[SIZE_OF_OBJ_NAME + 1] = {'\0'}; memset(longName, 'X', SIZE_OF_OBJ_NAME); @@ -44,6 +46,8 @@ int main() { if(!test.equals("name()", a.name(), "test")) n++; if(!test.check("equatorial()", a.equatorial() == Equatorial("12:00:00.00", "-30:00:00"))) n++; if(!test.check("system()", a.system() == Equinox::icrs())) n++; + if(!test.check("operator==()", a == a)) n++; + if(!test.check("operator!=()", !(a != a))) n++; a.distance(Coordinate(Unit::pc)); if(!test.equals("distance(1pc)", a.distance().m(), Unit::pc, 1e-15 * Unit::pc)) n++; diff --git a/test/cpp/FrameTest.cpp b/test/cpp/FrameTest.cpp index d63d98844..61792dfce 100644 --- a/test/cpp/FrameTest.cpp +++ b/test/cpp/FrameTest.cpp @@ -25,12 +25,18 @@ int main() { if(!test.check("observer_ssb_velocity(invalid)", !x.observer_ssb_velocity().is_valid())) n++; if(!test.check("geometric_moon_elp2000(invalid)", !x.geometric_moon_elp2000().is_valid())) n++; if(!test.check("apparent_moon_elp2000(invalid)", !x.apparent_moon_elp2000().is_valid())) n++; + if(!test.check("operator==(invalid)", !(x == x))) n++; + if(!test.check("operator!=(invalid)", x != x)) n++; + if(!test.check("invalid observer", !Frame(Observer::undefined(), Time::j2000(), (enum novas_accuracy) -1).is_valid())) n++; if(!test.check("invalid time", !Frame(gc, Time::undefined(), (enum novas_accuracy) -1).is_valid())) n++; if(!test.check("invalid accuracy", !Frame(gc, Time::j2000(), (enum novas_accuracy) -1).is_valid())) n++; Frame a = Frame::reduced_accuracy(gc, Time::j2000()); + if(!test.check("operator==()", a == a)) n++; + if(!test.check("operator!=()", !(a != a))) n++; + if(!test.check("operator!=(b1950)", Frame::reduced_accuracy(gc, Time::b1950()) != a)) n++; if(!test.equals("accuracy()", a.accuracy(), NOVAS_REDUCED_ACCURACY)) n++; if(!test.check("time()", a.time() == Time::j2000())) n++; if(!test.check("observer_ssb_position()", a.observer_ssb_position() == Position(a._novas_frame()->obs_pos, Unit::AU))) n++; @@ -43,6 +49,11 @@ int main() { a = a; // @suppress("Assignment to itself") if(!test.equals("self assign", a.to_string(), "Frame for Geocentric Observer at 2000-01-01T11:58:55.816 UTC")) n++; + Frame a1(a); + novas_frame *f1 = (novas_frame *) a1._novas_frame(); + f1->accuracy = NOVAS_FULL_ACCURACY; + if(!test.check("operator!=(acc)", a1 != a)) n++; + double mp[3] = {0.0}, mv[3] = {0.0}; novas_moon_elp_posvel_fp(a._novas_frame(), 0.1, NOVAS_ICRS, mp, mv); Geometric mg = a.geometric_moon_elp2000(0.1); @@ -65,6 +76,7 @@ int main() { b = Frame(gc, Time::j2000(), NOVAS_FULL_ACCURACY); if(!test.check("Frame(full accuracy).is_valid()", !b.is_valid())) n++; + if(!test.check("operator!=(acc)", a != b)) n++; b = Frame(Observer::undefined(), Time::j2000()); if(!test.check("Frame(obs invalid).is_valid()", !b.is_valid())) n++; @@ -88,6 +100,7 @@ int main() { if(!test.check("is_valid()", gf.is_valid())) n++; if(!test.check("observer().is_geodetic()", gf.observer().is_geodetic())) n++; if(!test.check("observer().is_geocentric()", !gf.observer().is_geocentric())) n++; + if(!test.check("operator!=(gc)", go != gc)) n++; if(!test.equals("to_string()", gf.to_string(), "Frame for GeodeticObserver at Site (E 10d 00m 00.000s, S 20d 00m 00.000s, altitude 30 m) at 2000-01-01T11:58:55.816 UTC")) n++; gf = Frame::reduced_accuracy(go, Time::undefined()); diff --git a/test/cpp/ObserverTest.cpp b/test/cpp/ObserverTest.cpp index 943f94def..f9b6726c8 100644 --- a/test/cpp/ObserverTest.cpp +++ b/test/cpp/ObserverTest.cpp @@ -23,16 +23,18 @@ int main() { Apparent app = Apparent::from_tod(1.234 * Unit::hour_angle, -23.45 * Unit::deg, Observer::at_geocenter().reduced_accuracy_frame_at(Time::b1950())); Observer x = Observer::undefined(); - const Observer *copy; - if(!test.check("undefined()", !x.is_valid())) n++; if(!test.equals("undefined().type", (int) x.type(), -1)) n++; + if(!test.check("undefined().operator==", !(x == x))) n++; + if(!test.check("undefined().operator!=", x != x)) n++; if(!test.check("undefined().to_interferometric", !x.to_interferometric(app).is_valid())) n++; - copy = x.copy(); + const Observer *copy = x.copy(); if(!test.check("undefined().copy()", !copy->is_valid())) n++; delete copy; + + if(!test.check("invalid Site", !Observer::on_earth(Site::undefined(), eop).is_valid())) n++; if(!test.check("invalid Site (moving)", !Observer::moving_on_earth(Site::undefined(), Velocity::stationary(), eop).is_valid())) n++; if(!test.check("invalid Site (moving ENU)", !Observer::moving_on_earth(Site::undefined(), eop, ScalarVelocity(1.0), Angle(0.0)).is_valid())) n++; @@ -57,6 +59,8 @@ int main() { GeodeticObserver gdx = Observer::moving_on_earth(Site::undefined(), Velocity::undefined(), EOP::undefined()); if(!test.check("is_valid(invalid)", !gdx.is_valid())) n++; + if(!test.check("operator==(invalid)", !(gdx == gdx))) n++; + if(!test.check("operator!(invalid)", gdx != gdx)) n++; if(!test.check("site(invalid)", !gdx.site().is_valid())) n++; if(!test.check("itrs_velocity(invalid)", !gdx.itrs_velocity().is_valid())) n++; if(!test.check("enu_velocity(invalid)", !gdx.enu_velocity().is_valid())) n++; @@ -65,6 +69,9 @@ int main() { GeodeticObserver g1 = Observer::on_earth(site, eop); if(!test.check("is_valid(on_earth)", g1.is_valid())) n++; if(!test.equals("type(on_earth)", g1.type(), NOVAS_OBSERVER_ON_EARTH)) n++; + if(!test.check("operator==(on earth)", g1 == g1)) n++; + if(!test.check("operator!(on_earth)", !(g1 != g1))) n++; + if(!test.check("operator!(on_earth-vs-invalid)", g1 != gdx)) n++; if(!test.check("is_geodetic(on_earth)", g1.is_geodetic())) n++; if(!test.check("is_geocentric(on_earth)", !g1.is_geocentric())) n++; if(!test.check("site()", g1.site() == site)) n++; @@ -76,9 +83,16 @@ int main() { "GeodeticObserver at Site (W 114d 35m 29.612s, N 57d 17m 44.806s, altitude 75 m)")) n++; copy = g1.copy(); - if(!test.check("copy(on_earth)", memcmp(copy->_novas_observer(), g1._novas_observer(), sizeof(observer)) == 0)) n++; + if(!test.check("copy(on_earth)", *copy == g1)) n++; delete copy; + EOP ex1 = EOP(eop.leap_seconds(), eop.dUT1(), eop.xp() + Angle(2.0 * Unit::mas), eop.yp()); + if(!test.check("operator!=(xp+)", Observer::on_earth(site, ex1) != g1)) n++; + + EOP ey1 = EOP(eop.leap_seconds(), eop.dUT1(), eop.xp(), eop.yp() + Angle(2.0 * Unit::mas)); + if(!test.check("operator!=(yp+)", Observer::on_earth(site, ey1) != g1)) n++; + if(!test.check("operator!=(gc)", Observer::at_geocenter() != g1)) n++; + EOP e = g1.eop_at(app.frame().time()); double uvw[3] = {0.0}; @@ -118,7 +132,7 @@ int main() { "GeodeticObserver at Site (W 114d 35m 29.612s, N 57d 17m 44.806s, altitude 75 m) moving at ENU Velocity (0.002 km/s, 0.000 km/s, 0.003 km/s)")) n++; copy = g2.copy(); - if(!test.check("copy(moving)", memcmp(copy->_novas_observer(), g2._novas_observer(), sizeof(observer)) == 0)) n++; + if(!test.check("copy(moving)", *copy == g2)) n++; delete copy; @@ -134,6 +148,9 @@ int main() { GeocentricObserver gc = Observer::at_geocenter(); if(!test.check("is_valid(gc)", gc.is_valid())) n++; if(!test.equals("type(gc)", gc.type(), NOVAS_OBSERVER_AT_GEOCENTER)) n++; + if(!test.check("operator==()", gc == gc)) n++; + if(!test.check("operator!()", !(gc != gc))) n++; + if(!test.check("operator!(earth)", gc != g1)) n++; if(!test.check("is_geocentric(gc)", gc.is_geocentric())) n++; if(!test.check("is_geodetic(gc)", !gc.is_geodetic())) n++; if(!test.check("geocentric_position(gc)", gc.gcrs_position() == Position::origin())) n++; @@ -144,7 +161,7 @@ int main() { copy = gc.copy(); - if(!test.check("copy(gc)", memcmp(copy->_novas_observer(), gc._novas_observer(), sizeof(observer)) == 0)) n++; + if(!test.check("copy(gc)", *copy == gc)) n++; delete copy; o = gc._novas_observer(); @@ -162,6 +179,9 @@ int main() { GeocentricObserver o1 = Observer::in_earth_orbit(p1, v1); if(!test.check("is_valid(orbit)", o1.is_valid())) n++; if(!test.equals("type(orbit)", o1.type(), NOVAS_OBSERVER_IN_EARTH_ORBIT)) n++; + if(!test.check("operator==()", o1 == o1)) n++; + if(!test.check("operator!()", !(o1 != o1))) n++; + if(!test.check("operator!(moving)", o1 != gc)) n++; if(!test.check("is_geocentric(orbit)", o1.is_geocentric())) n++; if(!test.check("is_geodetic(orbit)", !o1.is_geodetic())) n++; if(!test.check("geocentric_position(orbit)", o1.gcrs_position() == p1)) n++; @@ -174,7 +194,7 @@ int main() { if(!test.check("_novas_observer(orbit)", o != NULL && o->where == NOVAS_OBSERVER_IN_EARTH_ORBIT)) n++; copy = o1.copy(); - if(!test.check("copy(orbit)", memcmp(copy->_novas_observer(), o1._novas_observer(), sizeof(observer)) == 0)) n++; + if(!test.check("copy(orbit)", *copy == o1)) n++; delete copy; test = TestUtil("SolarSystemObserver"); @@ -182,6 +202,9 @@ int main() { SolarSystemObserver ssb = Observer::at_ssb(); if(!test.check("is_valid(ssb)", ssb.is_valid())) n++; if(!test.equals("type(ssb)", ssb.type(), NOVAS_SOLAR_SYSTEM_OBSERVER)) n++; + if(!test.check("operator==()", ssb == ssb)) n++; + if(!test.check("operator!()", !(ssb != ssb))) n++; + if(!test.check("operator!(moving)", ssb != gc)) n++; if(!test.check("is_geocentric(ssb)", !ssb.is_geocentric())) n++; if(!test.check("is_geodetic(ssb)", !ssb.is_geodetic())) n++; if(!test.check("frame_at(reduced)", ssb.frame_at(Time::j2000(), NOVAS_REDUCED_ACCURACY).is_valid())) n++; @@ -213,7 +236,7 @@ int main() { if(!test.check("_novas_observer(ss)", o != NULL && o->where == NOVAS_SOLAR_SYSTEM_OBSERVER)) n++; copy = s1.copy(); - if(!test.check("copy(ss)", memcmp(copy->_novas_observer(), s1._novas_observer(), sizeof(observer)) == 0)) n++; + if(!test.check("copy(ss)", *copy == s1)) n++; delete copy; std::cout << "Observer.cpp: " << (n > 0 ? "FAILED" : "OK") << "\n"; diff --git a/test/cpp/OrbitalTest.cpp b/test/cpp/OrbitalTest.cpp index 33768d3d3..d9dadd56e 100644 --- a/test/cpp/OrbitalTest.cpp +++ b/test/cpp/OrbitalTest.cpp @@ -29,6 +29,8 @@ int main() { if(!test.equals("obliquity()", s.obliquity().deg(), 1.0, 1e-15)) n++; if(!test.equals("ascending_node()", s.ascending_node().deg(), -2.0, 1e-15)) n++; if(!test.equals("system_type()", s.system_type(), NOVAS_ICRS)) n++; + if(!test.check("operator==()", s == s)) n++; + if(!test.check("operator!=()", !(s != s))) n++; if(!test.equals("to_string()", s.to_string(), "Ecliptic OrbitalSystem around SUN inclined at 1.000000 deg with node at -2.000000 deg.")) n++; s.pole(Spherical(-92.0 * Unit::deg, 89.0 * Unit::deg), Equinox::j2000()); @@ -115,6 +117,9 @@ int main() { if(!test.check("node_rate(invalid)", isnan(x.node_rate()))) n++; if(!test.check("position(invalid)", !x.position(Time::j2000()).is_valid())) n++; if(!test.check("velocity(invalid)", !x.velocity(Time::j2000()).is_valid())) n++; + if(!test.check("operator==(invalid)", !(x == x))) n++; + if(!test.check("operator!=(invalid)", x != x)) n++; + s = (OrbitalSystem::equatorial()); @@ -136,6 +141,8 @@ int main() { if(!test.equals("mean_anomaly()", o.reference_mean_anomaly().rad(), -1.0, 1e-15)) n++; if(!test.equals("mean_motion()", o.mean_motion(), Constant::pi / Unit::yr, 1e-14 * o.mean_motion())) n++; if(!test.equals("period()", o.period().years(), 2.0, 1e-15)) n++; + if(!test.check("operator==()", o == o)) n++; + if(!test.check("operator!=()", !(o != o))) n++; if(!test.equals("to_string()", o.to_string(), "Orbital (a = 1.000 AU, T = 2.000 yr, e = 0.000000) in Equatorial OrbitalSystem around SUN inclined at 0.000000 deg with node at 0.000000 deg.")) n++; if(!test.check("position(invalid time)", !x.position(Time::undefined()).is_valid())) n++; if(!test.check("velocity(invalid time)", !x.velocity(Time::undefined()).is_valid())) n++; @@ -305,19 +312,18 @@ int main() { no.node_period = -1.0 / Unit::julian_century; if(!test.check("from_novas_orbit(node period = OK)", Orbital::from_novas_orbit(&no).is_valid())) n++; - novas_orbital mo = {}; novas_make_moon_orbit(Time::b1950().jd(NOVAS_TDB), &mo); Orbital m = Orbital::moon_orbit_at(Time::b1950()); if(!test.check("moon_orbit_at(time invalid)", !Orbital::moon_orbit_at(Time::undefined()).is_valid())) n++; if(!test.check("moon_orbit_at()", m.is_valid())) n++; - if(!test.check("moon_orbit_at() ==", memcmp(m._novas_orbital(), &mo, sizeof(novas_orbital)) == 0)) n++; + if(!test.check("moon_orbit_at() ==", novas_equals_orbital(m._novas_orbital(), &mo))) n++; novas_make_moon_mean_orbit(Time::b1950().jd(NOVAS_TDB), &mo); m = Orbital::moon_mean_orbit_at(Time::b1950()); if(!test.check("moon_mean_orbit_at(time invalid)", !Orbital::moon_mean_orbit_at(Time::undefined()).is_valid())) n++; if(!test.check("moon_mean_orbit_at()", m.is_valid())) n++; - if(!test.check("moon_mean_orbit_at() ==", memcmp(m._novas_orbital(), &mo, sizeof(novas_orbital)) == 0)) n++; + if(!test.check("moon_mean_orbit_at() ==", novas_equals_orbital(m._novas_orbital(), &mo))) n++; std::cout << "Orbital.cpp: " << (n > 0 ? "FAILED" : "OK") << "\n"; diff --git a/test/cpp/PlanetTest.cpp b/test/cpp/PlanetTest.cpp index 46ce25f20..b65af0cc9 100644 --- a/test/cpp/PlanetTest.cpp +++ b/test/cpp/PlanetTest.cpp @@ -39,9 +39,9 @@ int main() { Orbital orb = pl.orbit(Time::b1950()); if((i >= NOVAS_MERCURY && i != NOVAS_EARTH && i <= NOVAS_PLUTO) || i == NOVAS_PLUTO_BARYCENTER) { novas_orbital orb0 = {}; - novas_make_planet_orbit(pl.novas_id(), NOVAS_JD_B1950, &orb0); + novas_make_planet_orbit(pl.novas_id(), Time::b1950().jd(NOVAS_TDB), &orb0); if(!test.check("orbit(" + std::to_string(i) + ")", orb.is_valid())) n++; - if(!test.check("orbit(" + std::to_string(i) + ") == ", memcmp(orb._novas_orbital(), &orb0, sizeof(novas_orbital)))) n++; + if(!test.check("orbit(" + std::to_string(i) + ") == ", novas_equals_orbital(orb._novas_orbital(), &orb0))) n++; } else { if(!test.check("orbit(" + std::to_string(i) + ")", !orb.is_valid())) n++; @@ -52,7 +52,8 @@ int main() { if(!test.equals("for_naif_id(" + std::to_string(i) + ").novas_id()", opt.novas_id(), i)) n++; const Source *p1 = pl.copy(); - if(!test.check("to_string(catalog)", memcmp(p1->_novas_object(), pl._novas_object(), sizeof(object)) == 0)) n++; + if(!test.check("copy()", *p1 == pl)) n++; + delete p1; } char lstr[SIZE_OF_OBJ_NAME + 1]; diff --git a/test/cpp/SourceTest.cpp b/test/cpp/SourceTest.cpp index 4fb9d7eec..b9b968182 100644 --- a/test/cpp/SourceTest.cpp +++ b/test/cpp/SourceTest.cpp @@ -18,7 +18,9 @@ int main() { int n = 0; CatalogEntry xe("Invalid", Equatorial::undefined()); - if(!test.check("invalid", !CatalogSource(xe).is_valid())) n++; + CatalogSource xs = CatalogSource(xe); + + if(!test.check("invalid", !xs.is_valid())) n++; char lname[SIZE_OF_OBJ_NAME + 1] = {'\0'}; memset(lname, 'a', SIZE_OF_OBJ_NAME); @@ -29,12 +31,15 @@ int main() { CatalogSource c(ce); const Source *c1 = c.copy(); - if(!test.check("copy()", memcmp(c1->_novas_object(), c._novas_object(), sizeof(object)) == 0)) n++; + if(!test.check("copy()", *c1 == c)) n++; + delete c1; if(!test.check("is_valid()", c.is_valid())) n++; if(!test.equals("type()", c.type(), NOVAS_CATALOG_OBJECT)) n++; if(!test.equals("name() insensitive", c.name(), "TEST")) n++; - if(!test.check("catalog_entry()", memcmp(c.catalog_entry()._cat_entry(), ce._cat_entry(), sizeof(cat_entry)) == 0)) n++; + if(!test.check("catalog_entry()", c.catalog_entry() == ce)) n++; + if(!test.check("operator==()", c == c)) n++; + if(!test.check("operator!=()", !(c != c))) n++; if(!test.equals("to_string()", c.to_string(), "CatalogSource Test @ 12h 34m 56.789s 12d 34m 56.789s ICRS")) n++; @@ -162,6 +167,9 @@ int main() { Geometric gs = sun.geometric_in(frame).to_icrs(); Geometric pv = sun.barycentric_at(frame.time() - gs.distance().m() / Constant::c, NOVAS_REDUCED_ACCURACY); + if(!test.check("operator==()", sun == sun)) n++; + if(!test.check("operator!=()", !(sun != sun))) n++; + if(!test.check("operator!=(catalog)", sun != c)) n++; if(!test.check("ssb_posvel_at()", pv.is_valid())) n++; if(!test.check("ssb_posvel_at() position", pv.position().equals(gs.position() + frame.observer_ssb_position(), Unit::cm))) { std::cout << "### " << (pv.position() - gs.position() - frame.observer_ssb_position()).to_string(9) << "\n"; @@ -211,9 +219,16 @@ int main() { OrbitalSource os = orb.to_source("test"); if(!test.check("is_valid()", os.is_valid())) n++; - if(!test.check("copy()", memcmp(os.copy()->_novas_object(), os._novas_object(), sizeof(object)) == 0)) n++; - if(!test.check("_novas_orbital()", memcmp(os._novas_orbital(), orb._novas_orbital(), sizeof(novas_orbital)) == 0)) n++; - if(!test.check("orbital()", memcmp(os.orbital()._novas_orbital(), orb._novas_orbital(), sizeof(novas_orbital)) == 0)) n++; + if(!test.check("operator==()", os == os)) n++; + if(!test.check("operator!=()", !(os != os))) n++; + if(!test.check("operator!=(planet)", os != sun)) n++; + + const Source *osc = os.copy(); + if(!test.check("copy()", *osc == os)) n++; + delete osc; + + if(!test.check("_novas_orbital()", novas_equals_orbital(os._novas_orbital(), orb._novas_orbital()))) n++; + if(!test.check("orbital()", os.orbital() == orb)) n++; if(!test.equals("solar_illumination()", os.solar_illumination(frame), novas_solar_illum(os._novas_object(), frame._novas_frame()), 1e-9)) n++; if(!test.equals("solar_power()", os.solar_power(frame.time()), novas_solar_power(frame.jd(NOVAS_TDB), os._novas_object()), 1e-9)) n++; @@ -229,10 +244,17 @@ int main() { EphemerisSource es = EphemerisSource("test", 123456L); if(!test.check("is_valid()", es.is_valid())) n++; - if(!test.check("copy()", memcmp(es.copy()->_novas_object(), es._novas_object(), sizeof(object)) == 0)) n++; + + const Source *esc = es.copy(); + if(!test.check("copy()", *esc == es)) n++; + delete esc; + if(!test.equals("type()", es.type(), NOVAS_EPHEM_OBJECT)) n++; if(!test.equals("name()", es.name(), "test")) n++; if(!test.equals("number()", es.number(), 123456L)) n++; + if(!test.check("operator==()", es == es)) n++; + if(!test.check("operator!=()", !(es != es))) n++; + if(!test.check("operator!=(orbital)", es != os)) n++; if(!test.equals("to_string()", es.to_string(), "EphemerisSource test (nr. 123456)")) n++; std::cout << "Source.cpp: " << (n > 0 ? "FAILED" : "OK") << "\n";