diff --git a/lib/date.c b/lib/date.c new file mode 100644 index 0000000..43b80a0 --- /dev/null +++ b/lib/date.c @@ -0,0 +1,287 @@ +#include "date.h" + +struct date *date_copy(struct date *date_to_copy) { + struct date *date = malloc(sizeof(struct date)); + if (date == NULL) { + perror("Error (date_copy)"); + exit(EXIT_FAILURE); + } + date->year = date_to_copy->year; + date->month = date_to_copy->month; + date->day = date_to_copy->day; + date->hours = date_to_copy->hours; + date->minutes = date_to_copy->minutes; + date->seconds = date_to_copy->seconds; + date->milliseconds = date_to_copy->milliseconds; + date->timezone_utc_offset = date_to_copy->timezone_utc_offset; + return date; +} + +bool date_get_is_valid_year(uint16_t year) { + return year <= 9999; +} + +bool date_get_is_valid_month(uint8_t month) { + return month >= 1 && month <= 12; +} + +bool date_get_is_valid_day(uint8_t day) { + return day >= 1 && day <= 31; +} + +bool date_get_is_valid_hours(uint8_t hours) { + return hours <= 23; +} + +bool date_get_is_valid_minutes(uint8_t minutes) { + return minutes <= 59; +} + +bool date_get_is_valid_seconds(uint8_t seconds) { + return seconds <= 59; +} + +bool date_get_is_valid_milliseconds(uint16_t milliseconds) { + return milliseconds <= 999; +} + +bool date_get_is_valid_timezone_utc_offset(int8_t timezone_utc_offset) { + return timezone_utc_offset >= -12 && timezone_utc_offset <= 14; +} + +bool date_get_is_valid(struct date *date) { + size_t date_days_of_month = date_get_days_of_month(date->month, date->year); + return (date_get_is_valid_month(date->month) && + date_get_is_valid_day(date->day) && + date->day <= date_days_of_month && + date_get_is_valid_hours(date->hours) && + date_get_is_valid_minutes(date->minutes) && + date_get_is_valid_seconds(date->seconds) && + date_get_is_valid_milliseconds(date->milliseconds) && + date_get_is_valid_timezone_utc_offset(date->timezone_utc_offset)); +} + +string_t date_to_iso_string(struct date *date_original) { + struct date *date = date_copy(date_original); + date_to_utc(date); + + size_t iso_string_length = 24; + string_t result = malloc(sizeof(char) * (iso_string_length + 1)); + + string_t year_string = string_zero_pad(date->year, 4); + string_t month_string = string_zero_pad(date->month, 2); + string_t day_string = string_zero_pad(date->day, 2); + string_t hours_string = string_zero_pad(date->hours, 2); + string_t minutes_string = string_zero_pad(date->minutes, 2); + string_t seconds_string = string_zero_pad(date->seconds, 2); + string_t milliseconds_string = string_zero_pad(date->milliseconds, 3); + sprintf(result, "%s-%s-%sT%s:%s:%s.%sZ", year_string, month_string, day_string, hours_string, minutes_string, seconds_string, milliseconds_string); + free(year_string); + free(month_string); + free(day_string); + free(hours_string); + free(minutes_string); + free(seconds_string); + free(milliseconds_string); + + free(date); + return result; +} + +string_t date_to_iso_string_without_time(struct date *date) { + size_t iso_string_length = 10; + string_t result = malloc(sizeof(char) * (iso_string_length + 1)); + if (result == NULL) { + perror("Error (date_to_iso_string_without_time)"); + exit(EXIT_FAILURE); + } + + string_t year_string = string_zero_pad(date->year, 4); + string_t month_string = string_zero_pad(date->month, 2); + string_t day_string = string_zero_pad(date->day, 2); + sprintf(result, "%s-%s-%s", year_string, month_string, day_string); + free(year_string); + free(month_string); + free(day_string); + + return result; +} + +struct date *date_from_iso_string(string_t iso_string) { + struct date *date = malloc(sizeof(struct date)); + if (date == NULL) { + perror("Error (date_from_iso_string)"); + exit(EXIT_FAILURE); + } + + string_t year_string = string_substring(iso_string, 0, 3); + date->year = (uint16_t)convert_string_to_number(year_string); + free(year_string); + + string_t month_string = string_substring(iso_string, 5, 6); + date->month = (uint8_t)convert_string_to_number(month_string); + free(month_string); + + string_t day_string = string_substring(iso_string, 8, 9); + date->day = (uint8_t)convert_string_to_number(day_string); + free(day_string); + + string_t hours_string = string_substring(iso_string, 11, 12); + date->hours = (uint8_t)convert_string_to_number(hours_string); + free(hours_string); + + string_t minutes_string = string_substring(iso_string, 14, 15); + date->minutes = (uint8_t)convert_string_to_number(minutes_string); + free(minutes_string); + + string_t seconds_string = string_substring(iso_string, 17, 18); + date->seconds = (uint8_t)convert_string_to_number(seconds_string); + free(seconds_string); + + string_t milliseconds_string = string_substring(iso_string, 20, 22); + date->milliseconds = (uint16_t)convert_string_to_number(milliseconds_string); + free(milliseconds_string); + + date->timezone_utc_offset = 0; + + return date; +} + +uint8_t date_get_days_of_month(uint8_t month, uint16_t year) { + switch (month) { + case 1: + return 31; + case 2: + return date_get_is_leap_year(year) ? 29 : 28; + case 3: + return 31; + case 4: + return 30; + case 5: + return 31; + case 6: + return 30; + case 7: + return 31; + case 8: + return 31; + case 9: + return 30; + case 10: + return 31; + case 11: + return 30; + case 12: + return 31; + default: + return 0; + } +} + +bool date_get_is_leap_year(uint16_t year) { + return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0; +} + +uint64_t date_convert_milliseconds_to_seconds(uint16_t milliseconds) { + return milliseconds / MILLISECONDS_PER_SECOND; +} + +uint64_t date_convert_seconds_to_milliseconds(uint64_t seconds) { + return seconds * MILLISECONDS_PER_SECOND; +} + +uint64_t date_convert_days_to_seconds(uint64_t days) { + return days * SECONDS_PER_DAY; +} + +uint64_t date_convert_hms_to_seconds(uint8_t hours, uint8_t minutes, uint8_t seconds) { + return (hours * SECONDS_PER_HOUR) + (minutes * SECONDS_PER_MINUTE) + seconds; +} + +uint64_t date_to_total_seconds(struct date *date) { + uint64_t total_seconds = 0; + + for (uint16_t year = 0; year < date->year; year++) { + total_seconds += 365 * SECONDS_PER_DAY; + if (date_get_is_leap_year(year)) { + total_seconds += SECONDS_PER_DAY; + } + } + + for (uint8_t month = 1; month < date->month; month++) { + total_seconds += date_convert_days_to_seconds(date_get_days_of_month(month, date->year)); + } + + total_seconds += date_convert_days_to_seconds(date->day - 1); + total_seconds += date_convert_hms_to_seconds(date->hours, date->minutes, date->seconds); + + return total_seconds; +} + +uint64_t date_duration_seconds_between_2_dates(struct date *date1, struct date *date2) { + struct date *utc_date1 = date_copy(date1); + struct date *utc_date2 = date_copy(date2); + date_to_utc(utc_date1); + date_to_utc(utc_date2); + + uint64_t total_seconds_date1 = date_to_total_seconds(utc_date1); + uint64_t total_seconds_date2 = date_to_total_seconds(utc_date2); + + free(utc_date1); + free(utc_date2); + + return total_seconds_date1 > total_seconds_date2 ? total_seconds_date1 - total_seconds_date2 : total_seconds_date2 - total_seconds_date1; +} + +void date_add_hours(struct date *date, int64_t hours) { + if (hours == 0) { + return; + } + int64_t total_hours = date->hours + hours; + int64_t additional_days = total_hours / 24; + int64_t new_hours = total_hours % 24; + + if (new_hours < 0) { + new_hours += 24; + additional_days -= 1; + } + + date->hours = (uint8_t)new_hours; + + if (additional_days != 0) { + date->day += additional_days; + + while (date->day > date_get_days_of_month(date->month, date->year)) { + date->day -= date_get_days_of_month(date->month, date->year); + date->month++; + + if (date->month > 12) { + date->month = 1; + date->year++; + } + } + + while (date->day < 1) { + date->month--; + if (date->month < 1) { + date->month = 12; + date->year--; + } + + date->day += date_get_days_of_month(date->month, date->year); + } + } +} + +void date_add_days(struct date *date, int64_t days) { + date_add_hours(date, days * 24); +} + +void date_to_utc(struct date *date) { + if (date->timezone_utc_offset == 0) { + return; + } + int8_t timezone_utc_offset = date->timezone_utc_offset; + date->timezone_utc_offset = 0; + date_add_hours(date, mathematics_opposite(timezone_utc_offset)); +} diff --git a/lib/date.h b/lib/date.h new file mode 100644 index 0000000..5d9e67d --- /dev/null +++ b/lib/date.h @@ -0,0 +1,324 @@ +#ifndef __LIBCPROJECT_DATE__ +#define __LIBCPROJECT_DATE__ + +#include +#include +#include +#include +#include + +#include "convert.h" +#include "mathematics.h" +#include "string.h" +#include "types.h" + +#define SECONDS_PER_MINUTE 60 +#define SECONDS_PER_HOUR (60 * SECONDS_PER_MINUTE) +#define SECONDS_PER_DAY (24 * SECONDS_PER_HOUR) +#define MILLISECONDS_PER_SECOND 1000 + +/** + * @brief Date object representing a single moment in time. + * @since v4.3.0 + */ +struct date { + /** + * Year. + * Between [0, 9999] (inclusive). + * As per ISO 8601, a four-digit year [YYYY] and represents years from 0000 to 9999, year 0000 being equal to 1 BC and all others AD. + */ + uint16_t year; + + /** + * Month. + * Between [1, 12] (inclusive). + */ + uint8_t month; + + /** + * Day. + * Between [1, 31] (inclusive). + */ + uint8_t day; + + /** + * Hours. + * Between [0, 23] (inclusive). + */ + uint8_t hours; + + /** + * Minutes. + * Between [0, 59] (inclusive). + */ + uint8_t minutes; + + /** + * Seconds. + * Between [0, 59] (inclusive). + */ + uint8_t seconds; + + /** + * Milliseconds. + * Between [0, 999] (inclusive). + */ + uint16_t milliseconds; + + /** + * Timezone UTC offset. + * Between [-12, 14] + */ + int8_t timezone_utc_offset; +}; + +/** + * @brief Return the copy of a date. + * + * @param date + * @return struct date* + * @since v4.3.0 + */ +struct date *date_copy(struct date *date_to_copy); + +/** + * @brief Check if a year is valid, between [0, 9999] (inclusive). + * + * @param year + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_year(uint16_t year); + +/** + * @brief Check if a month is valid, between [1, 12] (inclusive). + * + * @param month + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_month(uint8_t month); + +/** + * @brief Check if a day is valid, between [1, 31] (inclusive). + * + * @param day + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_day(uint8_t day); + +/** + * @brief Check if hours are valid, between [0, 23] (inclusive). + * + * @param hours + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_hours(uint8_t hours); + +/** + * @brief Check if minutes are valid, between [0, 59] (inclusive). + * + * @param minutes + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_minutes(uint8_t minutes); + +/** + * @brief Check if seconds are valid, between [0, 59] (inclusive). + * + * @param seconds + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_seconds(uint8_t seconds); + +/** + * @brief Check if milliseconds are valid, between [0, 999] (inclusive). + * + * @param milliseconds + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_milliseconds(uint16_t milliseconds); + +/** + * @brief Check if the timezone UTC offset is valid, between [-12, 14] (inclusive). + * + * @param timezone_utc_offset + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid_timezone_utc_offset(int8_t timezone_utc_offset); + +/** + * @brief Check if the date is valid (all fields are possible). + * + * @param date + * @return bool + * @since v4.3.0 + */ +bool date_get_is_valid(struct date *date); + +/** + * @brief String representing the date in the date time string format, a simplified format based on ISO 8601, which is always 24 characters long (`YYYY-MM-DDTHH:mm:ss.sssZ`). The timezone is always UTC, as denoted by the suffix `Z`. + * + * @param date + * @return string_t + * + * @code + * date_to_iso_string() // "2024-09-11T09:39:18.203Z" + * @endcode + * + * @since v4.3.0 + */ +string_t date_to_iso_string(struct date *date); + +/** + * @brief String representing the date in the ISO 8601 format, without time information (`YYYY-MM-DD`). + * + * @param date + * @return string_t + * + * @code + * date_to_iso_string_without_time() // "2024-09-11" + * @endcode + * + * @since v4.3.0 + */ +string_t date_to_iso_string_without_time(struct date *date); + +/** + * @brief Create a date from an ISO 8601 string, with the format `YYYY-MM-DDTHH:mm:ss.sssZ`. + * + * The timezone is always UTC, as denoted by the suffix `Z`. + * + * @param iso_string + * @return struct date* + * @since v4.3.0 + */ +struct date *date_from_iso_string(string_t iso_string); + +/** + * @brief Get number of days in one month [1, 12]. + * + * @param month + * @return uint8_t + * @since v4.3.0 + */ +uint8_t date_get_days_of_month(uint8_t month, uint16_t year); + +/** + * @brief Determine if a year is a leap year. + * + * @param year + * @return bool + * + * @code + * date_is_leap_year(2020) // true + * + * date_is_leap_year(2021) // false + * + * date_is_leap_year(2022) // false + * + * date_is_leap_year(2023) // false + * + * date_is_leap_year(2024) // true + * @endcode + * + * @since v4.3.0 + */ +bool date_get_is_leap_year(uint16_t year); + +/** + * @brief Convert milliseconds to seconds. + * + * @param milliseconds + * @return uint64_t + * @since v4.3.0 + */ +uint64_t date_convert_milliseconds_to_seconds(uint16_t milliseconds); + +/** + * @brief Convert seconds to milliseconds. + * + * @param seconds + * @return uint64_t + * @since v4.3.0 + */ +uint64_t date_convert_seconds_to_milliseconds(uint64_t seconds); + +/** + * @brief Convert days to seconds. + * + * @param days + * @return uint64_t + * @since v4.3.0 + */ +uint64_t date_convert_days_to_seconds(uint64_t days); + +/** + * @brief Convert hours, minutes, and seconds to seconds. + * + * @param hours + * @param minutes + * @param seconds + * @return uint64_t + * @since v4.3.0 + */ +uint64_t date_convert_hms_to_seconds(uint8_t hours, uint8_t minutes, uint8_t seconds); + +/** + * @brief Convert a date to total seconds. + * + * @param date + * @return uint64_t + * @since v4.3.0 + */ +uint64_t date_to_total_seconds(struct date *date); + +/** + * @brief Calculate the duration in seconds between 2 dates. + * + * @param date1 + * @param date2 + * @return uint64_t + * @since v4.3.0 + */ +uint64_t date_duration_seconds_between_2_dates(struct date *date1, struct date *date2); + +/** + * @brief Add hours to the date, managing the day, month, year changes if necessary. + * + * NOTE: Mutates the date. + * + * @param date + * @param hours + * @since v4.3.0 + */ +void date_add_hours(struct date *date, int64_t hours); + +/** + * @brief Adds days to the date, managing month and year changes as needed. + * + * NOTE: Mutates the date. + * + * @param date The date to which days are being added. + * @param days The number of days to add. + * @since v4.3.0 + */ +void date_add_days(struct date *date, int64_t days); + +/** + * @brief Transform the date with a Timezone UTC Offset to UTC (timezone_utc_offset = 0). + * + * NOTE: Mutates the date. + * + * @param date + * @since v4.3.0 + */ +void date_to_utc(struct date *date); + +#endif diff --git a/libcproject.h b/libcproject.h index 212d2d3..844de1b 100644 --- a/libcproject.h +++ b/libcproject.h @@ -4,6 +4,7 @@ #include "lib/array_list.h" #include "lib/character.h" #include "lib/convert.h" +#include "lib/date.h" #include "lib/filesystem.h" #include "lib/hash_map.h" #include "lib/linked_list.h" diff --git a/test/date_test.c b/test/date_test.c new file mode 100644 index 0000000..ba03d8b --- /dev/null +++ b/test/date_test.c @@ -0,0 +1,164 @@ +#include "date_test.h" + +void date_test() { + date_copy_test(); + date_to_iso_string_test(); + date_to_iso_string_without_time_test(); + date_from_iso_string_test(); + date_get_is_leap_year_test(); + date_duration_seconds_between_2_dates_test(); + date_to_utc_test(); +} + +void date_copy_test() { + struct date *date = malloc(sizeof(struct date)); + date->year = 2024; + date->month = 9; + date->day = 10; + date->hours = 20; + date->minutes = 34; + date->seconds = 25; + date->milliseconds = 76; + date->timezone_utc_offset = 0; + + struct date *date2 = date_copy(date); + assert(date != date2); + assert(date->year == date2->year); + assert(date->month == date2->month); + assert(date->day == date2->day); + assert(date->hours == date2->hours); + assert(date->minutes == date2->minutes); + assert(date->seconds == date2->seconds); + assert(date->milliseconds == date2->milliseconds); + assert(date->timezone_utc_offset == date2->timezone_utc_offset); + + date->year = 2025; + assert(date->year == 2025); + assert(date2->year == 2024); + + free(date); + free(date2); +} + +void date_to_iso_string_test() { + struct date *date = malloc(sizeof(struct date)); + date->year = 2024; + date->month = 9; + date->day = 10; + date->hours = 20; + date->minutes = 34; + date->seconds = 25; + date->milliseconds = 76; + date->timezone_utc_offset = 0; + + string_t iso_string = date_to_iso_string(date); + assert(assert_string_equal(iso_string, "2024-09-10T20:34:25.076Z")); + free(iso_string); + + free(date); +} + +void date_to_iso_string_without_time_test() { + struct date *date = malloc(sizeof(struct date)); + date->year = 2024; + date->month = 9; + date->day = 10; + date->hours = 20; + date->minutes = 34; + date->seconds = 25; + date->milliseconds = 76; + date->timezone_utc_offset = 0; + + string_t iso_string = date_to_iso_string_without_time(date); + assert(assert_string_equal(iso_string, "2024-09-10")); + free(iso_string); + + free(date); +} + +void date_from_iso_string_test() { + string_t iso_string = "2024-09-10T20:34:25.076Z"; + struct date *date = date_from_iso_string(iso_string); + assert(date->year == 2024); + assert(date->month == 9); + assert(date->day == 10); + assert(date->hours == 20); + assert(date->minutes == 34); + assert(date->seconds == 25); + assert(date->milliseconds == 76); + assert(date->timezone_utc_offset == 0); + + free(date); +} + +void date_get_is_leap_year_test() { + assert(date_get_is_leap_year(2020)); + assert(!date_get_is_leap_year(2021)); + assert(!date_get_is_leap_year(2022)); + assert(!date_get_is_leap_year(2023)); + assert(date_get_is_leap_year(2024)); +} + +void date_duration_seconds_between_2_dates_test() { + struct date *date1 = date_from_iso_string("2024-09-10T20:34:25.076Z"); + struct date *date2 = date_from_iso_string("2024-09-10T20:34:25.076Z"); + assert(date_duration_seconds_between_2_dates(date1, date2) == 0); + free(date1); + free(date2); + + date1 = date_from_iso_string("2024-09-10T20:34:25.076Z"); + date2 = date_from_iso_string("2024-09-10T23:34:26.076Z"); + assert(date_duration_seconds_between_2_dates(date1, date2) == 10801); + free(date1); + free(date2); + + date1 = date_from_iso_string("2024-09-10T20:34:25.076Z"); + date2 = date_from_iso_string("2024-09-10T20:48:25.076Z"); + assert(date_duration_seconds_between_2_dates(date1, date2) == 840); + free(date1); + free(date2); + + date1 = date_from_iso_string("2024-09-10T20:34:25.076Z"); + date2 = date_from_iso_string("2024-09-10T20:34:38.076Z"); + assert(date_duration_seconds_between_2_dates(date1, date2) == 13); + free(date1); + free(date2); +} + +void date_to_utc_test() { + struct date *date = date_from_iso_string("2024-09-10T20:34:25.076Z"); + date->timezone_utc_offset = 3; + date_to_utc(date); + assert(date->timezone_utc_offset == 0); + string_t iso_string = date_to_iso_string(date); + assert(assert_string_equal(iso_string, "2024-09-10T17:34:25.076Z")); + free(iso_string); + free(date); + + date = date_from_iso_string("2024-09-10T20:34:25.076Z"); + date->timezone_utc_offset = -3; + date_to_utc(date); + assert(date->timezone_utc_offset == 0); + iso_string = date_to_iso_string(date); + assert(assert_string_equal(iso_string, "2024-09-10T23:34:25.076Z")); + free(iso_string); + free(date); + + date = date_from_iso_string("2024-01-01T00:00:00.000Z"); + date->timezone_utc_offset = 3; + date_to_utc(date); + assert(date->timezone_utc_offset == 0); + iso_string = date_to_iso_string(date); + assert(assert_string_equal(iso_string, "2023-12-31T21:00:00.000Z")); + free(iso_string); + free(date); + + date = date_from_iso_string("2023-12-31T21:00:00.000Z"); + date->timezone_utc_offset = -4; + date_to_utc(date); + assert(date->timezone_utc_offset == 0); + iso_string = date_to_iso_string(date); + assert(assert_string_equal(iso_string, "2024-01-01T01:00:00.000Z")); + free(iso_string); + free(date); +} diff --git a/test/date_test.h b/test/date_test.h new file mode 100644 index 0000000..3d9ddaf --- /dev/null +++ b/test/date_test.h @@ -0,0 +1,25 @@ +#ifndef __DATE_TEST__ +#define __DATE_TEST__ + +#include + +#include "libcproject.h" +#include "test.h" + +void date_test(); + +void date_copy_test(); + +void date_to_iso_string_test(); + +void date_to_iso_string_without_time_test(); + +void date_from_iso_string_test(); + +void date_get_is_leap_year_test(); + +void date_duration_seconds_between_2_dates_test(); + +void date_to_utc_test(); + +#endif diff --git a/test/main.c b/test/main.c index 638a224..f488c73 100644 --- a/test/main.c +++ b/test/main.c @@ -4,6 +4,7 @@ #include "array_list_test.h" #include "character_test.h" #include "convert_test.h" +#include "date_test.h" #include "hash_map_test.h" #include "linked_list_test.h" #include "mathematics_test.h" @@ -15,6 +16,7 @@ int main() { array_list_test(); character_test(); convert_test(); + date_test(); hash_map_test(); linked_list_test(); mathematics_test();