From 16b68b6fbaadf1c714e1b4d6a0501c040666d0f6 Mon Sep 17 00:00:00 2001 From: Max Wash Date: Mon, 22 Sep 2025 10:44:56 +0100 Subject: [PATCH] object: add a type for storing, parsing, and stringifying date/time values --- object/datetime.c | 489 ++++++++++++++++++++++++++ object/datetime.h | 17 + object/include/blue/object/datetime.h | 49 +++ object/include/blue/object/type.h | 1 + 4 files changed, 556 insertions(+) create mode 100644 object/datetime.c create mode 100644 object/datetime.h create mode 100644 object/include/blue/object/datetime.h diff --git a/object/datetime.c b/object/datetime.c new file mode 100644 index 0000000..4e6e6eb --- /dev/null +++ b/object/datetime.c @@ -0,0 +1,489 @@ +#include "datetime.h" + +#include "blue/core/stream.h" +#include "blue/object/string.h" + +#include + +static void datetime_to_string(const struct b_object *obj, struct b_stream *out); + +static struct b_object_type string_type = { + .t_name = "corelib::string", + .t_flags = B_OBJECT_FUNDAMENTAL, + .t_id = B_OBJECT_TYPE_DATETIME, + .t_instance_size = sizeof(struct b_datetime), + .t_to_string = datetime_to_string, +}; + +struct b_datetime *b_datetime_create(void) +{ + return (struct b_datetime *)b_object_type_instantiate(&string_type); +} + +static bool is_leap_year(const struct b_datetime *dt) +{ + if ((dt->dt_year % 400) == 0) { + return true; + } + + if ((dt->dt_year % 4) == 0 && (dt->dt_year % 100) != 0) { + return true; + } + + return false; +} + +static bool is_year_valid(const struct b_datetime *dt) +{ + return dt->dt_year >= 0; +} + +static bool is_month_valid(const struct b_datetime *dt) +{ + return dt->dt_month >= 1 && dt->dt_month <= 12; +} + +static bool is_day_valid(const struct b_datetime *dt) +{ + if (dt->dt_day < 1) { + return false; + } + + switch (dt->dt_month) { + case 2: + return dt->dt_day <= (is_leap_year(dt) ? 29 : 28); + case 4: + case 6: + case 9: + case 11: + return dt->dt_day <= 30; + case 1: + case 3: + case 5: + case 7: + case 8: + case 10: + case 12: + return dt->dt_day <= 31; + default: + return false; + } +} + +static bool is_time_valid(const struct b_datetime *dt) +{ + if (!(dt->dt_hour >= 0 && dt->dt_hour <= 23)) { + return false; + } + + if (!(dt->dt_min >= 0 && dt->dt_min <= 59)) { + return false; + } + + if (!(dt->dt_sec >= 0 && dt->dt_sec <= 60)) { + return false; + } + + return true; +} + +static bool is_zone_valid(const struct b_datetime *dt) +{ + if (!(dt->dt_zone_offset_hour >= 0 && dt->dt_zone_offset_hour <= 23)) { + return false; + } + + if (!(dt->dt_zone_offset_minute >= 0 && dt->dt_zone_offset_minute <= 59)) { + return false; + } + + return true; +} + +static bool validate(const struct b_datetime *dt) +{ + if (dt->dt_has_date) { + if (!is_year_valid(dt)) { + return false; + } + + if (!is_month_valid(dt)) { + return false; + } + + if (!is_day_valid(dt)) { + return false; + } + } + + if (dt->dt_has_time) { + if (!is_time_valid(dt)) { + return false; + } + + if (!is_zone_valid(dt)) { + return false; + } + } + + return true; +} + +struct b_datetime *parse_rfc3339(const char *s) +{ + struct b_datetime *dt = b_datetime_create(); + if (!dt) { + return NULL; + } + + size_t len = strlen(s); + + size_t i = 0, c = 0; + + bool has_date = false, has_time = false; + dt->dt_localtime = true; + + if (len >= 10 && s[4] == '-' && s[7] == '-') { + has_date = true; + } + + if (len >= 8 && s[2] == ':' && s[5] == ':') { + has_time = true; + } + + if (len >= 19 && s[4] == '-' && s[7] == '-' + && (s[10] == 'T' || s[10] == 't' || s[10] == ' ') && s[13] == ':' + && s[16] == ':') { + has_date = true; + has_time = true; + } + + if (!has_date && !has_time) { + goto fail; + } + + if (has_date) { + for (c = 0; c < 4; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_year *= 10; + dt->dt_year += (s[i] - '0'); + } + + if (s[i++] != '-') { + goto fail; + } + + for (c = 0; c < 2; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_month *= 10; + dt->dt_month += (s[i] - '0'); + } + + if (s[i++] != '-' || dt->dt_month > 12) { + goto fail; + } + + for (c = 0; c < 2; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_day *= 10; + dt->dt_day += (s[i] - '0'); + } + + if (dt->dt_day > 31) { + goto fail; + } + } + + if ((s[i] == 'T' || s[i] == 't' || s[i] == ' ') && !has_time) { + goto fail; + } + + if (has_date && has_time) { + if (s[i] != 'T' && s[i] != 't' && s[i] != ' ') { + goto fail; + } + + i++; + } + + if (has_time) { + for (c = 0; c < 2; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_hour *= 10; + dt->dt_hour += (s[i] - '0'); + } + + if (s[i++] != ':') { + goto fail; + } + + for (c = 0; c < 2; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_min *= 10; + dt->dt_min += (s[i] - '0'); + } + + if (s[i++] != ':') { + goto fail; + } + + for (c = 0; c < 2; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_sec *= 10; + dt->dt_sec += (s[i] - '0'); + } + + if (s[i] == '.') { + i++; + for (c = 0; s[i]; c++, i++) { + if (!isdigit(s[i])) { + break; + } + + dt->dt_msec *= 10; + dt->dt_msec += (s[i] - '0'); + } + + if (c == 0) { + goto fail; + } + } + + if (s[i] == '+' || s[i] == '-') { + dt->dt_localtime = false; + dt->dt_zone_offset_negative = s[i] == '-'; + i++; + + for (c = 0; c < 2; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_zone_offset_hour *= 10; + dt->dt_zone_offset_hour += (s[i] - '0'); + } + + if (s[i++] != ':') { + goto fail; + } + + for (c = 0; c < 2; c++, i++) { + if (!isdigit(s[i])) { + goto fail; + } + + dt->dt_zone_offset_minute *= 10; + dt->dt_zone_offset_minute += (s[i] - '0'); + } + } else if (s[i] == 'Z' || s[i] == 'z') { + dt->dt_localtime = false; + i++; + } + } + + if (s[i] != 0) { + goto fail; + } + + dt->dt_has_date = has_date; + dt->dt_has_time = has_time; + return dt; +fail: + b_datetime_release(dt); + return NULL; +} + +struct b_datetime *b_datetime_parse(enum b_datetime_format format, const char *s) +{ + struct b_datetime *dt = NULL; + + switch (format) { + case B_DATETIME_FORMAT_RFC3339: + dt = parse_rfc3339(s); + break; + default: + return NULL; + } + + if (!dt) { + return NULL; + } + + if (!validate(dt)) { + b_datetime_release(dt); + return NULL; + } + + return dt; +} + +enum b_status encode_rfc3339(const struct b_datetime *dt, struct b_stream *out) +{ + if (dt->dt_has_date) { + b_stream_write_fmt( + out, NULL, "%04ld-%02ld-%02ld", dt->dt_year, + dt->dt_month, dt->dt_day); + } + + if (dt->dt_has_date && dt->dt_has_time) { + b_stream_write_char(out, 'T'); + } + + if (dt->dt_has_time) { + b_stream_write_fmt( + out, NULL, "%02ld:%02ld:%02ld", dt->dt_hour, dt->dt_min, + dt->dt_sec); + + if (dt->dt_msec > 0) { + b_stream_write_fmt(out, NULL, ".%04ld", dt->dt_msec); + } + + if (!dt->dt_localtime) { + if (dt->dt_zone_offset_hour == 0 + && dt->dt_zone_offset_minute == 0) { + b_stream_write_char(out, 'Z'); + } else { + b_stream_write_fmt( + out, NULL, "%c%02ld:%02ld", + dt->dt_zone_offset_negative ? '-' : '+', + dt->dt_zone_offset_hour, + dt->dt_zone_offset_minute); + } + } + } + + return B_SUCCESS; +} + +void b_datetime_to_string( + const b_datetime *dt, b_datetime_format format, struct b_string *dest) +{ + struct b_stream *out; + b_string_open_stream(dest, &out); + + switch (format) { + case B_DATETIME_FORMAT_RFC3339: + encode_rfc3339(dt, out); + break; + default: + break; + } + + b_stream_close(out); +} + +bool b_datetime_is_localtime(const b_datetime *dt) +{ + return dt->dt_localtime; +} + +bool b_datetime_has_date(const b_datetime *dt) +{ + return dt->dt_has_date; +} + +bool b_datetime_has_time(const b_datetime *dt) +{ + return dt->dt_has_time; +} + +long b_datetime_year(const b_datetime *dt) +{ + return dt->dt_year; +} + +long b_datetime_month(const b_datetime *dt) +{ + return dt->dt_month; +} + +long b_datetime_day(const b_datetime *dt) +{ + return dt->dt_day; +} + +long b_datetime_hour(const b_datetime *dt) +{ + return dt->dt_hour; +} + +long b_datetime_minute(const b_datetime *dt) +{ + return dt->dt_min; +} + +long b_datetime_second(const b_datetime *dt) +{ + return dt->dt_sec; +} + +long b_datetime_subsecond(const b_datetime *dt) +{ + return dt->dt_msec; +} + +bool b_datetime_zone_offset_is_negative(const b_datetime *dt) +{ + return dt->dt_zone_offset_negative; +} + +long b_datetime_zone_offset_hour(const b_datetime *dt) +{ + return dt->dt_zone_offset_hour; +} + +long b_datetime_zone_offset_minute(const b_datetime *dt) +{ + return dt->dt_zone_offset_minute; +} + +static void datetime_to_string(const struct b_object *obj, struct b_stream *out) +{ + struct b_datetime *dt = B_DATETIME(obj); + + if (dt->dt_has_date) { + b_stream_write_fmt( + out, NULL, "%04ld-%02ld-%02ld", dt->dt_year, + dt->dt_month, dt->dt_day); + } + + if (dt->dt_has_date && dt->dt_has_time) { + b_stream_write_char(out, ' '); + } + + if (dt->dt_has_time) { + b_stream_write_fmt( + out, NULL, "%02ld:%02ld:%02ld", dt->dt_hour, dt->dt_min, + dt->dt_sec); + + if (dt->dt_msec > 0) { + b_stream_write_fmt(out, NULL, ".%04ld", dt->dt_msec); + } + + if (!dt->dt_localtime) { + b_stream_write_fmt( + out, NULL, " %c%02ld:%02ld", + dt->dt_zone_offset_negative ? '-' : '+', + dt->dt_zone_offset_hour, + dt->dt_zone_offset_minute); + } + } +} diff --git a/object/datetime.h b/object/datetime.h new file mode 100644 index 0000000..ff391f8 --- /dev/null +++ b/object/datetime.h @@ -0,0 +1,17 @@ +#ifndef _BLUELIB_DATETIME_H_ +#define _BLUELIB_DATETIME_H_ + +#include "object.h" + +struct b_datetime { + struct b_object dt_base; + unsigned int dt_year, dt_month, dt_day; + unsigned short dt_hour, dt_min, dt_sec; + unsigned int dt_msec; + + bool dt_has_date, dt_has_time, dt_localtime; + unsigned short dt_zone_offset_hour, dt_zone_offset_minute; + bool dt_zone_offset_negative; +}; + +#endif diff --git a/object/include/blue/object/datetime.h b/object/include/blue/object/datetime.h new file mode 100644 index 0000000..0f07fbd --- /dev/null +++ b/object/include/blue/object/datetime.h @@ -0,0 +1,49 @@ +#ifndef BLUELIB_DATETIME_H_ +#define BLUELIB_DATETIME_H_ + +#include +#include +#include +#include + +struct b_string; + +#define B_DATETIME(p) ((b_datetime *)(p)) + +typedef struct b_datetime b_datetime; + +typedef enum b_datetime_format { + B_DATETIME_FORMAT_RFC3339 = 1, +} b_datetime_format; + +BLUE_API b_datetime *b_datetime_create(void); +BLUE_API b_datetime *b_datetime_parse(b_datetime_format format, const char *s); +BLUE_API void b_datetime_to_string( + const b_datetime *dt, b_datetime_format format, struct b_string *dest); + +static inline b_datetime *b_datetime_retain(b_datetime *dt) +{ + return B_DATETIME(b_retain(B_OBJECT(dt))); +} +static inline void b_datetime_release(b_datetime *dt) +{ + b_release(B_OBJECT(dt)); +} + +BLUE_API bool b_datetime_is_localtime(const b_datetime *dt); +BLUE_API bool b_datetime_has_date(const b_datetime *dt); +BLUE_API bool b_datetime_has_time(const b_datetime *dt); + +BLUE_API long b_datetime_year(const b_datetime *dt); +BLUE_API long b_datetime_month(const b_datetime *dt); +BLUE_API long b_datetime_day(const b_datetime *dt); +BLUE_API long b_datetime_hour(const b_datetime *dt); +BLUE_API long b_datetime_minute(const b_datetime *dt); +BLUE_API long b_datetime_second(const b_datetime *dt); +BLUE_API long b_datetime_subsecond(const b_datetime *dt); + +BLUE_API bool b_datetime_zone_offset_is_negative(const b_datetime *dt); +BLUE_API long b_datetime_zone_offset_hour(const b_datetime *dt); +BLUE_API long b_datetime_zone_offset_minute(const b_datetime *dt); + +#endif diff --git a/object/include/blue/object/type.h b/object/include/blue/object/type.h index b45c200..2888813 100644 --- a/object/include/blue/object/type.h +++ b/object/include/blue/object/type.h @@ -30,6 +30,7 @@ typedef enum b_fundamental_type_id { B_OBJECT_TYPE_PATH, B_OBJECT_TYPE_FILE, B_OBJECT_TYPE_DIRECTORY, + B_OBJECT_TYPE_DATETIME, } b_fundamental_type_id; typedef enum b_object_type_flags {