From 61601df9c97676373f91ce2e65d6df11583a92ad Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 30 Oct 2025 09:16:34 -0400 Subject: [PATCH 1/6] Initial commit of time extension --- include/CMakeLists.txt | 1 + include/asdf/core/time.h | 85 ++++++ src/CMakeLists.txt | 1 + src/core/time.c | 484 +++++++++++++++++++++++++++++++++++ tests/fixtures/time.asdf | 22 ++ tests/test-core-extensions.c | 1 + 6 files changed, 594 insertions(+) create mode 100644 include/asdf/core/time.h create mode 100644 src/core/time.c create mode 100644 tests/fixtures/time.asdf diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 18e986b..351290c 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -26,6 +26,7 @@ install( asdf/core/extension_metadata.h asdf/core/software.h asdf/core/ndarray.h + asdf/core/time.h DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/asdf/core" ) diff --git a/include/asdf/core/time.h b/include/asdf/core/time.h new file mode 100644 index 0000000..1828345 --- /dev/null +++ b/include/asdf/core/time.h @@ -0,0 +1,85 @@ +/* Data type an extension for http://stsci.edu/schemas/asdf/core/time-1.0.0 schema */ +#ifndef ASDF_CORE_TIME_H +#define ASDF_CORE_TIME_H + +#include +#include + + +ASDF_BEGIN_DECLS + +#define ASDF_TIME_TIMESTR_MAXLEN 255 + +typedef enum { + ASDF_TIME_FORMAT_ISO_TIME=0, + ASDF_TIME_FORMAT_YDAY, + ASDF_TIME_FORMAT_BYEAR, + ASDF_TIME_FORMAT_JYEAR, + ASDF_TIME_FORMAT_DECIMALYEAR, + ASDF_TIME_FORMAT_JD, + ASDF_TIME_FORMAT_MJD, + ASDF_TIME_FORMAT_GPS, + ASDF_TIME_FORMAT_UNIX, + ASDF_TIME_FORMAT_UTIME, + ASDF_TIME_FORMAT_TAI_SECONDS, + ASDF_TIME_FORMAT_CXCSEC, + ASDF_TIME_FORMAT_GALEXSEC, + ASDF_TIME_FORMAT_UNIX_TAI, + ASDF_TIME_FORMAT_RESERVED1, + // "other" format(s) below + ASDF_TIME_FORMAT_BYEAR_STR, + ASDF_TIME_FORMAT_DATETIME, + ASDF_TIME_FORMAT_FITS, + ASDF_TIME_FORMAT_ISOT, + ASDF_TIME_FORMAT_JYEAR_STR, + ASDF_TIME_FORMAT_PLOT_DATE, + ASDF_TIME_FORMAT_YMDHMS, + ASDF_TIME_FORMAT_datetime64, +} asdf_time_base_format; + + +typedef enum { + ASDF_TIME_SCALE_UTC=0, + ASDF_TIME_SCALE_TAI, + ASDF_TIME_SCALE_TCB, + ASDF_TIME_SCALE_TCG, + ASDF_TIME_SCALE_TDB, + ASDF_TIME_SCALE_TT, + ASDF_TIME_SCALE_UT1, +} asdf_time_scale; + +typedef struct { + double longitude; + double latitude; + double height; +} asdf_time_location_t; + +typedef struct { + bool is_base_format; + asdf_time_base_format type; +} asdf_time_format_t; + +struct asdf_time_info_t { + struct timespec ts; + struct tm tm; +}; + +typedef struct { + char *value; + struct asdf_time_info_t info; + asdf_time_format_t format; + asdf_time_scale scale; + asdf_time_location_t location; +} asdf_time_t; + +ASDF_DECLARE_EXTENSION(time, asdf_time_t); + +ASDF_LOCAL int asdf_time_parse_std(const char *s, const asdf_time_format_t *format, struct asdf_time_info_t *out); +ASDF_LOCAL int asdf_time_parse_byear(const char *s, struct asdf_time_info_t *out); +ASDF_LOCAL int asdf_time_parse_yday(const char *s, struct asdf_time_info_t *out); +ASDF_LOCAL void show_asdf_time_info(const struct asdf_time_info_t *t); + + +ASDF_END_DECLS + +#endif /* ASDF_CORE_TIME_H */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0fc8a8b..5d034af 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(libasdf PUBLIC core/ndarray.c core/ndarray_convert.c core/software.c + core/time.c block.c compression.c context.c diff --git a/src/core/time.c b/src/core/time.c new file mode 100644 index 0000000..7d95628 --- /dev/null +++ b/src/core/time.c @@ -0,0 +1,484 @@ +#include + +#include +#include +#include + +#include "../log.h" +#include "../util.h" +#include "../value.h" +#include "stc/cregex.h" + +#ifdef HAVE_STRPTIME +const char *ASDF_TIME_SFMT_ISO_TIME[] = {"%Y-%m-%d %H:%M:%S", "%Y-%m-%d"}; +const char *ASDF_TIME_SFMT_JD[] = {"%j"}; +const char *ASDF_TIME_SFMT_YDAY[] = {"%Y:%j:%H:%M:%S", "%Y:%j"}; +const char *ASDF_TIME_SFMT_UNIX[] = {"%s"}; + +#define check_format_strptime(TYPE, BUF, TM, HAS_TIME, STATUS) \ + { \ + size_t i = 0; \ + do { \ + (STATUS) = strptime((BUF), (TYPE)[i], (TM)); \ + if ((STATUS)) { \ + (HAS_TIME) = true; \ + break; \ + } \ + } while (i++ && i < sizeof((TYPE)) / sizeof(*(TYPE))); \ + } + +int is_leap_year(int year) { + return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); +} + +int get_days_in_month(int month, int year) { + int days[] = {31,28,31,30,31,30,31,31,30,31,30,31}; + if (month == 2 && is_leap_year(year)) return 29; + return days[month - 1]; +} + +#define JD_B1900 2415020.31352 +#define JD_J2000 2451545.0 +#define JD_MJD 2400000.5 +#define JD_UNIX 2440587.5 + +// Calendar constants +const double JD_GREGORIAN_START = 2299161.0; +const double JD_CORRECTION_REF = 1867216.25; +const double JD_CALENDAR_OFFSET = 122.1; +const int JD_BASE_YEAR = 4716; +const double JD_YEAR_LENGTH = 365.25; +const double JD_EPOCH_BASE = 1524.0; +const double GREGORIAN_OFFSET = (int) JD_EPOCH_BASE; +const double JD_EPOCH_SHIFT = JD_EPOCH_BASE + 0.5; + +const double AVG_MONTH_LENGTH = 30.6001; +const double AVG_YEAR_LENGTH = 365.242198781; +const double DAYS_IN_CENTURY = 36524.2198781; +const int HOURS_PER_DAY = 24; +const int SECONDS_PER_DAY = 86400; +const int SECONDS_PER_HOUR = 3600; +const int SECONDS_PER_MINUTE = 60; + +void show_timespec(const struct timespec *t) { + printf("seconds = %lu\n", t->tv_sec); + printf("nanoseconds = %lu\n", t->tv_nsec); +} + +void show_tm(const struct tm *t) { + printf("year = %d\n", t->tm_year + 1900); + printf("month = %d\n", t->tm_mon + 1); + printf("day = %d\n", t->tm_mday); + printf("hour = %d\n", t->tm_hour); + printf("minute = %d\n", t->tm_min); + printf("second = %d\n", t->tm_sec); + printf("weekday = %d\n", t->tm_wday); + printf("year day = %d\n", t->tm_yday); + printf("dst = %d\n", t->tm_isdst); + printf("gmt offset = %lu\n", t->tm_gmtoff); + printf("timezone: %s\n", t->tm_zone); +} + +void show_asdf_time_info(const struct asdf_time_info_t *t) { + show_tm(&t->tm); + printf("\n"); + show_timespec(&t->ts); + printf("\n"); +} + +double jd_to_unix(const double jd) { + return (jd - JD_UNIX) / SECONDS_PER_DAY; +} + +// Julian Date to Gregorian calendar conversion +void julian_to_tm(const double jd, struct tm *t, time_t *nanoseconds) { + const double jd_shift = jd + 0.5; + const int jd_int = (int)jd_shift; + const double day_fraction = jd_shift - jd_int; + + int jd_adjust; + if (jd_int < JD_GREGORIAN_START) { + jd_adjust = jd_int; + } else { + const int leap_adjust = (int)((jd_int - JD_CORRECTION_REF) / DAYS_IN_CENTURY); + jd_adjust = jd_int + 1 + leap_adjust - leap_adjust / 4; + } + + const int calendar_day = jd_adjust + GREGORIAN_OFFSET; + const int year_base = (int)((calendar_day - JD_CALENDAR_OFFSET) / JD_YEAR_LENGTH); + const int days_in_years = (int)(JD_YEAR_LENGTH * year_base); + const int month_base = (int)((calendar_day - days_in_years) / AVG_MONTH_LENGTH); + + const int day = calendar_day - days_in_years - (int)(AVG_MONTH_LENGTH * month_base) + day_fraction; + const int month = month_base < 14 ? month_base - 1 : month_base - 13; + const int year = month > 2 ? year_base - JD_BASE_YEAR : year_base - JD_BASE_YEAR - 1; + + const double total_seconds = day_fraction * SECONDS_PER_DAY + 0.5; + const int hour = (int)(total_seconds / SECONDS_PER_HOUR); + const int minute = (int)((total_seconds - hour * SECONDS_PER_HOUR) / SECONDS_PER_MINUTE); + const double seconds_whole = total_seconds - hour * SECONDS_PER_HOUR - minute * SECONDS_PER_MINUTE; + const int second = (int)seconds_whole; + const double fractional_seconds = seconds_whole - second; + + t->tm_year = year - 1900; + t->tm_mon = month - 1; + t->tm_mday = day; + t->tm_hour = hour; + t->tm_min = minute; + t->tm_sec = second; + + if (nanoseconds) { + *nanoseconds = (time_t) (fractional_seconds * 1e9) + 0.5; + } +} + +double tm_to_julian(const struct tm *t) { + const int calendar_year = t->tm_year + 1900; + const int calendar_month = t->tm_mon + 1; + const int day_of_month = t->tm_mday; + const int hour = t->tm_hour; + const int minute = t->tm_min; + const int second = t->tm_sec; + + // Adjust year and month for Julian date formula + const int adjusted_year = (calendar_month <= 2) ? calendar_year - 1 : calendar_year; + const int adjusted_month = (calendar_month <= 2) ? calendar_month + 12 : calendar_month; + + // Gregorian calendar correction + const int century = adjusted_year / 100; + const int gregorian_correction = 2 - century + (century / 4); + + // Fractional day from time + const double fractional_day = (hour + minute / (double)SECONDS_PER_MINUTE + second / (double)SECONDS_PER_HOUR) / HOURS_PER_DAY; + + // Julian Date calculation using approximate year/month lengths + const double julian_date = floor(JD_YEAR_LENGTH * (adjusted_year + JD_BASE_YEAR)) + + floor(AVG_MONTH_LENGTH * (adjusted_month + 1)) + + day_of_month + fractional_day + gregorian_correction - JD_EPOCH_SHIFT; + + return julian_date; +} + +double julian_to_mjd(const double jd) { + return jd - JD_MJD; +} + +void mjd_to_tm(const double mjd, struct tm *t, time_t *nsec) { + const double jd = mjd + JD_MJD; + julian_to_tm(jd, t, nsec); +} + +double julian_to_besselian(const double jd) { + return 1900.0 + (jd - JD_B1900) / AVG_YEAR_LENGTH; +} + +double besselian_to_julian(const double b) { + return JD_B1900 + AVG_YEAR_LENGTH * (b - 1900.0); +} + +void besselian_to_tm(const double b, struct tm *t, time_t *nsec) { + const double jd = besselian_to_julian(b); + julian_to_tm(jd, t, nsec); +} + +int asdf_time_parse_std(const char *s, const asdf_time_format_t *format, struct asdf_time_info_t *out) { + if (!s || !out) { + return -1; + } + + struct tm tm = {0}; + char tz_sign = 0; + int tz_hour = 0; + int tz_min = 0; + long nsec = 0; + bool has_time = false; + char *rest = NULL; + char *buf = strdup(s); + + if (!buf) { + return -1; + } + + // Normalize separators (replace 'T' or 't' with space) + for (char *c = buf; *c; ++c) { + if (*c == 'T' || *c == 't') + *c = ' '; + } + + switch (format->type) { + case ASDF_TIME_FORMAT_DATETIME: + case ASDF_TIME_FORMAT_ISO_TIME: + check_format_strptime(ASDF_TIME_SFMT_ISO_TIME, buf, &tm, has_time, rest); + break; + case ASDF_TIME_FORMAT_YDAY: + check_format_strptime(ASDF_TIME_SFMT_YDAY, buf, &tm, has_time, rest); + break; + case ASDF_TIME_FORMAT_UNIX: + check_format_strptime(ASDF_TIME_SFMT_UNIX, buf, &tm, has_time, rest); + break; + default: + return -1; + } + + if (!rest) { + free(buf); + return -1; + } + + // Handle optional fractional seconds + if (has_time) { + const char *dot = strchr(rest, '.'); + if (dot) { + double frac = 0; + sscanf(dot, "%lf", &frac); + nsec = (long)((frac - (int)frac) * 1e9); + } + + // Handle timezone offsets (Z/z = Zulu is ignored, just don't add any offset) + const char *tz = strpbrk(rest, "+-"); + if (tz && (*tz == '+' || *tz == '-')) { + tz_sign = *tz == '-' ? -1 : 1; + if (sscanf(tz + 1, "%2d:%2d", &tz_hour, &tz_min) < 1) + sscanf(tz + 1, "%2d", &tz_hour); + } + } + + // Convert to time_t and adjust for time zone + time_t t = timegm(&tm); + if (t == (time_t)-1) { + free(buf); + return -1; + } + + t -= tz_sign * (tz_hour * SECONDS_PER_HOUR + tz_min * SECONDS_PER_MINUTE); + + out->tm = *gmtime(&t); + out->ts.tv_sec = t; + out->ts.tv_nsec = nsec; + free(buf); + return 0; +} + + +int asdf_time_parse_jd(UNUSED(const char *s), struct asdf_time_info_t *out) { + const double jd = strtod(s, NULL); + struct tm jd_tm; + time_t nsec = 0; + julian_to_tm(jd, &jd_tm, &nsec); + const time_t t = timegm(&jd_tm); + + if (out) { + out->tm = jd_tm; + out->ts.tv_sec = t; + out->ts.tv_nsec = nsec; + } else { + return -1; + } + return 0; +} + +int asdf_time_parse_mjd(UNUSED(const char *s), struct asdf_time_info_t *out) { + const double mjd = julian_to_mjd(strtod(s, NULL)) + JD_MJD; + struct tm mjd_tm; + time_t nsec = 0; + mjd_to_tm(mjd, &mjd_tm, &nsec); + const time_t t = timegm(&mjd_tm); + + if (out) { + out->tm = mjd_tm; + out->ts.tv_sec = t; + out->ts.tv_nsec = nsec; + } else { + return -1; + } + return 0; +} + +int asdf_time_parse_byear(UNUSED(const char *s), struct asdf_time_info_t *out) { + const double byear = strtod(s, NULL); + const double jd = besselian_to_julian(byear); + struct tm tm; + time_t nsec = 0; + + julian_to_tm(jd, &tm, &nsec); + const time_t t = timegm(&tm); + + if (out) { + out->tm = *gmtime(&t); + out->ts.tv_sec = t; + out->ts.tv_nsec = nsec; + } else { + return -1; + } + return 0; +} +#else +#warning "strptime() not available, times will not be parsed" +static int asdf_time_parse_std(UNUSED(const char *s), struct timespec *out) { + if (out) { + out->tv_sec = 0; + out->tv_nsec = 0; + } + return 0; +} +#endif + +static int asdf_time_parse_time(UNUSED(const char *s), const asdf_time_format_t *format, struct asdf_time_info_t *out) { + int status = -1; + switch (format->type) { + case ASDF_TIME_FORMAT_YDAY: + case ASDF_TIME_FORMAT_ISO_TIME: + case ASDF_TIME_FORMAT_DATETIME: + case ASDF_TIME_FORMAT_UNIX: + status = asdf_time_parse_std(s, format, out); + break; + case ASDF_TIME_FORMAT_MJD: + status = asdf_time_parse_mjd(s, out); + break; + case ASDF_TIME_FORMAT_JD: + status = asdf_time_parse_jd(s, out); + break; + case ASDF_TIME_FORMAT_BYEAR: + status = asdf_time_parse_byear(s, out); + break; + default: + break; + } + return status; +} + +static asdf_value_err_t asdf_time_deserialize(asdf_value_t *value, UNUSED(const void *userdata), void **out) { + const char *value_s = NULL; + const char *format_s = NULL; + + asdf_value_t *prop = NULL; + asdf_value_err_t err = ASDF_VALUE_ERR_PARSE_FAILURE; + + asdf_time_t *time = calloc(1, sizeof(asdf_time_t)); + if (!time) { + return ASDF_VALUE_ERR_OOM; + } + + if (asdf_value_is_mapping(value)) { + prop = asdf_mapping_get(value, "value"); + } else { + prop = value; + } + + time->value = calloc(ASDF_TIME_TIMESTR_MAXLEN, sizeof(*time->value)); + if (!time->value) { + return ASDF_VALUE_ERR_OOM; + } + + const asdf_value_type_t type = asdf_value_get_type(prop); + switch (type) { + case ASDF_VALUE_INT64: { + time_t value_tmp = 0; + asdf_value_as_int64(value, &value_tmp); + snprintf(time->value, ASDF_TIME_TIMESTR_MAXLEN, "%ld", value_tmp); + } + break; + case ASDF_VALUE_DOUBLE: { + double value_tmp = 0.0; + asdf_value_as_double(value, &value_tmp); + snprintf(time->value, ASDF_TIME_TIMESTR_MAXLEN, "%lf", value_tmp); + } + break; + case ASDF_VALUE_FLOAT: { + float value_tmp = 0.0f; + asdf_value_as_float(value, &value_tmp); + snprintf(time->value, ASDF_TIME_TIMESTR_MAXLEN, "%f", value_tmp); + } + break; + case ASDF_VALUE_STRING: { + asdf_value_as_string0(prop, &value_s); + strncpy(time->value, value_s, ASDF_TIME_TIMESTR_MAXLEN - 1); + } + break; + default: + fprintf(stderr, "unhandled property conversion from scalar enum %d\n", prop->type); + goto failure; + } + + if (prop != value) { + asdf_value_destroy(prop); + } + + if (asdf_value_is_mapping(value)) { + prop = asdf_mapping_get(value, "format"); + if (ASDF_VALUE_OK != asdf_value_as_string0(prop, &format_s)) { + goto failure; + } + asdf_value_destroy(prop); + } + + const char *time_auto_keys[] = { + "iso_time", + "byear", + "jyear", + "yday", + }; + + const char *time_auto_patterns[] = { + // iso_time + "[0-9]{4}-(0[1-9])|(1[0-2])-(0[1-9])|([1-2][0-9])|(3[0-1])[T ]([0-1][0-9])|(2[0-4]):[0-5][0-9]:[0-5][0-9](.[0-9]+)?", + // byear + "B[0-9]+(.[0-9]+)?", + // jyear + "J[0-9]+(.[0-9]+)?", + // yday + "[0-9]{4}:(00[1-9])|(0[1-9][0-9])|([1-2][0-9][0-9])|(3[0-5][0-9])|(36[0-5]):([0-1][0-9])|([0-1][0-9])|(2[0-4]):[0-5][0-9]:[0-5][0-9](.[0-9]+)?" + }; + + time->format.is_base_format = true; + for (size_t i = 0; i < sizeof(time_auto_patterns) / sizeof(time_auto_patterns[0]); i++) { + cregex re = cregex_make(time_auto_patterns[i], CREG_DEFAULT); + if (cregex_is_match(&re, time->value) == true) { + const char *fmt_have = format_s ? format_s : time_auto_keys[i]; + if (!strcmp(fmt_have, "iso_time")) { + time->format.type = ASDF_TIME_FORMAT_ISO_TIME; + } else if (!strcmp(fmt_have, "byear") || !strncmp(time->value, "B", 1)) { + time->format.type = ASDF_TIME_FORMAT_BYEAR; + } else if (!strcmp(fmt_have, "jd")) { + time->format.type = ASDF_TIME_FORMAT_JD; + } else if (!strcmp(fmt_have, "mjd")) { + time->format.type = ASDF_TIME_FORMAT_MJD; + } else if (!strcmp(fmt_have, "jyear") || !strncmp(time->value, "J", 1)) { + time->format.type = ASDF_TIME_FORMAT_JYEAR; + } else if (!strcmp(fmt_have, "yday")) { + time->format.type = ASDF_TIME_FORMAT_YDAY; + } else if (!strcmp(fmt_have, "unix")) { + time->format.type = ASDF_TIME_FORMAT_UNIX; + } + time->scale = ASDF_TIME_SCALE_UTC; + cregex_drop(&re); + break; + } + cregex_drop(&re); + } + + if (time->format.type > ASDF_TIME_FORMAT_RESERVED1) { + time->format.is_base_format = false; + } + + asdf_time_parse_time(time->value, &time->format, &time->info); + + *out = time; + + return ASDF_VALUE_OK; +failure: + free(time->value); + asdf_value_destroy(prop); + return err; +} + + +static void asdf_time_dealloc(void *value) { + asdf_time_t *t = (asdf_time_t *)value; + free(t->value); + free(t); +} + + +ASDF_REGISTER_EXTENSION(time, ASDF_STANDARD_TAG_PREFIX "time/time-1.4.0", asdf_time_t, &libasdf_software, + asdf_time_deserialize, asdf_time_dealloc, NULL); diff --git a/tests/fixtures/time.asdf b/tests/fixtures/time.asdf new file mode 100644 index 0000000..9c2f225 --- /dev/null +++ b/tests/fixtures/time.asdf @@ -0,0 +1,22 @@ +#ASDF 1.0.0 +#ASDF_STANDARD 1.6.0 +%YAML 1.1 +%TAG ! tag:stsci.edu:asdf/ +--- !core/asdf-1.1.0 +asdf_library: !core/software-1.0.0 {author: The ASDF Developers, homepage: 'http://github.com/asdf-format/asdf', + name: asdf, version: 5.0.0} +history: + extensions: + - !core/extension_metadata-1.0.0 + extension_class: asdf.extension._manifest.ManifestExtension + extension_uri: asdf://asdf-format.org/core/extensions/core-1.6.0 + manifest_software: !core/software-1.0.0 {name: asdf_standard, version: 1.4.0} + software: !core/software-1.0.0 {name: asdf, version: 5.0.0} +t_byear: !time/time-1.4.0 {format: byear, value: '2025.78707178'} +t_datetime: !time/time-1.4.0 {format: datetime, value: '2025-10-14 13:26:41.0000+00:00'} +t_iso_time: !time/time-1.4.0 {format: iso_time, value: '2025-10-14T13:26:41.0000'} +t_jd: !time/time-1.4.0 {format: jd, value: '2460963.060197'} +t_mjd: !time/time-1.4.0 {format: mjd, value: '60962.560196'} +t_unix: !time/time-1.4.0 {format: unix, value: '1760462801.00'} +t_yday: !time/time-1.4.0 {format: yday, value: '2025:287:13:26:41.0000'} +... diff --git a/tests/test-core-extensions.c b/tests/test-core-extensions.c index 7ceb316..c470dcc 100644 --- a/tests/test-core-extensions.c +++ b/tests/test-core-extensions.c @@ -6,6 +6,7 @@ #include #include +#include " #include #include #include From 11ef8a474b8522ab300b5cc657a5e27fdd1e3668 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 30 Oct 2025 18:53:41 -0400 Subject: [PATCH 2/6] Fix bad include * Use asdf_time_info_t structure member --- tests/test-core-extensions.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test-core-extensions.c b/tests/test-core-extensions.c index c470dcc..c783f74 100644 --- a/tests/test-core-extensions.c +++ b/tests/test-core-extensions.c @@ -6,7 +6,7 @@ #include #include -#include " +#include #include #include #include @@ -80,8 +80,8 @@ MU_TEST(test_asdf_history_entry) { assert_not_null(entry); assert_string_equal(entry->description, "test file containing integers from 0 to 255 in the " "block data, for simple tests against known data"); - assert_int(entry->time.tv_sec, ==, 1753271775); - assert_int(entry->time.tv_nsec, ==, 0); + assert_int(entry->time->info.ts.tv_sec, ==, 1753271775); + assert_int(entry->time->info.ts.tv_nsec, ==, 0); // TODO: Oops, current test case does not include software used to write the history entry // A little strange that Python asdf excludes it...maybe no one cares because it's the only // code writing asdf history entries? Need more test cases... @@ -185,7 +185,6 @@ MU_TEST(test_asdf_software) { return MUNIT_OK; } - MU_TEST_SUITE( test_asdf_core_extensions, MU_RUN_TEST(test_asdf_extension_metadata), From a7b57c52742f986ffd2bc13a89955ddcf6165dd4 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 30 Oct 2025 18:54:32 -0400 Subject: [PATCH 3/6] Print time info when NDEBUG is not defined --- src/core/time.c | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/src/core/time.c b/src/core/time.c index 7d95628..ce0b7c8 100644 --- a/src/core/time.c +++ b/src/core/time.c @@ -61,29 +61,34 @@ const int SECONDS_PER_HOUR = 3600; const int SECONDS_PER_MINUTE = 60; void show_timespec(const struct timespec *t) { - printf("seconds = %lu\n", t->tv_sec); - printf("nanoseconds = %lu\n", t->tv_nsec); + fprintf(stderr, "seconds = %lu\n", t->tv_sec); + fprintf(stderr, "nanoseconds = %lu\n", t->tv_nsec); } + void show_tm(const struct tm *t) { - printf("year = %d\n", t->tm_year + 1900); - printf("month = %d\n", t->tm_mon + 1); - printf("day = %d\n", t->tm_mday); - printf("hour = %d\n", t->tm_hour); - printf("minute = %d\n", t->tm_min); - printf("second = %d\n", t->tm_sec); - printf("weekday = %d\n", t->tm_wday); - printf("year day = %d\n", t->tm_yday); - printf("dst = %d\n", t->tm_isdst); - printf("gmt offset = %lu\n", t->tm_gmtoff); - printf("timezone: %s\n", t->tm_zone); + fprintf(stderr, "year = %d\n", t->tm_year + 1900); + fprintf(stderr, "month = %d\n", t->tm_mon + 1); + fprintf(stderr, "day = %d\n", t->tm_mday); + fprintf(stderr, "hour = %d\n", t->tm_hour); + fprintf(stderr, "minute = %d\n", t->tm_min); + fprintf(stderr, "second = %d\n", t->tm_sec); + fprintf(stderr, "weekday = %d\n", t->tm_wday); + fprintf(stderr, "year day = %d\n", t->tm_yday); + fprintf(stderr, "dst = %d\n", t->tm_isdst); + fprintf(stderr, "gmt offset = %lu\n", t->tm_gmtoff); + fprintf(stderr, "timezone: %s\n", t->tm_zone); } void show_asdf_time_info(const struct asdf_time_info_t *t) { + #if !defined(NDEBUG) show_tm(&t->tm); printf("\n"); show_timespec(&t->ts); printf("\n"); + #else + (void *) t; + #endif } double jd_to_unix(const double jd) { From 89d45fa41a2e82344993afd3b2b397a5cb773a59 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Thu, 30 Oct 2025 18:54:52 -0400 Subject: [PATCH 4/6] Use asdf_time_t --- include/asdf/core/history_entry.h | 4 +- src/core/history_entry.c | 129 +++++++----------------------- 2 files changed, 31 insertions(+), 102 deletions(-) diff --git a/include/asdf/core/history_entry.h b/include/asdf/core/history_entry.h index f312506..2c06574 100644 --- a/include/asdf/core/history_entry.h +++ b/include/asdf/core/history_entry.h @@ -7,12 +7,14 @@ #include #include +#include "time.h" + ASDF_BEGIN_DECLS typedef struct { const char *description; - struct timespec time; + const asdf_time_t *time; const asdf_software_t **software; } asdf_history_entry_t; diff --git a/src/core/history_entry.c b/src/core/history_entry.c index b106e71..38d3311 100644 --- a/src/core/history_entry.c +++ b/src/core/history_entry.c @@ -15,91 +15,7 @@ #include "../log.h" #include "../util.h" #include "../value.h" - - -/* - * Parse a YAML-serialized timestamp - * - * Generally in ISO8601 but can be "relaxed" having a space between the date and the time (the - * Python asdf actually appears to output in this format though maybe it depends on the Python - * yaml version--we should specify this more strictly maybe... - */ -#ifdef HAVE_STRPTIME -static int asdf_parse_datetime(const char *s, struct timespec *out) { - if (!s || !out) - return -1; - - struct tm tm = {0}; - char tz_sign = 0; - int tz_hour = 0; - int tz_min = 0; - long nsec = 0; - bool has_time = false; - char *rest = NULL; - char *buf = strdup(s); - - if (!buf) - return -1; - - // Normalize separators (replace 'T' or 't' with space) - for (char *c = buf; *c; ++c) - if (*c == 'T' || *c == 't') - *c = ' '; - - // Try to parse date and time (without optional fractional seconds and timezone) - rest = strptime(buf, "%Y-%m-%d %H:%M:%S", &tm); - - if (!rest) - rest = strptime(buf, "%Y-%m-%d", &tm); - else - has_time = true; - - if (!rest) { - free(buf); - return -1; - } - - // Handle optional fractional seconds - if (has_time) { - const char *dot = strchr(rest, '.'); - if (dot) { - double frac = 0; - sscanf(dot, "%lf", &frac); - nsec = (long)((frac - (int)frac) * 1e9); - } - - // Handle timezone offsets (Z/z = Zulu is ignored, just don't add any offset) - const char *tz = strpbrk(rest, "+-"); - if (tz && (*tz == '+' || *tz == '-')) { - tz_sign = (*tz == '-') ? -1 : 1; - if (sscanf(tz + 1, "%2d:%2d", &tz_hour, &tz_min) < 1) - sscanf(tz + 1, "%2d", &tz_hour); - } - } - - // Convert to time_t and adjust for time zone - time_t t = timegm(&tm); - if (t == (time_t)-1) { - free(buf); - return -1; - } - - t -= tz_sign * (tz_hour * 3600 + tz_min * 60); - out->tv_sec = t; - out->tv_nsec = nsec; - free(buf); - return 0; -} -#else -#warning "strptime() not available, times will not be parsed" -static int asdf_parse_datetime(UNUSED(const char *s), struct timespec *out) { - if (out) { - out->tv_sec = 0; - out->tv_nsec = 0; - } - return 0; -} -#endif +#include "asdf/core/time.h" static asdf_software_t **asdf_history_entry_deserialize_software(asdf_value_t *value) { @@ -146,14 +62,13 @@ static asdf_software_t **asdf_history_entry_deserialize_software(asdf_value_t *v return software; } - static asdf_value_err_t asdf_history_entry_deserialize( asdf_value_t *value, UNUSED(const void *userdata), void **out) { asdf_value_err_t err = ASDF_VALUE_ERR_PARSE_FAILURE; asdf_value_t *prop = NULL; const char *description = NULL; const char *time_str = NULL; - struct timespec time = {0}; + asdf_time_t *time = NULL; asdf_software_t **software = NULL; if (!asdf_value_is_mapping(value)) @@ -170,26 +85,31 @@ static asdf_value_err_t asdf_history_entry_deserialize( asdf_value_destroy(prop); prop = asdf_mapping_get(value, "time"); - if (prop) { - bool valid_time = false; - if (ASDF_VALUE_OK == asdf_value_as_string0(prop, &time_str)) { - if (0 == asdf_parse_datetime(time_str, &time)) + + // cast the value of "time" to an asdf_time_t + const asdf_extension_t *time_ext = asdf_extension_get(value->file, "tag:stsci.edu:asdf/time/time-1.4.0"); + if (time_ext) { + bool valid_time = false; + time_ext->deserialize(prop, NULL, (void *) &time); + + if (time) { valid_time = true; - } + } -#ifdef ASDF_LOG_ENABLED - if (!valid_time) { - if (ASDF_VALUE_OK != asdf_value_as_scalar0(prop, &time_str)) { - time_str = ""; + #ifdef ASDF_LOG_ENABLED + if (!valid_time) { + if (ASDF_VALUE_OK != asdf_value_as_scalar0(prop, &time_str)) { + time_str = ""; + } + ASDF_LOG( + value->file, ASDF_LOG_WARN, "ignoring invalid time %s in history_entry", time_str); } - ASDF_LOG( - value->file, ASDF_LOG_WARN, "ignoring invalid time %s in history_entry", time_str); + #endif } -#endif + asdf_value_destroy(prop); } - asdf_value_destroy(prop); /* Software can be either an array of software or a single entry, but here it is always * returned as a NULL-terminated array of asdf_software_t * @@ -208,9 +128,12 @@ static asdf_value_err_t asdf_history_entry_deserialize( return ASDF_VALUE_ERR_OOM; entry->description = description; - entry->time = time; entry->software = (const asdf_software_t **)software; + if (time) { + entry->time = time; + } *out = entry; + return ASDF_VALUE_OK; failure: asdf_value_destroy(prop); @@ -224,6 +147,10 @@ static void asdf_history_entry_dealloc(void *value) { asdf_history_entry_t *entry = value; + if (entry->time) { + asdf_time_destroy((asdf_time_t *) entry->time); + } + if (entry->software) { for (asdf_software_t **sp = (asdf_software_t **)entry->software; *sp; ++sp) { asdf_software_destroy(*sp); From 01b17c0cc2bc82e45aae3b60a0874ad71a74aae5 Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Fri, 31 Oct 2025 16:56:48 -0400 Subject: [PATCH 5/6] Initial commit of time extension test --- tests/test-time.c | 81 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/test-time.c diff --git a/tests/test-time.c b/tests/test-time.c new file mode 100644 index 0000000..700e1e5 --- /dev/null +++ b/tests/test-time.c @@ -0,0 +1,81 @@ +#include +#include +#include + +#include "munit.h" +#include "util.h" + +#include +#include +#include "asdf/core/time.h" +#include +#include + + +MU_TEST(test_asdf_time) { + const char *path = get_fixture_file_path("time.asdf"); + assert_not_null(path); + + asdf_file_t *file = asdf_open_file(path, "r"); + assert_not_null(file); + + asdf_value_t *value = NULL; + + // buffer for time string + char time_str[255] = {0}; + const int format_type[] = { + ASDF_TIME_FORMAT_ISO_TIME, + ASDF_TIME_FORMAT_DATETIME, + ASDF_TIME_FORMAT_YDAY, + ASDF_TIME_FORMAT_UNIX, + ASDF_TIME_FORMAT_JD, + ASDF_TIME_FORMAT_MJD, + ASDF_TIME_FORMAT_BYEAR, + }; + + asdf_time_t *t = NULL; + for (size_t i = 0; i < sizeof(format_type) / sizeof(format_type[0]); i++) { + const char *fixture_key[] = { + "t_iso_time", + "t_datetime", + "t_yday", + "t_unix", + "t_jd", + "t_mjd", + "t_byear", + }; + + const char *key = fixture_key[i]; + assert_true(asdf_is_time(file, key)); + + value = asdf_get_value(file, key); + if (asdf_value_as_time(value, &t) != ASDF_VALUE_OK) { + fprintf(stderr, "asdf_value_as_time failed: %s\n", key); + asdf_time_destroy(t); + return 1; + }; + assert_true(t != NULL); + assert_true(t->value != NULL); + time_t x = t->info.ts.tv_sec; + strftime(time_str, sizeof(time_str), "%m/%d/%Y %T %Z", gmtime(&x)); + printf("[%zu] key: %10s, value: %30s, time: %10s\n", i, key, t->value, time_str); + show_asdf_time_info(&t->info); + + asdf_time_destroy(t); + t = NULL; + memset(time_str, 0, sizeof(time_str)); + asdf_value_destroy(value); + } + + asdf_close(file); + + return MUNIT_OK; +} + +MU_TEST_SUITE( + test_asdf_time_extension, + MU_RUN_TEST(test_asdf_time) +); + + +MU_RUN_SUITE(test_asdf_time_extension); \ No newline at end of file From a67e90b92a27c0b16b8069dad01270d541b2e14b Mon Sep 17 00:00:00 2001 From: Joseph Hunkeler Date: Mon, 1 Dec 2025 13:17:49 -0500 Subject: [PATCH 6/6] Add time extension to automake --- Makefile.am | 1 + tests/Makefile.am | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/Makefile.am b/Makefile.am index 7b0222d..d02b8fa 100644 --- a/Makefile.am +++ b/Makefile.am @@ -8,6 +8,7 @@ src_files = \ src/core/ndarray.c \ src/core/ndarray_convert.c \ src/core/software.c \ + src/core/time.c \ src/error.c \ src/event.c \ src/extension_registry.c \ diff --git a/tests/Makefile.am b/tests/Makefile.am index 6303dc2..49329c8 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -32,6 +32,7 @@ check_PROGRAMS = \ test-parse.unit \ test-parse-util.unit \ test-stream.unit \ + test-time.unit \ test-value.unit \ test-value-util.unit @@ -129,6 +130,13 @@ test_stream_unit_CFLAGS = $(unit_test_cflags) test_stream_unit_LDFLAGS = $(unit_test_ldflags) test_stream_unit_LDADD = $(FYAML_LIBS) libmunit.a $(STATGRAB_LIBS) +# test-time.unit +test_time_unit_SOURCES = test-time.c +test_time_unit_CPPFLAGS = $(unit_test_cppflags) +test_time_unit_CFLAGS = $(unit_test_cflags) +test_time_unit_LDFLAGS = $(unit_test_ldflags) +test_time_unit_LDADD = $(unit_test_ldadd) + # test-value.unit test_value_unit_SOURCES = test-value.c test_value_unit_CPPFLAGS = $(unit_test_cppflags)