diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..8450693 --- /dev/null +++ b/.clang-format @@ -0,0 +1,11 @@ +BasedOnStyle: LLVM +ColumnLimit: 100 +BinPackArguments: false +BinPackParameters: false +AllowAllArgumentsOnNextLine: false +AlignAfterOpenBracket: BlockIndent +UseTab: ForIndentation +IndentWidth: 4 +TabWidth: 4 +ContinuationIndentWidth: 4 +AllowShortFunctionsOnASingleLine: None diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d89c76d --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +charset = utf-8 + +[*.{c,cc,cpp,h,hpp,ino}] +indent_style = tab +indent_size = tab +tab_width = 4 diff --git a/.gitignore b/.gitignore index 78f49b6..6346d5c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ .venv build/ build_prev_runner/ -.vscode \ No newline at end of file diff --git a/.vscode/bin/clang-format b/.vscode/bin/clang-format new file mode 100755 index 0000000..0df371f --- /dev/null +++ b/.vscode/bin/clang-format @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if command -v clang-format >/dev/null 2>&1; then + exec clang-format "$@" +fi + +_home_dir="${HOME:-}" +if [ -n "$_home_dir" ]; then + _candidate="$(ls -1d "$_home_dir"/.vscode/extensions/ms-vscode.cpptools-*-linux-x64/LLVM/bin/clang-format 2>/dev/null | tail -n 1 || true)" + if [ -n "$_candidate" ] && [ -x "$_candidate" ]; then + exec "$_candidate" "$@" + fi +fi + +echo "clang-format executable not found." >&2 +echo "Install clang-format system-wide or install/update ms-vscode.cpptools." >&2 +exit 127 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f814711 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "pioarduino.pioarduino-ide", + "xaver.clang-format" + ], + "unwantedRecommendations": [ + "ms-vscode.cpptools-extension-pack" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..24368c8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,30 @@ +{ + "files.associations": { + "*.ino": "cpp" + }, + "editor.defaultFormatter": "xaver.clang-format", + "C_Cpp.formatting": "Disabled", + "clang-format.style": "file", + "clang-format.executable": "${workspaceRoot}/.vscode/bin/clang-format", + "[cpp]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[c]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + }, + "[arduino]": { + "editor.defaultFormatter": "xaver.clang-format", + "editor.detectIndentation": false, + "editor.insertSpaces": false, + "editor.tabSize": 4, + "editor.formatOnSave": true + } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..20e66d5 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,12 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Format Firmware Sources", + "type": "shell", + "command": "bash ${workspaceFolder}/scripts/format_cpp.sh", + "group": "build", + "problemMatcher": [] + } + ] +} diff --git a/README.md b/README.md index bb015e0..95ca936 100644 --- a/README.md +++ b/README.md @@ -509,6 +509,13 @@ if (rise.ok && set.ok) { - You can also run `pio ci examples/basic_date --board esp32dev --project-option "build_flags=-std=gnu++17"` locally. - Unity smoke tests live in `test/test_esp_date`; run them on hardware with `pio test -e esp32dev` (or your board environment) to exercise arithmetic, formatting, and parsing routines. +## Formatting Baseline + +This repository follows the firmware formatting baseline from `esptoolkit-template`: +- `.clang-format` is the source of truth for C/C++/INO layout. +- `.editorconfig` enforces tabs (`tab_width = 4`), LF endings, and final newline. +- Format all tracked firmware sources with `bash scripts/format_cpp.sh`. + ## License MIT — see [LICENSE.md](LICENSE.md). diff --git a/examples/basic_date/basic_date.ino b/examples/basic_date/basic_date.ino index 143e7ac..3a25cc8 100644 --- a/examples/basic_date/basic_date.ino +++ b/examples/basic_date/basic_date.ino @@ -6,103 +6,115 @@ ESPDate date; bool releasedDateResources = false; class SyncObserver { - public: - void onNtpSync(const DateTime &syncedAtUtc) { - Serial.printf("NTP synced, epoch: %lld\n", static_cast(syncedAtUtc.epochSeconds)); - } + public: + void onNtpSync(const DateTime &syncedAtUtc) { + Serial.printf( + "NTP synced, epoch: %lld\n", + static_cast(syncedAtUtc.epochSeconds) + ); + } }; SyncObserver syncObserver; void printFormatted(const char *label, const DateTime &dt) { - char buf[32]; - if (date.formatUtc(dt, ESPDateFormat::Iso8601, buf, sizeof(buf))) { - Serial.print(label); - Serial.println(buf); - } + char buf[32]; + if (date.formatUtc(dt, ESPDateFormat::Iso8601, buf, sizeof(buf))) { + Serial.print(label); + Serial.println(buf); + } } void setup() { - Serial.begin(115200); - delay(200); - Serial.println("ESPDate basic example"); - Serial.println("Connect WiFi so configTzTime can sync time, or set the system clock manually before using date.now()."); - ESPDateConfig cfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000}; - cfg.ntpServer2 = "time.google.com"; - cfg.ntpServer3 = "time.cloudflare.com"; - date.init(cfg); - date.setNtpSyncCallback(std::bind(&SyncObserver::onNtpSync, &syncObserver, std::placeholders::_1)); - date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update - date.syncNTP(); // force an immediate refresh using configured NTP - - DateTime now = date.now(); - DateTime tomorrow = date.addDays(now, 1); - DateTime lastWeek = date.subDays(now, 7); - DateTime lastYear = date.subYears(now, 1); - - printFormatted("Now (UTC): ", now); - printFormatted("Tomorrow (UTC): ", tomorrow); - printFormatted("Last week (UTC): ", lastWeek); - - char localNowBuffer[32]; - if (date.nowLocalString(localNowBuffer, sizeof(localNowBuffer))) { - Serial.printf("Now (Local string): %s\n", localNowBuffer); - } - std::string utcNowString = date.nowUtcString(); - Serial.printf("Now (UTC string): %s\n", utcNowString.c_str()); - - char lastYearLocalBuffer[32]; - if (lastYear.localString(lastYearLocalBuffer, sizeof(lastYearLocalBuffer))) { - Serial.printf("Last year (Local string): %s\n", lastYearLocalBuffer); - } - - int64_t deltaDays = date.differenceInDays(tomorrow, now); - Serial.printf("Days between now and tomorrow: %lld\n", static_cast(deltaDays)); - - bool equalSeconds = date.isEqual(now, tomorrow); - bool equalMinutes = date.isEqualMinutes(now, date.addSeconds(now, 30)); // same minute - Serial.printf("Equal (seconds precision): %s\n", equalSeconds ? "true" : "false"); - Serial.printf("Equal (minutes precision): %s\n", equalMinutes ? "true" : "false"); - - DateTime start = date.startOfDayLocal(now); - DateTime end = date.endOfDayLocal(now); - - char buf[32]; - if (date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf))) { - Serial.print("Local day starts at: "); - Serial.println(buf); - } - if (date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf))) { - Serial.print("Local day ends at: "); - Serial.println(buf); - } - - ESPDate::ParseResult parsed = date.parseIso8601Utc("2025-12-31T23:59:30Z"); - if (parsed.ok) { - Serial.print("Parsed ISO-8601 (UTC): "); - date.formatUtc(parsed.value, ESPDateFormat::DateTime, buf, sizeof(buf)); - Serial.println(buf); - } - - ESPDate::ParseResult parsedLocal = date.parseDateTimeLocal("2025-01-01 03:00:00"); - if (parsedLocal.ok) { - Serial.print("Parsed local datetime: "); - if (date.formatLocal(parsedLocal.value, ESPDateFormat::DateTime, buf, sizeof(buf))) { - Serial.println(buf); - } - } - - MoonPhaseResult phase = date.moonPhase(); - if (phase.ok) { - Serial.printf("Moon phase: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination); - } + Serial.begin(115200); + delay(200); + Serial.println("ESPDate basic example"); + Serial.println( + "Connect WiFi so configTzTime can sync time, or set the system clock manually before using " + "date.now()." + ); + ESPDateConfig cfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000}; + cfg.ntpServer2 = "time.google.com"; + cfg.ntpServer3 = "time.cloudflare.com"; + date.init(cfg); + date.setNtpSyncCallback( + std::bind(&SyncObserver::onNtpSync, &syncObserver, std::placeholders::_1) + ); + date.setNtpSyncIntervalMs(10 * 60 * 1000); // optional runtime update + date.syncNTP(); // force an immediate refresh using configured NTP + + DateTime now = date.now(); + DateTime tomorrow = date.addDays(now, 1); + DateTime lastWeek = date.subDays(now, 7); + DateTime lastYear = date.subYears(now, 1); + + printFormatted("Now (UTC): ", now); + printFormatted("Tomorrow (UTC): ", tomorrow); + printFormatted("Last week (UTC): ", lastWeek); + + char localNowBuffer[32]; + if (date.nowLocalString(localNowBuffer, sizeof(localNowBuffer))) { + Serial.printf("Now (Local string): %s\n", localNowBuffer); + } + std::string utcNowString = date.nowUtcString(); + Serial.printf("Now (UTC string): %s\n", utcNowString.c_str()); + + char lastYearLocalBuffer[32]; + if (lastYear.localString(lastYearLocalBuffer, sizeof(lastYearLocalBuffer))) { + Serial.printf("Last year (Local string): %s\n", lastYearLocalBuffer); + } + + int64_t deltaDays = date.differenceInDays(tomorrow, now); + Serial.printf("Days between now and tomorrow: %lld\n", static_cast(deltaDays)); + + bool equalSeconds = date.isEqual(now, tomorrow); + bool equalMinutes = date.isEqualMinutes(now, date.addSeconds(now, 30)); // same minute + Serial.printf("Equal (seconds precision): %s\n", equalSeconds ? "true" : "false"); + Serial.printf("Equal (minutes precision): %s\n", equalMinutes ? "true" : "false"); + + DateTime start = date.startOfDayLocal(now); + DateTime end = date.endOfDayLocal(now); + + char buf[32]; + if (date.formatLocal(start, ESPDateFormat::DateTime, buf, sizeof(buf))) { + Serial.print("Local day starts at: "); + Serial.println(buf); + } + if (date.formatLocal(end, ESPDateFormat::DateTime, buf, sizeof(buf))) { + Serial.print("Local day ends at: "); + Serial.println(buf); + } + + ESPDate::ParseResult parsed = date.parseIso8601Utc("2025-12-31T23:59:30Z"); + if (parsed.ok) { + Serial.print("Parsed ISO-8601 (UTC): "); + date.formatUtc(parsed.value, ESPDateFormat::DateTime, buf, sizeof(buf)); + Serial.println(buf); + } + + ESPDate::ParseResult parsedLocal = date.parseDateTimeLocal("2025-01-01 03:00:00"); + if (parsedLocal.ok) { + Serial.print("Parsed local datetime: "); + if (date.formatLocal(parsedLocal.value, ESPDateFormat::DateTime, buf, sizeof(buf))) { + Serial.println(buf); + } + } + + MoonPhaseResult phase = date.moonPhase(); + if (phase.ok) { + Serial.printf( + "Moon phase: %d deg, illumination: %.3f\n", + phase.angleDegrees, + phase.illumination + ); + } } void loop() { - // Demonstrate explicit teardown in long-running sketches. - if (!releasedDateResources && millis() > 60000UL && date.isInitialized()) { - date.deinit(); - releasedDateResources = true; - Serial.println("ESPDate deinitialized."); - } + // Demonstrate explicit teardown in long-running sketches. + if (!releasedDateResources && millis() > 60000UL && date.isInitialized()) { + date.deinit(); + releasedDateResources = true; + Serial.println("ESPDate deinitialized."); + } } diff --git a/examples/ntp_sync_tracking/ntp_sync_tracking.ino b/examples/ntp_sync_tracking/ntp_sync_tracking.ino index 3255600..3ba4952 100644 --- a/examples/ntp_sync_tracking/ntp_sync_tracking.ino +++ b/examples/ntp_sync_tracking/ntp_sync_tracking.ino @@ -6,52 +6,54 @@ ESPDate date; unsigned long lastPrintMs = 0; void printLastSync() { - char localBuf[32]; - char utcBuf[32]; - - if (!date.hasLastNtpSync()) { - Serial.println("NTP has not synced yet."); - return; - } - - if (date.lastNtpSyncStringLocal(localBuf, sizeof(localBuf))) { - Serial.printf("Last NTP sync (local): %s\n", localBuf); - } - if (date.lastNtpSyncStringUtc(utcBuf, sizeof(utcBuf), ESPDateFormat::Iso8601)) { - Serial.printf("Last NTP sync (UTC) : %s\n", utcBuf); - } + char localBuf[32]; + char utcBuf[32]; + + if (!date.hasLastNtpSync()) { + Serial.println("NTP has not synced yet."); + return; + } + + if (date.lastNtpSyncStringLocal(localBuf, sizeof(localBuf))) { + Serial.printf("Last NTP sync (local): %s\n", localBuf); + } + if (date.lastNtpSyncStringUtc(utcBuf, sizeof(utcBuf), ESPDateFormat::Iso8601)) { + Serial.printf("Last NTP sync (UTC) : %s\n", utcBuf); + } } void setup() { - Serial.begin(115200); - delay(200); - Serial.println("ESPDate NTP sync tracking example"); - Serial.println("Connect WiFi before this sketch runs so SNTP can reach the configured servers."); - - ESPDateConfig cfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000}; - cfg.ntpServer2 = "time.google.com"; - cfg.ntpServer3 = "time.cloudflare.com"; - date.init(cfg); - date.setNtpSyncCallback([](const DateTime &syncedAtUtc) { - char buf[32]; - if (syncedAtUtc.localString(buf, sizeof(buf))) { - Serial.printf("Callback: synced at %s\n", buf); - } - }); - - // Force an immediate sync attempt. - if (!date.syncNTP()) { - Serial.println("syncNTP() failed (missing NTP config or runtime support).\n"); - } - - printLastSync(); - lastPrintMs = millis(); + Serial.begin(115200); + delay(200); + Serial.println("ESPDate NTP sync tracking example"); + Serial.println( + "Connect WiFi before this sketch runs so SNTP can reach the configured servers." + ); + + ESPDateConfig cfg{0.0f, 0.0f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org", 15 * 60 * 1000}; + cfg.ntpServer2 = "time.google.com"; + cfg.ntpServer3 = "time.cloudflare.com"; + date.init(cfg); + date.setNtpSyncCallback([](const DateTime &syncedAtUtc) { + char buf[32]; + if (syncedAtUtc.localString(buf, sizeof(buf))) { + Serial.printf("Callback: synced at %s\n", buf); + } + }); + + // Force an immediate sync attempt. + if (!date.syncNTP()) { + Serial.println("syncNTP() failed (missing NTP config or runtime support).\n"); + } + + printLastSync(); + lastPrintMs = millis(); } void loop() { - // Re-print every 30 seconds so you can observe last sync state. - if (millis() - lastPrintMs >= 30000UL) { - printLastSync(); - lastPrintMs = millis(); - } + // Re-print every 30 seconds so you can observe last sync state. + if (millis() - lastPrintMs >= 30000UL) { + printLastSync(); + lastPrintMs = millis(); + } } diff --git a/examples/string_helpers/string_helpers.ino b/examples/string_helpers/string_helpers.ino index 10c5e2b..0b519a2 100644 --- a/examples/string_helpers/string_helpers.ino +++ b/examples/string_helpers/string_helpers.ino @@ -4,45 +4,45 @@ ESPDate date; void setup() { - Serial.begin(115200); - delay(200); - Serial.println("ESPDate string helpers example"); - - // Local formatting methods use current TZ. Here we use UTC for deterministic output. - date.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); - - DateTime release = date.fromUtc(2025, 1, 2, 3, 4, 5); - - char buf[32]; - if (release.utcString(buf, sizeof(buf))) { - Serial.printf("DateTime::utcString -> %s\n", buf); - } - if (release.localString(buf, sizeof(buf))) { - Serial.printf("DateTime::localString -> %s\n", buf); - } - - std::string localStr = release.localString(); - Serial.printf("DateTime local std::string -> %s\n", localStr.c_str()); - - // Convert to local components, then format directly from LocalDateTime. - LocalDateTime local = date.toLocal(release); - if (local.localString(buf, sizeof(buf))) { - Serial.printf("LocalDateTime::localString -> %s\n", buf); - } - - // ESPDate wrapper methods still available when preferred. - if (date.dateTimeToStringUtc(release, buf, sizeof(buf), ESPDateFormat::Iso8601)) { - Serial.printf("ESPDate::dateTimeToStringUtc -> %s\n", buf); - } - - if (date.nowLocalString(buf, sizeof(buf))) { - Serial.printf("ESPDate::nowLocalString -> %s\n", buf); - } - - std::string nowUtc = date.nowUtcString(ESPDateFormat::Iso8601); - Serial.printf("ESPDate::nowUtcString(ISO) -> %s\n", nowUtc.c_str()); + Serial.begin(115200); + delay(200); + Serial.println("ESPDate string helpers example"); + + // Local formatting methods use current TZ. Here we use UTC for deterministic output. + date.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); + + DateTime release = date.fromUtc(2025, 1, 2, 3, 4, 5); + + char buf[32]; + if (release.utcString(buf, sizeof(buf))) { + Serial.printf("DateTime::utcString -> %s\n", buf); + } + if (release.localString(buf, sizeof(buf))) { + Serial.printf("DateTime::localString -> %s\n", buf); + } + + std::string localStr = release.localString(); + Serial.printf("DateTime local std::string -> %s\n", localStr.c_str()); + + // Convert to local components, then format directly from LocalDateTime. + LocalDateTime local = date.toLocal(release); + if (local.localString(buf, sizeof(buf))) { + Serial.printf("LocalDateTime::localString -> %s\n", buf); + } + + // ESPDate wrapper methods still available when preferred. + if (date.dateTimeToStringUtc(release, buf, sizeof(buf), ESPDateFormat::Iso8601)) { + Serial.printf("ESPDate::dateTimeToStringUtc -> %s\n", buf); + } + + if (date.nowLocalString(buf, sizeof(buf))) { + Serial.printf("ESPDate::nowLocalString -> %s\n", buf); + } + + std::string nowUtc = date.nowUtcString(ESPDateFormat::Iso8601); + Serial.printf("ESPDate::nowUtcString(ISO) -> %s\n", nowUtc.c_str()); } void loop() { - // no-op + // no-op } diff --git a/examples/sun_cycle/sun_cycle.ino b/examples/sun_cycle/sun_cycle.ino index 18322b9..97ab6b4 100644 --- a/examples/sun_cycle/sun_cycle.ino +++ b/examples/sun_cycle/sun_cycle.ino @@ -4,52 +4,60 @@ // Configure coordinates and TZ once in setup for repeated use (with NTP sync via configTzTime) ESPDate solar; -void printLocal(const char* label, const DateTime& dt) { - char buf[32]; - if (solar.formatLocal(dt, ESPDateFormat::DateTime, buf, sizeof(buf))) { - Serial.print(label); - Serial.println(buf); - } +void printLocal(const char *label, const DateTime &dt) { + char buf[32]; + if (solar.formatLocal(dt, ESPDateFormat::DateTime, buf, sizeof(buf))) { + Serial.print(label); + Serial.println(buf); + } } void setup() { - Serial.begin(115200); - delay(250); - Serial.println("ESPDate sun cycle example"); - Serial.println("Connect WiFi so configTzTime can sync time, or set system clock/TZ manually before running."); - ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"}; - cfg.ntpServer2 = "time.google.com"; - cfg.ntpServer3 = "time.cloudflare.com"; - solar.init(cfg); - - DateTime today = solar.now(); - - SunCycleResult rise = solar.sunrise(today); - SunCycleResult set = solar.sunset(today); - - if (rise.ok && set.ok) { - printLocal("Sunrise: ", rise.value); - printLocal("Sunset : ", set.value); - } else { - Serial.println("No sunrise/sunset for these coordinates today."); - } - - MoonPhaseResult phase = solar.moonPhase(today); - if (phase.ok) { - Serial.printf("Moon phase: %d deg, illumination: %.3f\n", phase.angleDegrees, phase.illumination); - } - - // Use offsets to extend the daylight window (e.g., start 15 min earlier, end 30 min earlier) - bool isDayWithOffsets = solar.isDay(-900, -1800, today); - Serial.printf("Is day (with offsets): %s\n", isDayWithOffsets ? "yes" : "no"); - - // Manual call with explicit parameters (New York, auto-DST via TZ string) - SunCycleResult nycRise = solar.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2", today); - if (nycRise.ok) { - printLocal("NYC Sunrise (local TZ set for solar): ", nycRise.value); - } + Serial.begin(115200); + delay(250); + Serial.println("ESPDate sun cycle example"); + Serial.println( + "Connect WiFi so configTzTime can sync time, or set system clock/TZ manually before " + "running." + ); + ESPDateConfig cfg{47.4979f, 19.0402f, "CET-1CEST,M3.5.0/2,M10.5.0/3", "pool.ntp.org"}; + cfg.ntpServer2 = "time.google.com"; + cfg.ntpServer3 = "time.cloudflare.com"; + solar.init(cfg); + + DateTime today = solar.now(); + + SunCycleResult rise = solar.sunrise(today); + SunCycleResult set = solar.sunset(today); + + if (rise.ok && set.ok) { + printLocal("Sunrise: ", rise.value); + printLocal("Sunset : ", set.value); + } else { + Serial.println("No sunrise/sunset for these coordinates today."); + } + + MoonPhaseResult phase = solar.moonPhase(today); + if (phase.ok) { + Serial.printf( + "Moon phase: %d deg, illumination: %.3f\n", + phase.angleDegrees, + phase.illumination + ); + } + + // Use offsets to extend the daylight window (e.g., start 15 min earlier, end 30 min earlier) + bool isDayWithOffsets = solar.isDay(-900, -1800, today); + Serial.printf("Is day (with offsets): %s\n", isDayWithOffsets ? "yes" : "no"); + + // Manual call with explicit parameters (New York, auto-DST via TZ string) + SunCycleResult nycRise = + solar.sunrise(40.7128f, -74.0060f, "EST5EDT,M3.2.0/2,M11.1.0/2", today); + if (nycRise.ok) { + printLocal("NYC Sunrise (local TZ set for solar): ", nycRise.value); + } } void loop() { - // no-op + // no-op } diff --git a/scripts/format_cpp.sh b/scripts/format_cpp.sh new file mode 100755 index 0000000..7d17b04 --- /dev/null +++ b/scripts/format_cpp.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash + +set -euo pipefail + +_repo_root="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +_clang_format="${_repo_root}/.vscode/bin/clang-format" + +if [ ! -x "${_clang_format}" ]; then + echo "clang-format wrapper not found: ${_clang_format}" >&2 + exit 1 +fi + +mapfile -d '' _format_files < <( + git -C "${_repo_root}" ls-files -z -- '*.c' '*.cc' '*.cpp' '*.h' '*.hpp' '*.ino' +) + +if [ "${#_format_files[@]}" -eq 0 ]; then + echo "No tracked C/C++/INO files found to format." + exit 0 +fi + +"${_clang_format}" -i --style=file "${_format_files[@]}" + +echo "Formatted ${#_format_files[@]} files." diff --git a/src/esp_date/date.cpp b/src/esp_date/date.cpp index 5cfe2e6..725b7e9 100644 --- a/src/esp_date/date.cpp +++ b/src/esp_date/date.cpp @@ -7,25 +7,25 @@ #include #if defined(__has_include) -# if __has_include() -# include -# define ESPDATE_HAS_CONFIG_TZ_TIME 1 -# define ESPDATE_HAS_SNTP_NOTIFICATION_CB 1 -# define ESPDATE_HAS_SNTP_SYNC_INTERVAL 1 -# elif __has_include() -# include -# define ESPDATE_HAS_CONFIG_TZ_TIME 1 -# define ESPDATE_HAS_SNTP_NOTIFICATION_CB 0 -# define ESPDATE_HAS_SNTP_SYNC_INTERVAL 0 -# else -# define ESPDATE_HAS_CONFIG_TZ_TIME 0 -# define ESPDATE_HAS_SNTP_NOTIFICATION_CB 0 -# define ESPDATE_HAS_SNTP_SYNC_INTERVAL 0 -# endif +#if __has_include() +#include +#define ESPDATE_HAS_CONFIG_TZ_TIME 1 +#define ESPDATE_HAS_SNTP_NOTIFICATION_CB 1 +#define ESPDATE_HAS_SNTP_SYNC_INTERVAL 1 +#elif __has_include() +#include +#define ESPDATE_HAS_CONFIG_TZ_TIME 1 +#define ESPDATE_HAS_SNTP_NOTIFICATION_CB 0 +#define ESPDATE_HAS_SNTP_SYNC_INTERVAL 0 #else -# define ESPDATE_HAS_CONFIG_TZ_TIME 0 -# define ESPDATE_HAS_SNTP_NOTIFICATION_CB 0 -# define ESPDATE_HAS_SNTP_SYNC_INTERVAL 0 +#define ESPDATE_HAS_CONFIG_TZ_TIME 0 +#define ESPDATE_HAS_SNTP_NOTIFICATION_CB 0 +#define ESPDATE_HAS_SNTP_SYNC_INTERVAL 0 +#endif +#else +#define ESPDATE_HAS_CONFIG_TZ_TIME 0 +#define ESPDATE_HAS_SNTP_NOTIFICATION_CB 0 +#define ESPDATE_HAS_SNTP_SYNC_INTERVAL 0 #endif #if defined(__SIZEOF_TIME_T__) && __SIZEOF_TIME_T__ < 8 @@ -35,999 +35,1046 @@ using Utils = ESPDateUtils; namespace { -const char* patternForStyle(ESPDateFormat style, bool localIso8601) { - switch (style) { - case ESPDateFormat::Iso8601: - return localIso8601 ? "%Y-%m-%dT%H:%M:%S%z" : "%Y-%m-%dT%H:%M:%SZ"; - case ESPDateFormat::DateTime: - return "%Y-%m-%d %H:%M:%S"; - case ESPDateFormat::Date: - return "%Y-%m-%d"; - case ESPDateFormat::Time: - return "%H:%M:%S"; - } - return "%Y-%m-%d %H:%M:%S"; -} - -bool formatWithTm(const tm& value, const char* pattern, char* outBuffer, size_t outSize) { - if (!pattern || !outBuffer || outSize == 0) { - return false; - } - tm copy = value; - const size_t written = strftime(outBuffer, outSize, pattern, ©); - return written > 0; -} -} // namespace +const char *patternForStyle(ESPDateFormat style, bool localIso8601) { + switch (style) { + case ESPDateFormat::Iso8601: + return localIso8601 ? "%Y-%m-%dT%H:%M:%S%z" : "%Y-%m-%dT%H:%M:%SZ"; + case ESPDateFormat::DateTime: + return "%Y-%m-%d %H:%M:%S"; + case ESPDateFormat::Date: + return "%Y-%m-%d"; + case ESPDateFormat::Time: + return "%H:%M:%S"; + } + return "%Y-%m-%d %H:%M:%S"; +} + +bool formatWithTm(const tm &value, const char *pattern, char *outBuffer, size_t outSize) { + if (!pattern || !outBuffer || outSize == 0) { + return false; + } + tm copy = value; + const size_t written = strftime(outBuffer, outSize, pattern, ©); + return written > 0; +} +} // namespace ESPDate::NtpSyncCallback ESPDate::activeNtpSyncCallback_ = nullptr; ESPDate::NtpSyncCallable ESPDate::activeNtpSyncCallbackCallable_{}; -ESPDate* ESPDate::activeNtpSyncOwner_ = nullptr; +ESPDate *ESPDate::activeNtpSyncOwner_ = nullptr; #if ESPDATE_HAS_SNTP_NOTIFICATION_CB -void ESPDate::handleSntpSync(struct timeval* tv) { - int64_t syncedEpoch = static_cast(time(nullptr)); - if (tv) { - syncedEpoch = static_cast(tv->tv_sec); - } - const DateTime syncedAtUtc{syncedEpoch}; - if (activeNtpSyncOwner_) { - activeNtpSyncOwner_->lastNtpSync_ = syncedAtUtc; - activeNtpSyncOwner_->hasLastNtpSync_ = true; - } - - if (activeNtpSyncCallbackCallable_) { - activeNtpSyncCallbackCallable_(syncedAtUtc); - return; - } - if (activeNtpSyncCallback_) { - activeNtpSyncCallback_(syncedAtUtc); - } +void ESPDate::handleSntpSync(struct timeval *tv) { + int64_t syncedEpoch = static_cast(time(nullptr)); + if (tv) { + syncedEpoch = static_cast(tv->tv_sec); + } + const DateTime syncedAtUtc{syncedEpoch}; + if (activeNtpSyncOwner_) { + activeNtpSyncOwner_->lastNtpSync_ = syncedAtUtc; + activeNtpSyncOwner_->hasLastNtpSync_ = true; + } + + if (activeNtpSyncCallbackCallable_) { + activeNtpSyncCallbackCallable_(syncedAtUtc); + return; + } + if (activeNtpSyncCallback_) { + activeNtpSyncCallback_(syncedAtUtc); + } } #endif int DateTime::yearUtc() const { - tm t{}; - if (!Utils::toUtcTm(*this, t)) { - return 0; - } - return t.tm_year + 1900; + tm t{}; + if (!Utils::toUtcTm(*this, t)) { + return 0; + } + return t.tm_year + 1900; } int DateTime::monthUtc() const { - tm t{}; - if (!Utils::toUtcTm(*this, t)) { - return 0; - } - return t.tm_mon + 1; + tm t{}; + if (!Utils::toUtcTm(*this, t)) { + return 0; + } + return t.tm_mon + 1; } int DateTime::dayUtc() const { - tm t{}; - if (!Utils::toUtcTm(*this, t)) { - return 0; - } - return t.tm_mday; + tm t{}; + if (!Utils::toUtcTm(*this, t)) { + return 0; + } + return t.tm_mday; } int DateTime::hourUtc() const { - tm t{}; - if (!Utils::toUtcTm(*this, t)) { - return 0; - } - return t.tm_hour; + tm t{}; + if (!Utils::toUtcTm(*this, t)) { + return 0; + } + return t.tm_hour; } int DateTime::minuteUtc() const { - tm t{}; - if (!Utils::toUtcTm(*this, t)) { - return 0; - } - return t.tm_min; + tm t{}; + if (!Utils::toUtcTm(*this, t)) { + return 0; + } + return t.tm_min; } int DateTime::secondUtc() const { - tm t{}; - if (!Utils::toUtcTm(*this, t)) { - return 0; - } - return t.tm_sec; + tm t{}; + if (!Utils::toUtcTm(*this, t)) { + return 0; + } + return t.tm_sec; } -bool DateTime::utcString(char* outBuffer, size_t outSize, ESPDateFormat style) const { - tm t{}; - if (!Utils::toUtcTm(*this, t)) { - return false; - } - return formatWithTm(t, patternForStyle(style, false), outBuffer, outSize); +bool DateTime::utcString(char *outBuffer, size_t outSize, ESPDateFormat style) const { + tm t{}; + if (!Utils::toUtcTm(*this, t)) { + return false; + } + return formatWithTm(t, patternForStyle(style, false), outBuffer, outSize); } -bool DateTime::localString(char* outBuffer, size_t outSize, ESPDateFormat style) const { - tm t{}; - if (!Utils::toLocalTm(*this, t)) { - return false; - } - return formatWithTm(t, patternForStyle(style, true), outBuffer, outSize); +bool DateTime::localString(char *outBuffer, size_t outSize, ESPDateFormat style) const { + tm t{}; + if (!Utils::toLocalTm(*this, t)) { + return false; + } + return formatWithTm(t, patternForStyle(style, true), outBuffer, outSize); } std::string DateTime::utcString(ESPDateFormat style) const { - char buffer[40]; - if (!utcString(buffer, sizeof(buffer), style)) { - return std::string(); - } - return std::string(buffer); + char buffer[40]; + if (!utcString(buffer, sizeof(buffer), style)) { + return std::string(); + } + return std::string(buffer); } std::string DateTime::localString(ESPDateFormat style) const { - char buffer[48]; - if (!localString(buffer, sizeof(buffer), style)) { - return std::string(); - } - return std::string(buffer); -} - -bool LocalDateTime::localString(char* outBuffer, size_t outSize) const { - if (!ok || !outBuffer || outSize == 0) { - return false; - } - const int written = - std::snprintf(outBuffer, outSize, "%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second); - return written > 0 && static_cast(written) < outSize; + char buffer[48]; + if (!localString(buffer, sizeof(buffer), style)) { + return std::string(); + } + return std::string(buffer); +} + +bool LocalDateTime::localString(char *outBuffer, size_t outSize) const { + if (!ok || !outBuffer || outSize == 0) { + return false; + } + const int written = std::snprintf( + outBuffer, + outSize, + "%04d-%02d-%02d %02d:%02d:%02d", + year, + month, + day, + hour, + minute, + second + ); + return written > 0 && static_cast(written) < outSize; } std::string LocalDateTime::localString() const { - char buffer[32]; - if (!localString(buffer, sizeof(buffer))) { - return std::string(); - } - return std::string(buffer); + char buffer[32]; + if (!localString(buffer, sizeof(buffer))) { + return std::string(); + } + return std::string(buffer); } ESPDate::ESPDate() = default; ESPDate::~ESPDate() { - deinit(); + deinit(); } void ESPDate::deinit() { - ntpSyncCallback_ = nullptr; - ntpSyncCallbackCallable_ = NtpSyncCallable{}; - hasLastNtpSync_ = false; - lastNtpSync_ = DateTime{}; - hasLocation_ = false; - latitude_ = 0.0f; - longitude_ = 0.0f; - ntpSyncIntervalMs_ = 0; - const bool usePSRAM = usePSRAMBuffers_; - timeZone_ = DateString(DateAllocator(usePSRAM)); - for (size_t i = 0; i < kMaxNtpServers; ++i) { - ntpServers_[i] = DateString(DateAllocator(usePSRAM)); - } - usePSRAMBuffers_ = false; - initialized_ = false; - - if (activeNtpSyncOwner_ == this) { - activeNtpSyncOwner_ = nullptr; - activeNtpSyncCallback_ = nullptr; - activeNtpSyncCallbackCallable_ = NtpSyncCallable{}; + ntpSyncCallback_ = nullptr; + ntpSyncCallbackCallable_ = NtpSyncCallable{}; + hasLastNtpSync_ = false; + lastNtpSync_ = DateTime{}; + hasLocation_ = false; + latitude_ = 0.0f; + longitude_ = 0.0f; + ntpSyncIntervalMs_ = 0; + const bool usePSRAM = usePSRAMBuffers_; + timeZone_ = DateString(DateAllocator(usePSRAM)); + for (size_t i = 0; i < kMaxNtpServers; ++i) { + ntpServers_[i] = DateString(DateAllocator(usePSRAM)); + } + usePSRAMBuffers_ = false; + initialized_ = false; + + if (activeNtpSyncOwner_ == this) { + activeNtpSyncOwner_ = nullptr; + activeNtpSyncCallback_ = nullptr; + activeNtpSyncCallbackCallable_ = NtpSyncCallable{}; #if ESPDATE_HAS_SNTP_NOTIFICATION_CB - sntp_set_time_sync_notification_cb(nullptr); + sntp_set_time_sync_notification_cb(nullptr); #endif - } -} - -void ESPDate::init(const ESPDateConfig& config) { - latitude_ = config.latitude; - longitude_ = config.longitude; - hasLocation_ = true; - usePSRAMBuffers_ = config.usePSRAMBuffers; - timeZone_ = DateString(DateAllocator(usePSRAMBuffers_)); - for (size_t i = 0; i < kMaxNtpServers; ++i) { - ntpServers_[i] = DateString(DateAllocator(usePSRAMBuffers_)); - } - ntpSyncIntervalMs_ = config.ntpSyncIntervalMs; - hasLastNtpSync_ = false; - lastNtpSync_ = DateTime{}; - - const bool hasTz = config.timeZone && config.timeZone[0] != '\0'; - const char* configuredNtpServers[kMaxNtpServers] = {config.ntpServer, config.ntpServer2, config.ntpServer3}; - size_t ntpServerCount = 0; - if (hasTz) { - timeZone_ = config.timeZone; - } - for (size_t i = 0; i < kMaxNtpServers; ++i) { - const char* server = configuredNtpServers[i]; - if (!server || server[0] == '\0') { - continue; - } - ntpServers_[ntpServerCount++] = server; - } - - if (!applyNtpConfig() && hasTz) { - setenv("TZ", timeZone_.c_str(), 1); - tzset(); - } - initialized_ = true; + } +} + +void ESPDate::init(const ESPDateConfig &config) { + latitude_ = config.latitude; + longitude_ = config.longitude; + hasLocation_ = true; + usePSRAMBuffers_ = config.usePSRAMBuffers; + timeZone_ = DateString(DateAllocator(usePSRAMBuffers_)); + for (size_t i = 0; i < kMaxNtpServers; ++i) { + ntpServers_[i] = DateString(DateAllocator(usePSRAMBuffers_)); + } + ntpSyncIntervalMs_ = config.ntpSyncIntervalMs; + hasLastNtpSync_ = false; + lastNtpSync_ = DateTime{}; + + const bool hasTz = config.timeZone && config.timeZone[0] != '\0'; + const char *configuredNtpServers[kMaxNtpServers] = + {config.ntpServer, config.ntpServer2, config.ntpServer3}; + size_t ntpServerCount = 0; + if (hasTz) { + timeZone_ = config.timeZone; + } + for (size_t i = 0; i < kMaxNtpServers; ++i) { + const char *server = configuredNtpServers[i]; + if (!server || server[0] == '\0') { + continue; + } + ntpServers_[ntpServerCount++] = server; + } + + if (!applyNtpConfig() && hasTz) { + setenv("TZ", timeZone_.c_str(), 1); + tzset(); + } + initialized_ = true; } void ESPDate::setNtpSyncCallback(NtpSyncCallback callback) { - activeNtpSyncOwner_ = this; - ntpSyncCallback_ = callback; - ntpSyncCallbackCallable_ = NtpSyncCallable{}; - activeNtpSyncCallback_ = callback; - activeNtpSyncCallbackCallable_ = NtpSyncCallable{}; + activeNtpSyncOwner_ = this; + ntpSyncCallback_ = callback; + ntpSyncCallbackCallable_ = NtpSyncCallable{}; + activeNtpSyncCallback_ = callback; + activeNtpSyncCallbackCallable_ = NtpSyncCallable{}; #if ESPDATE_HAS_SNTP_NOTIFICATION_CB - const bool keepTrackingEnabled = hasAnyNtpServerConfigured(); - sntp_set_time_sync_notification_cb((callback || keepTrackingEnabled) ? &ESPDate::handleSntpSync : nullptr); + const bool keepTrackingEnabled = hasAnyNtpServerConfigured(); + sntp_set_time_sync_notification_cb( + (callback || keepTrackingEnabled) ? &ESPDate::handleSntpSync : nullptr + ); #endif } -void ESPDate::setNtpSyncCallbackCallable(const NtpSyncCallable& callback) { - activeNtpSyncOwner_ = this; - ntpSyncCallback_ = nullptr; - ntpSyncCallbackCallable_ = callback; - activeNtpSyncCallback_ = nullptr; - activeNtpSyncCallbackCallable_ = callback; +void ESPDate::setNtpSyncCallbackCallable(const NtpSyncCallable &callback) { + activeNtpSyncOwner_ = this; + ntpSyncCallback_ = nullptr; + ntpSyncCallbackCallable_ = callback; + activeNtpSyncCallback_ = nullptr; + activeNtpSyncCallbackCallable_ = callback; #if ESPDATE_HAS_SNTP_NOTIFICATION_CB - const bool keepTrackingEnabled = hasAnyNtpServerConfigured(); - sntp_set_time_sync_notification_cb((static_cast(callback) || keepTrackingEnabled) ? &ESPDate::handleSntpSync - : nullptr); + const bool keepTrackingEnabled = hasAnyNtpServerConfigured(); + sntp_set_time_sync_notification_cb( + (static_cast(callback) || keepTrackingEnabled) ? &ESPDate::handleSntpSync : nullptr + ); #endif } bool ESPDate::setNtpSyncIntervalMs(uint32_t intervalMs) { - ntpSyncIntervalMs_ = intervalMs; + ntpSyncIntervalMs_ = intervalMs; #if ESPDATE_HAS_SNTP_SYNC_INTERVAL - if (intervalMs > 0) { - sntp_set_sync_interval(intervalMs); - } - return true; + if (intervalMs > 0) { + sntp_set_sync_interval(intervalMs); + } + return true; #else - return intervalMs == 0; + return intervalMs == 0; #endif } bool ESPDate::hasLastNtpSync() const { - return hasLastNtpSync_; + return hasLastNtpSync_; } DateTime ESPDate::lastNtpSync() const { - return lastNtpSync_; + return lastNtpSync_; } bool ESPDate::syncNTP() { - return applyNtpConfig(); + return applyNtpConfig(); } bool ESPDate::hasAnyNtpServerConfigured() const { - for (size_t i = 0; i < kMaxNtpServers; ++i) { - if (!ntpServers_[i].empty()) { - return true; - } - } - return false; + for (size_t i = 0; i < kMaxNtpServers; ++i) { + if (!ntpServers_[i].empty()) { + return true; + } + } + return false; } bool ESPDate::applyNtpConfig() const { #if ESPDATE_HAS_CONFIG_TZ_TIME - if (!hasAnyNtpServerConfigured()) { - return false; - } - activeNtpSyncOwner_ = const_cast(this); + if (!hasAnyNtpServerConfigured()) { + return false; + } + activeNtpSyncOwner_ = const_cast(this); #if ESPDATE_HAS_SNTP_NOTIFICATION_CB - activeNtpSyncCallback_ = ntpSyncCallback_; - activeNtpSyncCallbackCallable_ = ntpSyncCallbackCallable_; - const bool hasCallback = (ntpSyncCallback_ != nullptr) || static_cast(ntpSyncCallbackCallable_); - sntp_set_time_sync_notification_cb((hasCallback || activeNtpSyncOwner_ != nullptr) ? &ESPDate::handleSntpSync - : nullptr); + activeNtpSyncCallback_ = ntpSyncCallback_; + activeNtpSyncCallbackCallable_ = ntpSyncCallbackCallable_; + const bool hasCallback = + (ntpSyncCallback_ != nullptr) || static_cast(ntpSyncCallbackCallable_); + sntp_set_time_sync_notification_cb( + (hasCallback || activeNtpSyncOwner_ != nullptr) ? &ESPDate::handleSntpSync : nullptr + ); #endif #if ESPDATE_HAS_SNTP_SYNC_INTERVAL - if (ntpSyncIntervalMs_ > 0) { - sntp_set_sync_interval(ntpSyncIntervalMs_); - } + if (ntpSyncIntervalMs_ > 0) { + sntp_set_sync_interval(ntpSyncIntervalMs_); + } #endif - const char* tz = timeZone_.empty() ? "UTC0" : timeZone_.c_str(); - const char* ntpServer1 = ntpServers_[0].empty() ? nullptr : ntpServers_[0].c_str(); - const char* ntpServer2 = ntpServers_[1].empty() ? nullptr : ntpServers_[1].c_str(); - const char* ntpServer3 = ntpServers_[2].empty() ? nullptr : ntpServers_[2].c_str(); - configTzTime(tz, ntpServer1, ntpServer2, ntpServer3); - return true; + const char *tz = timeZone_.empty() ? "UTC0" : timeZone_.c_str(); + const char *ntpServer1 = ntpServers_[0].empty() ? nullptr : ntpServers_[0].c_str(); + const char *ntpServer2 = ntpServers_[1].empty() ? nullptr : ntpServers_[1].c_str(); + const char *ntpServer3 = ntpServers_[2].empty() ? nullptr : ntpServers_[2].c_str(); + configTzTime(tz, ntpServer1, ntpServer2, ntpServer3); + return true; #else - return false; + return false; #endif } DateTime ESPDate::now() const { - return DateTime{static_cast(time(nullptr))}; + return DateTime{static_cast(time(nullptr))}; } DateTime ESPDate::nowUtc() const { - return now(); + return now(); } LocalDateTime ESPDate::nowLocal() const { - return toLocal(now(), nullptr); + return toLocal(now(), nullptr); } -LocalDateTime ESPDate::toLocal(const DateTime& dt) const { - return toLocal(dt, nullptr); +LocalDateTime ESPDate::toLocal(const DateTime &dt) const { + return toLocal(dt, nullptr); } -LocalDateTime ESPDate::toLocal(const DateTime& dt, const char* timeZone) const { - LocalDateTime result{}; - const char* tz = timeZone; - if (!tz || tz[0] == '\0') { - tz = timeZone_.empty() ? nullptr : timeZone_.c_str(); - } +LocalDateTime ESPDate::toLocal(const DateTime &dt, const char *timeZone) const { + LocalDateTime result{}; + const char *tz = timeZone; + if (!tz || tz[0] == '\0') { + tz = timeZone_.empty() ? nullptr : timeZone_.c_str(); + } - Utils::ScopedTz scoped(tz, usePSRAMBuffers_); - time_t raw = static_cast(dt.epochSeconds); - tm local{}; - if (localtime_r(&raw, &local) == nullptr) { - return result; - } + Utils::ScopedTz scoped(tz, usePSRAMBuffers_); + time_t raw = static_cast(dt.epochSeconds); + tm local{}; + if (localtime_r(&raw, &local) == nullptr) { + return result; + } - const int offsetSeconds = static_cast(Utils::timegm64(local) - static_cast(raw)); - result.ok = true; - result.year = local.tm_year + 1900; - result.month = local.tm_mon + 1; - result.day = local.tm_mday; - result.hour = local.tm_hour; - result.minute = local.tm_min; - result.second = local.tm_sec; - result.offsetMinutes = offsetSeconds / 60; - result.utc = dt; - return result; + const int offsetSeconds = static_cast(Utils::timegm64(local) - static_cast(raw)); + result.ok = true; + result.year = local.tm_year + 1900; + result.month = local.tm_mon + 1; + result.day = local.tm_mday; + result.hour = local.tm_hour; + result.minute = local.tm_min; + result.second = local.tm_sec; + result.offsetMinutes = offsetSeconds / 60; + result.utc = dt; + return result; } DateTime ESPDate::fromUnixSeconds(int64_t seconds) const { - return DateTime{seconds}; + return DateTime{seconds}; } DateTime ESPDate::fromUtc(int year, int month, int day, int hour, int minute, int second) const { - if (!Utils::validHms(hour, minute, second) || month < 1 || month > 12 || year < 0 || year > 9999) { - return DateTime{}; - } - const int clampedDay = Utils::clampDay(year, month, day, *this); - tm t{}; - t.tm_year = year - 1900; - t.tm_mon = month - 1; - t.tm_mday = clampedDay; - t.tm_hour = hour; - t.tm_min = minute; - t.tm_sec = second; - t.tm_isdst = 0; - return Utils::fromUtcTm(t); + if (!Utils::validHms(hour, minute, second) || month < 1 || month > 12 || year < 0 || + year > 9999) { + return DateTime{}; + } + const int clampedDay = Utils::clampDay(year, month, day, *this); + tm t{}; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = clampedDay; + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = second; + t.tm_isdst = 0; + return Utils::fromUtcTm(t); } DateTime ESPDate::fromLocal(int year, int month, int day, int hour, int minute, int second) const { - if (!Utils::validHms(hour, minute, second) || month < 1 || month > 12 || year < 0 || year > 9999) { - return DateTime{}; - } - const int clampedDay = Utils::clampDay(year, month, day, *this); - tm t{}; - t.tm_year = year - 1900; - t.tm_mon = month - 1; - t.tm_mday = clampedDay; - t.tm_hour = hour; - t.tm_min = minute; - t.tm_sec = second; - t.tm_isdst = -1; // let the runtime figure DST - return Utils::fromLocalTm(t); -} - -int64_t ESPDate::toUnixSeconds(const DateTime& dt) const { - return dt.epochSeconds; + if (!Utils::validHms(hour, minute, second) || month < 1 || month > 12 || year < 0 || + year > 9999) { + return DateTime{}; + } + const int clampedDay = Utils::clampDay(year, month, day, *this); + tm t{}; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = clampedDay; + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = second; + t.tm_isdst = -1; // let the runtime figure DST + return Utils::fromLocalTm(t); +} + +int64_t ESPDate::toUnixSeconds(const DateTime &dt) const { + return dt.epochSeconds; } bool ESPDate::isDstActive() const { - return isDstActive(now()); + return isDstActive(now()); } -bool ESPDate::isDstActive(const DateTime& dt) const { - return isDstActive(dt, nullptr); +bool ESPDate::isDstActive(const DateTime &dt) const { + return isDstActive(dt, nullptr); } -bool ESPDate::isDstActive(const char* timeZone) const { - return isDstActive(now(), timeZone); +bool ESPDate::isDstActive(const char *timeZone) const { + return isDstActive(now(), timeZone); } -bool ESPDate::isDstActive(const DateTime& dt, const char* timeZone) const { - const char* tz = timeZone; - if (!tz || tz[0] == '\0') { - if (!timeZone_.empty()) { - tz = timeZone_.c_str(); - } else { - tz = nullptr; - } - } - return Utils::isDstActiveFor(dt, tz, usePSRAMBuffers_); +bool ESPDate::isDstActive(const DateTime &dt, const char *timeZone) const { + const char *tz = timeZone; + if (!tz || tz[0] == '\0') { + if (!timeZone_.empty()) { + tz = timeZone_.c_str(); + } else { + tz = nullptr; + } + } + return Utils::isDstActiveFor(dt, tz, usePSRAMBuffers_); } -DateTime ESPDate::addSeconds(const DateTime& dt, int64_t seconds) const { - return DateTime{dt.epochSeconds + seconds}; +DateTime ESPDate::addSeconds(const DateTime &dt, int64_t seconds) const { + return DateTime{dt.epochSeconds + seconds}; } -DateTime ESPDate::addMinutes(const DateTime& dt, int64_t minutes) const { - return addSeconds(dt, minutes * Utils::kSecondsPerMinute); +DateTime ESPDate::addMinutes(const DateTime &dt, int64_t minutes) const { + return addSeconds(dt, minutes * Utils::kSecondsPerMinute); } -DateTime ESPDate::addHours(const DateTime& dt, int64_t hours) const { - return addSeconds(dt, hours * Utils::kSecondsPerHour); +DateTime ESPDate::addHours(const DateTime &dt, int64_t hours) const { + return addSeconds(dt, hours * Utils::kSecondsPerHour); } -DateTime ESPDate::addDays(const DateTime& dt, int32_t days) const { - return addSeconds(dt, static_cast(days) * Utils::kSecondsPerDay); +DateTime ESPDate::addDays(const DateTime &dt, int32_t days) const { + return addSeconds(dt, static_cast(days) * Utils::kSecondsPerDay); } -DateTime ESPDate::addMonths(const DateTime& dt, int32_t months) const { - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return dt; - } +DateTime ESPDate::addMonths(const DateTime &dt, int32_t months) const { + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return dt; + } - int totalMonths = t.tm_mon + months; - int yearsDelta = totalMonths / 12; - int newMonth = totalMonths % 12; - if (newMonth < 0) { - newMonth += 12; - --yearsDelta; - } + int totalMonths = t.tm_mon + months; + int yearsDelta = totalMonths / 12; + int newMonth = totalMonths % 12; + if (newMonth < 0) { + newMonth += 12; + --yearsDelta; + } - t.tm_year += yearsDelta; - t.tm_mon = newMonth; - t.tm_mday = Utils::clampDay(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, *this); + t.tm_year += yearsDelta; + t.tm_mon = newMonth; + t.tm_mday = Utils::clampDay(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, *this); - return Utils::fromUtcTm(t); + return Utils::fromUtcTm(t); } -DateTime ESPDate::addYears(const DateTime& dt, int32_t years) const { - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return dt; - } - t.tm_year += years; - t.tm_mday = Utils::clampDay(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, *this); - return Utils::fromUtcTm(t); +DateTime ESPDate::addYears(const DateTime &dt, int32_t years) const { + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return dt; + } + t.tm_year += years; + t.tm_mday = Utils::clampDay(t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, *this); + return Utils::fromUtcTm(t); } -DateTime ESPDate::subSeconds(const DateTime& dt, int64_t seconds) const { - return addSeconds(dt, -seconds); +DateTime ESPDate::subSeconds(const DateTime &dt, int64_t seconds) const { + return addSeconds(dt, -seconds); } -DateTime ESPDate::subMinutes(const DateTime& dt, int64_t minutes) const { - return addMinutes(dt, -minutes); +DateTime ESPDate::subMinutes(const DateTime &dt, int64_t minutes) const { + return addMinutes(dt, -minutes); } -DateTime ESPDate::subHours(const DateTime& dt, int64_t hours) const { - return addHours(dt, -hours); +DateTime ESPDate::subHours(const DateTime &dt, int64_t hours) const { + return addHours(dt, -hours); } -DateTime ESPDate::subDays(const DateTime& dt, int32_t days) const { - return addDays(dt, -days); +DateTime ESPDate::subDays(const DateTime &dt, int32_t days) const { + return addDays(dt, -days); } -DateTime ESPDate::subMonths(const DateTime& dt, int32_t months) const { - return addMonths(dt, -months); +DateTime ESPDate::subMonths(const DateTime &dt, int32_t months) const { + return addMonths(dt, -months); } -DateTime ESPDate::subYears(const DateTime& dt, int32_t years) const { - return addYears(dt, -years); +DateTime ESPDate::subYears(const DateTime &dt, int32_t years) const { + return addYears(dt, -years); } DateTime ESPDate::addSeconds(int64_t seconds) const { - return addSeconds(now(), seconds); + return addSeconds(now(), seconds); } DateTime ESPDate::addMinutes(int64_t minutes) const { - return addMinutes(now(), minutes); + return addMinutes(now(), minutes); } DateTime ESPDate::addHours(int64_t hours) const { - return addHours(now(), hours); + return addHours(now(), hours); } DateTime ESPDate::addDays(int32_t days) const { - return addDays(now(), days); + return addDays(now(), days); } DateTime ESPDate::addMonths(int32_t months) const { - return addMonths(now(), months); + return addMonths(now(), months); } DateTime ESPDate::addYears(int32_t years) const { - return addYears(now(), years); + return addYears(now(), years); } DateTime ESPDate::subSeconds(int64_t seconds) const { - return subSeconds(now(), seconds); + return subSeconds(now(), seconds); } DateTime ESPDate::subMinutes(int64_t minutes) const { - return subMinutes(now(), minutes); + return subMinutes(now(), minutes); } DateTime ESPDate::subHours(int64_t hours) const { - return subHours(now(), hours); + return subHours(now(), hours); } DateTime ESPDate::subDays(int32_t days) const { - return subDays(now(), days); + return subDays(now(), days); } DateTime ESPDate::subMonths(int32_t months) const { - return subMonths(now(), months); + return subMonths(now(), months); } DateTime ESPDate::subYears(int32_t years) const { - return subYears(now(), years); + return subYears(now(), years); } -int64_t ESPDate::differenceInSeconds(const DateTime& a, const DateTime& b) const { - return a.epochSeconds - b.epochSeconds; +int64_t ESPDate::differenceInSeconds(const DateTime &a, const DateTime &b) const { + return a.epochSeconds - b.epochSeconds; } -int64_t ESPDate::differenceInMinutes(const DateTime& a, const DateTime& b) const { - return differenceInSeconds(a, b) / Utils::kSecondsPerMinute; +int64_t ESPDate::differenceInMinutes(const DateTime &a, const DateTime &b) const { + return differenceInSeconds(a, b) / Utils::kSecondsPerMinute; } -int64_t ESPDate::differenceInHours(const DateTime& a, const DateTime& b) const { - return differenceInSeconds(a, b) / Utils::kSecondsPerHour; +int64_t ESPDate::differenceInHours(const DateTime &a, const DateTime &b) const { + return differenceInSeconds(a, b) / Utils::kSecondsPerHour; } -int64_t ESPDate::differenceInDays(const DateTime& a, const DateTime& b) const { - return differenceInSeconds(a, b) / Utils::kSecondsPerDay; +int64_t ESPDate::differenceInDays(const DateTime &a, const DateTime &b) const { + return differenceInSeconds(a, b) / Utils::kSecondsPerDay; } -bool ESPDate::isBefore(const DateTime& a, const DateTime& b) const { - return a.epochSeconds < b.epochSeconds; +bool ESPDate::isBefore(const DateTime &a, const DateTime &b) const { + return a.epochSeconds < b.epochSeconds; } -bool ESPDate::isAfter(const DateTime& a, const DateTime& b) const { - return a.epochSeconds > b.epochSeconds; +bool ESPDate::isAfter(const DateTime &a, const DateTime &b) const { + return a.epochSeconds > b.epochSeconds; } -bool ESPDate::isEqual(const DateTime& a, const DateTime& b) const { - return a.epochSeconds == b.epochSeconds; +bool ESPDate::isEqual(const DateTime &a, const DateTime &b) const { + return a.epochSeconds == b.epochSeconds; } -bool ESPDate::isEqualMinutes(const DateTime& a, const DateTime& b) const { - return (a.epochSeconds / Utils::kSecondsPerMinute) == (b.epochSeconds / Utils::kSecondsPerMinute); +bool ESPDate::isEqualMinutes(const DateTime &a, const DateTime &b) const { + return (a.epochSeconds / Utils::kSecondsPerMinute) == + (b.epochSeconds / Utils::kSecondsPerMinute); } -bool ESPDate::isEqualMinutesUtc(const DateTime& a, const DateTime& b) const { - return isEqualMinutes(a, b); +bool ESPDate::isEqualMinutesUtc(const DateTime &a, const DateTime &b) const { + return isEqualMinutes(a, b); } -bool ESPDate::isSameDay(const DateTime& a, const DateTime& b) const { - return isEqual(startOfDayUtc(a), startOfDayUtc(b)); +bool ESPDate::isSameDay(const DateTime &a, const DateTime &b) const { + return isEqual(startOfDayUtc(a), startOfDayUtc(b)); } -DateTime ESPDate::startOfDayUtc(const DateTime& dt) const { - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return dt; - } - t.tm_hour = 0; - t.tm_min = 0; - t.tm_sec = 0; - return Utils::fromUtcTm(t); +DateTime ESPDate::startOfDayUtc(const DateTime &dt) const { + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return dt; + } + t.tm_hour = 0; + t.tm_min = 0; + t.tm_sec = 0; + return Utils::fromUtcTm(t); } -DateTime ESPDate::endOfDayUtc(const DateTime& dt) const { - return addSeconds(startOfDayUtc(dt), Utils::kSecondsPerDay - 1); +DateTime ESPDate::endOfDayUtc(const DateTime &dt) const { + return addSeconds(startOfDayUtc(dt), Utils::kSecondsPerDay - 1); } -DateTime ESPDate::startOfMonthUtc(const DateTime& dt) const { - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return dt; - } - t.tm_mday = 1; - t.tm_hour = 0; - t.tm_min = 0; - t.tm_sec = 0; - return Utils::fromUtcTm(t); +DateTime ESPDate::startOfMonthUtc(const DateTime &dt) const { + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return dt; + } + t.tm_mday = 1; + t.tm_hour = 0; + t.tm_min = 0; + t.tm_sec = 0; + return Utils::fromUtcTm(t); } -DateTime ESPDate::endOfMonthUtc(const DateTime& dt) const { - DateTime start = startOfMonthUtc(dt); - DateTime nextMonth = addMonths(start, 1); - return subSeconds(nextMonth, 1); +DateTime ESPDate::endOfMonthUtc(const DateTime &dt) const { + DateTime start = startOfMonthUtc(dt); + DateTime nextMonth = addMonths(start, 1); + return subSeconds(nextMonth, 1); } -int ESPDate::getYearUtc(const DateTime& dt) const { - return dt.yearUtc(); +int ESPDate::getYearUtc(const DateTime &dt) const { + return dt.yearUtc(); } -int ESPDate::getMonthUtc(const DateTime& dt) const { - return dt.monthUtc(); +int ESPDate::getMonthUtc(const DateTime &dt) const { + return dt.monthUtc(); } -int ESPDate::getDayUtc(const DateTime& dt) const { - return dt.dayUtc(); +int ESPDate::getDayUtc(const DateTime &dt) const { + return dt.dayUtc(); } -int ESPDate::getWeekdayUtc(const DateTime& dt) const { - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return 0; - } - return t.tm_wday; +int ESPDate::getWeekdayUtc(const DateTime &dt) const { + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return 0; + } + return t.tm_wday; } -DateTime ESPDate::startOfDayLocal(const DateTime& dt) const { - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return dt; - } - t.tm_hour = 0; - t.tm_min = 0; - t.tm_sec = 0; - return Utils::fromLocalTm(t); +DateTime ESPDate::startOfDayLocal(const DateTime &dt) const { + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return dt; + } + t.tm_hour = 0; + t.tm_min = 0; + t.tm_sec = 0; + return Utils::fromLocalTm(t); } -DateTime ESPDate::endOfDayLocal(const DateTime& dt) const { - return addSeconds(startOfDayLocal(dt), Utils::kSecondsPerDay - 1); +DateTime ESPDate::endOfDayLocal(const DateTime &dt) const { + return addSeconds(startOfDayLocal(dt), Utils::kSecondsPerDay - 1); } -DateTime ESPDate::startOfMonthLocal(const DateTime& dt) const { - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return dt; - } - t.tm_mday = 1; - t.tm_hour = 0; - t.tm_min = 0; - t.tm_sec = 0; - return Utils::fromLocalTm(t); -} - -DateTime ESPDate::endOfMonthLocal(const DateTime& dt) const { - DateTime start = startOfMonthLocal(dt); - tm t{}; - if (!Utils::toLocalTm(start, t)) { - return start; - } - t.tm_mon += 1; - DateTime nextMonth = Utils::fromLocalTm(t); - return subSeconds(nextMonth, 1); -} - -DateTime ESPDate::startOfYearUtc(const DateTime& dt) const { - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return dt; - } - t.tm_mon = 0; - t.tm_mday = 1; - t.tm_hour = 0; - t.tm_min = 0; - t.tm_sec = 0; - return Utils::fromUtcTm(t); -} - -DateTime ESPDate::startOfYearLocal(const DateTime& dt) const { - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return dt; - } - t.tm_mon = 0; - t.tm_mday = 1; - t.tm_hour = 0; - t.tm_min = 0; - t.tm_sec = 0; - return Utils::fromLocalTm(t); -} - -DateTime ESPDate::setTimeOfDayLocal(const DateTime& dt, int hour, int minute, int second) const { - if (!Utils::validHms(hour, minute, second)) { - return dt; - } - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return dt; - } - t.tm_hour = hour; - t.tm_min = minute; - t.tm_sec = second; - return Utils::fromLocalTm(t); -} - -DateTime ESPDate::setTimeOfDayUtc(const DateTime& dt, int hour, int minute, int second) const { - if (!Utils::validHms(hour, minute, second)) { - return dt; - } - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return dt; - } - t.tm_hour = hour; - t.tm_min = minute; - t.tm_sec = second; - return Utils::fromUtcTm(t); -} - -DateTime ESPDate::nextDailyAtLocal(int hour, int minute, int second, const DateTime& from) const { - if (!Utils::validHms(hour, minute, second)) { - return from; - } - DateTime candidate = setTimeOfDayLocal(from, hour, minute, second); - if (!isAfter(from, candidate)) { - return candidate; - } - DateTime nextDay = addDays(from, 1); - return setTimeOfDayLocal(nextDay, hour, minute, second); -} - -DateTime ESPDate::nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime& from) const { - if (!Utils::validHms(hour, minute, second) || weekday < 0 || weekday > 6) { - return from; - } - const int current = getWeekdayLocal(from); - int daysAhead = (weekday - current + 7) % 7; - DateTime candidateDay = addDays(from, daysAhead); - DateTime candidate = setTimeOfDayLocal(candidateDay, hour, minute, second); - if (daysAhead == 0 && isAfter(from, candidate)) { - candidate = setTimeOfDayLocal(addDays(from, 7), hour, minute, second); - } - return candidate; -} - -int ESPDate::getYearLocal(const DateTime& dt) const { - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return 0; - } - return t.tm_year + 1900; -} - -int ESPDate::getMonthLocal(const DateTime& dt) const { - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return 0; - } - return t.tm_mon + 1; -} - -int ESPDate::getDayLocal(const DateTime& dt) const { - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return 0; - } - return t.tm_mday; -} - -int ESPDate::getWeekdayLocal(const DateTime& dt) const { - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return 0; - } - return t.tm_wday; +DateTime ESPDate::startOfMonthLocal(const DateTime &dt) const { + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return dt; + } + t.tm_mday = 1; + t.tm_hour = 0; + t.tm_min = 0; + t.tm_sec = 0; + return Utils::fromLocalTm(t); +} + +DateTime ESPDate::endOfMonthLocal(const DateTime &dt) const { + DateTime start = startOfMonthLocal(dt); + tm t{}; + if (!Utils::toLocalTm(start, t)) { + return start; + } + t.tm_mon += 1; + DateTime nextMonth = Utils::fromLocalTm(t); + return subSeconds(nextMonth, 1); +} + +DateTime ESPDate::startOfYearUtc(const DateTime &dt) const { + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return dt; + } + t.tm_mon = 0; + t.tm_mday = 1; + t.tm_hour = 0; + t.tm_min = 0; + t.tm_sec = 0; + return Utils::fromUtcTm(t); +} + +DateTime ESPDate::startOfYearLocal(const DateTime &dt) const { + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return dt; + } + t.tm_mon = 0; + t.tm_mday = 1; + t.tm_hour = 0; + t.tm_min = 0; + t.tm_sec = 0; + return Utils::fromLocalTm(t); +} + +DateTime ESPDate::setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const { + if (!Utils::validHms(hour, minute, second)) { + return dt; + } + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return dt; + } + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = second; + return Utils::fromLocalTm(t); +} + +DateTime ESPDate::setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const { + if (!Utils::validHms(hour, minute, second)) { + return dt; + } + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return dt; + } + t.tm_hour = hour; + t.tm_min = minute; + t.tm_sec = second; + return Utils::fromUtcTm(t); +} + +DateTime ESPDate::nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const { + if (!Utils::validHms(hour, minute, second)) { + return from; + } + DateTime candidate = setTimeOfDayLocal(from, hour, minute, second); + if (!isAfter(from, candidate)) { + return candidate; + } + DateTime nextDay = addDays(from, 1); + return setTimeOfDayLocal(nextDay, hour, minute, second); +} + +DateTime ESPDate::nextWeekdayAtLocal( + int weekday, int hour, int minute, int second, const DateTime &from +) const { + if (!Utils::validHms(hour, minute, second) || weekday < 0 || weekday > 6) { + return from; + } + const int current = getWeekdayLocal(from); + int daysAhead = (weekday - current + 7) % 7; + DateTime candidateDay = addDays(from, daysAhead); + DateTime candidate = setTimeOfDayLocal(candidateDay, hour, minute, second); + if (daysAhead == 0 && isAfter(from, candidate)) { + candidate = setTimeOfDayLocal(addDays(from, 7), hour, minute, second); + } + return candidate; +} + +int ESPDate::getYearLocal(const DateTime &dt) const { + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return 0; + } + return t.tm_year + 1900; +} + +int ESPDate::getMonthLocal(const DateTime &dt) const { + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return 0; + } + return t.tm_mon + 1; +} + +int ESPDate::getDayLocal(const DateTime &dt) const { + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return 0; + } + return t.tm_mday; +} + +int ESPDate::getWeekdayLocal(const DateTime &dt) const { + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return 0; + } + return t.tm_wday; } bool ESPDate::isLeapYear(int year) const { - if (year % 4 != 0) { - return false; - } - if (year % 100 != 0) { - return true; - } - return (year % 400) == 0; + if (year % 4 != 0) { + return false; + } + if (year % 100 != 0) { + return true; + } + return (year % 400) == 0; } int ESPDate::daysInMonth(int year, int month) const { - static const int daysPerMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; - if (month < 1 || month > 12) { - return 0; - } - if (month == 2 && isLeapYear(year)) { - return 29; - } - return daysPerMonth[month - 1]; + static const int daysPerMonth[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; + if (month < 1 || month > 12) { + return 0; + } + if (month == 2 && isLeapYear(year)) { + return 29; + } + return daysPerMonth[month - 1]; } -bool ESPDate::formatUtc(const DateTime& dt, ESPDateFormat style, char* outBuffer, size_t outSize) const { - return formatWithPatternUtc(dt, patternForStyle(style, false), outBuffer, outSize); +bool ESPDate::formatUtc( + const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize +) const { + return formatWithPatternUtc(dt, patternForStyle(style, false), outBuffer, outSize); } -bool ESPDate::formatLocal(const DateTime& dt, ESPDateFormat style, char* outBuffer, size_t outSize) const { - return formatWithPatternLocal(dt, patternForStyle(style, true), outBuffer, outSize); +bool ESPDate::formatLocal( + const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize +) const { + return formatWithPatternLocal(dt, patternForStyle(style, true), outBuffer, outSize); } -bool ESPDate::formatWithPatternUtc(const DateTime& dt, const char* pattern, char* outBuffer, size_t outSize) const { - if (!pattern || !outBuffer || outSize == 0) { - return false; - } - tm t{}; - if (!Utils::toUtcTm(dt, t)) { - return false; - } - size_t written = strftime(outBuffer, outSize, pattern, &t); - return written > 0; +bool ESPDate::formatWithPatternUtc( + const DateTime &dt, const char *pattern, char *outBuffer, size_t outSize +) const { + if (!pattern || !outBuffer || outSize == 0) { + return false; + } + tm t{}; + if (!Utils::toUtcTm(dt, t)) { + return false; + } + size_t written = strftime(outBuffer, outSize, pattern, &t); + return written > 0; } -bool ESPDate::formatWithPatternLocal(const DateTime& dt, const char* pattern, char* outBuffer, size_t outSize) const { - if (!pattern || !outBuffer || outSize == 0) { - return false; - } - tm t{}; - if (!Utils::toLocalTm(dt, t)) { - return false; - } - size_t written = strftime(outBuffer, outSize, pattern, &t); - return written > 0; +bool ESPDate::formatWithPatternLocal( + const DateTime &dt, const char *pattern, char *outBuffer, size_t outSize +) const { + if (!pattern || !outBuffer || outSize == 0) { + return false; + } + tm t{}; + if (!Utils::toLocalTm(dt, t)) { + return false; + } + size_t written = strftime(outBuffer, outSize, pattern, &t); + return written > 0; } -bool ESPDate::dateTimeToStringUtc(const DateTime& dt, char* outBuffer, size_t outSize, ESPDateFormat style) const { - return dt.utcString(outBuffer, outSize, style); +bool ESPDate::dateTimeToStringUtc( + const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style +) const { + return dt.utcString(outBuffer, outSize, style); } -bool ESPDate::dateTimeToStringLocal(const DateTime& dt, char* outBuffer, size_t outSize, ESPDateFormat style) const { - return dt.localString(outBuffer, outSize, style); +bool ESPDate::dateTimeToStringLocal( + const DateTime &dt, char *outBuffer, size_t outSize, ESPDateFormat style +) const { + return dt.localString(outBuffer, outSize, style); } -bool ESPDate::localDateTimeToString(const LocalDateTime& dt, char* outBuffer, size_t outSize) const { - return dt.localString(outBuffer, outSize); +bool ESPDate::localDateTimeToString( + const LocalDateTime &dt, char *outBuffer, size_t outSize +) const { + return dt.localString(outBuffer, outSize); } -bool ESPDate::nowUtcString(char* outBuffer, size_t outSize, ESPDateFormat style) const { - return dateTimeToStringUtc(nowUtc(), outBuffer, outSize, style); +bool ESPDate::nowUtcString(char *outBuffer, size_t outSize, ESPDateFormat style) const { + return dateTimeToStringUtc(nowUtc(), outBuffer, outSize, style); } -bool ESPDate::nowLocalString(char* outBuffer, size_t outSize, ESPDateFormat style) const { - return dateTimeToStringLocal(now(), outBuffer, outSize, style); +bool ESPDate::nowLocalString(char *outBuffer, size_t outSize, ESPDateFormat style) const { + return dateTimeToStringLocal(now(), outBuffer, outSize, style); } -bool ESPDate::lastNtpSyncStringUtc(char* outBuffer, size_t outSize, ESPDateFormat style) const { - if (!hasLastNtpSync_) { - return false; - } - return dateTimeToStringUtc(lastNtpSync_, outBuffer, outSize, style); +bool ESPDate::lastNtpSyncStringUtc(char *outBuffer, size_t outSize, ESPDateFormat style) const { + if (!hasLastNtpSync_) { + return false; + } + return dateTimeToStringUtc(lastNtpSync_, outBuffer, outSize, style); } -bool ESPDate::lastNtpSyncStringLocal(char* outBuffer, size_t outSize, ESPDateFormat style) const { - if (!hasLastNtpSync_) { - return false; - } - return dateTimeToStringLocal(lastNtpSync_, outBuffer, outSize, style); +bool ESPDate::lastNtpSyncStringLocal(char *outBuffer, size_t outSize, ESPDateFormat style) const { + if (!hasLastNtpSync_) { + return false; + } + return dateTimeToStringLocal(lastNtpSync_, outBuffer, outSize, style); } -std::string ESPDate::dateTimeToStringUtc(const DateTime& dt, ESPDateFormat style) const { - char buffer[40]; - if (!dateTimeToStringUtc(dt, buffer, sizeof(buffer), style)) { - return std::string(); - } - return std::string(buffer); +std::string ESPDate::dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style) const { + char buffer[40]; + if (!dateTimeToStringUtc(dt, buffer, sizeof(buffer), style)) { + return std::string(); + } + return std::string(buffer); } -std::string ESPDate::dateTimeToStringLocal(const DateTime& dt, ESPDateFormat style) const { - char buffer[48]; - if (!dateTimeToStringLocal(dt, buffer, sizeof(buffer), style)) { - return std::string(); - } - return std::string(buffer); +std::string ESPDate::dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style) const { + char buffer[48]; + if (!dateTimeToStringLocal(dt, buffer, sizeof(buffer), style)) { + return std::string(); + } + return std::string(buffer); } -std::string ESPDate::localDateTimeToString(const LocalDateTime& dt) const { - char buffer[32]; - if (!localDateTimeToString(dt, buffer, sizeof(buffer))) { - return std::string(); - } - return std::string(buffer); +std::string ESPDate::localDateTimeToString(const LocalDateTime &dt) const { + char buffer[32]; + if (!localDateTimeToString(dt, buffer, sizeof(buffer))) { + return std::string(); + } + return std::string(buffer); } std::string ESPDate::nowUtcString(ESPDateFormat style) const { - return dateTimeToStringUtc(nowUtc(), style); + return dateTimeToStringUtc(nowUtc(), style); } std::string ESPDate::nowLocalString(ESPDateFormat style) const { - return dateTimeToStringLocal(now(), style); + return dateTimeToStringLocal(now(), style); } std::string ESPDate::lastNtpSyncStringUtc(ESPDateFormat style) const { - if (!hasLastNtpSync_) { - return std::string(); - } - return dateTimeToStringUtc(lastNtpSync_, style); + if (!hasLastNtpSync_) { + return std::string(); + } + return dateTimeToStringUtc(lastNtpSync_, style); } std::string ESPDate::lastNtpSyncStringLocal(ESPDateFormat style) const { - if (!hasLastNtpSync_) { - return std::string(); - } - return dateTimeToStringLocal(lastNtpSync_, style); -} - -ESPDate::ParseResult ESPDate::parseIso8601Utc(const char* str) const { - ParseResult result{false, DateTime{}}; - if (!str) { - return result; - } - const size_t len = std::strlen(str); - if (len != 20 || str[4] != '-' || str[7] != '-' || (str[10] != 'T' && str[10] != 't') || - str[13] != ':' || str[16] != ':' || (str[19] != 'Z' && str[19] != 'z')) { - return result; - } - - int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; - if (!Utils::parseIntSlice(str, 4, 0, 9999, year) || - !Utils::parseIntSlice(str + 5, 2, 1, 12, month) || - !Utils::parseIntSlice(str + 8, 2, 1, 31, day) || - !Utils::parseIntSlice(str + 11, 2, 0, 23, hour) || - !Utils::parseIntSlice(str + 14, 2, 0, 59, minute) || - !Utils::parseIntSlice(str + 17, 2, 0, 60, second)) { - return result; - } - - const int maxDay = daysInMonth(year, month); - if (day > maxDay) { - return result; - } - - tm t{}; - 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; - t.tm_isdst = 0; - - result.ok = true; - result.value = Utils::fromUtcTm(t); - return result; -} - -ESPDate::ParseResult ESPDate::parseDateTimeLocal(const char* str) const { - ParseResult result{false, DateTime{}}; - if (!str) { - return result; - } - const size_t len = std::strlen(str); - if (len != 19 || str[4] != '-' || str[7] != '-' || str[10] != ' ' || str[13] != ':' || str[16] != ':') { - return result; - } - - int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; - if (!Utils::parseIntSlice(str, 4, 0, 9999, year) || - !Utils::parseIntSlice(str + 5, 2, 1, 12, month) || - !Utils::parseIntSlice(str + 8, 2, 1, 31, day) || - !Utils::parseIntSlice(str + 11, 2, 0, 23, hour) || - !Utils::parseIntSlice(str + 14, 2, 0, 59, minute) || - !Utils::parseIntSlice(str + 17, 2, 0, 60, second)) { - return result; - } - - const int maxDay = daysInMonth(year, month); - if (day > maxDay) { - return result; - } - - tm t{}; - 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; - t.tm_isdst = -1; // let the runtime decide - - result.ok = true; - result.value = Utils::fromLocalTm(t); - return result; -} - -const char* ESPDate::monthName(int month) const { - static const char* kMonths[12] = {"January", "February", "March", "April", "May", "June", - "July", "August", "September","October", "November", "December"}; - if (month < 1 || month > 12) { - return nullptr; - } - return kMonths[month - 1]; -} - -const char* ESPDate::monthName(const DateTime& dt) const { - return monthName(dt.monthUtc()); + if (!hasLastNtpSync_) { + return std::string(); + } + return dateTimeToStringLocal(lastNtpSync_, style); +} + +ESPDate::ParseResult ESPDate::parseIso8601Utc(const char *str) const { + ParseResult result{false, DateTime{}}; + if (!str) { + return result; + } + const size_t len = std::strlen(str); + if (len != 20 || str[4] != '-' || str[7] != '-' || (str[10] != 'T' && str[10] != 't') || + str[13] != ':' || str[16] != ':' || (str[19] != 'Z' && str[19] != 'z')) { + return result; + } + + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!Utils::parseIntSlice(str, 4, 0, 9999, year) || + !Utils::parseIntSlice(str + 5, 2, 1, 12, month) || + !Utils::parseIntSlice(str + 8, 2, 1, 31, day) || + !Utils::parseIntSlice(str + 11, 2, 0, 23, hour) || + !Utils::parseIntSlice(str + 14, 2, 0, 59, minute) || + !Utils::parseIntSlice(str + 17, 2, 0, 60, second)) { + return result; + } + + const int maxDay = daysInMonth(year, month); + if (day > maxDay) { + return result; + } + + tm t{}; + 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; + t.tm_isdst = 0; + + result.ok = true; + result.value = Utils::fromUtcTm(t); + return result; +} + +ESPDate::ParseResult ESPDate::parseDateTimeLocal(const char *str) const { + ParseResult result{false, DateTime{}}; + if (!str) { + return result; + } + const size_t len = std::strlen(str); + if (len != 19 || str[4] != '-' || str[7] != '-' || str[10] != ' ' || str[13] != ':' || + str[16] != ':') { + return result; + } + + int year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0; + if (!Utils::parseIntSlice(str, 4, 0, 9999, year) || + !Utils::parseIntSlice(str + 5, 2, 1, 12, month) || + !Utils::parseIntSlice(str + 8, 2, 1, 31, day) || + !Utils::parseIntSlice(str + 11, 2, 0, 23, hour) || + !Utils::parseIntSlice(str + 14, 2, 0, 59, minute) || + !Utils::parseIntSlice(str + 17, 2, 0, 60, second)) { + return result; + } + + const int maxDay = daysInMonth(year, month); + if (day > maxDay) { + return result; + } + + tm t{}; + 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; + t.tm_isdst = -1; // let the runtime decide + + result.ok = true; + result.value = Utils::fromLocalTm(t); + return result; +} + +const char *ESPDate::monthName(int month) const { + static const char *kMonths[12] = { + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December" + }; + if (month < 1 || month > 12) { + return nullptr; + } + return kMonths[month - 1]; +} + +const char *ESPDate::monthName(const DateTime &dt) const { + return monthName(dt.monthUtc()); } diff --git a/src/esp_date/date.h b/src/esp_date/date.h index ac406b0..eec09ad 100644 --- a/src/esp_date/date.h +++ b/src/esp_date/date.h @@ -1,309 +1,334 @@ #pragma once -#include #include "date_allocator.h" +#include #include #include -#include #include +#include #include #include struct timeval; -enum class ESPDateFormat { - Iso8601, - DateTime, - Date, - Time -}; +enum class ESPDateFormat { Iso8601, DateTime, Date, Time }; struct DateTime { - int64_t epochSeconds = 0; // seconds since 1970-01-01T00:00:00Z - - int yearUtc() const; - int monthUtc() const; // 1..12 - int dayUtc() const; // 1..31 - int hourUtc() const; // 0..23 - int minuteUtc() const; // 0..59 - int secondUtc() const; // 0..59 - - // Uses current system TZ for local formatting. - bool utcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const; - bool localString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const; - std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const; - std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const; + int64_t epochSeconds = 0; // seconds since 1970-01-01T00:00:00Z + + int yearUtc() const; + int monthUtc() const; // 1..12 + int dayUtc() const; // 1..31 + int hourUtc() const; // 0..23 + int minuteUtc() const; // 0..59 + int secondUtc() const; // 0..59 + + // Uses current system TZ for local formatting. + bool + utcString(char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const; + bool localString( + char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime + ) const; + std::string utcString(ESPDateFormat style = ESPDateFormat::DateTime) const; + std::string localString(ESPDateFormat style = ESPDateFormat::DateTime) const; }; struct LocalDateTime { - bool ok = false; - int year = 0; - int month = 0; - int day = 0; - int hour = 0; - int minute = 0; - int second = 0; - int offsetMinutes = 0; // local - UTC - DateTime utc{}; - - bool localString(char* outBuffer, size_t outSize) const; - std::string localString() const; + bool ok = false; + int year = 0; + int month = 0; + int day = 0; + int hour = 0; + int minute = 0; + int second = 0; + int offsetMinutes = 0; // local - UTC + DateTime utc{}; + + bool localString(char *outBuffer, size_t outSize) const; + std::string localString() const; }; struct ESPDateConfig { - float latitude = 0.0f; - float longitude = 0.0f; - const char* timeZone = nullptr; // POSIX TZ string, e.g. "CET-1CEST,M3.5.0/2,M10.5.0/3" - const char* ntpServer = nullptr; // optional primary NTP server; used with timeZone to call configTzTime - uint32_t ntpSyncIntervalMs = 0; // optional SNTP sync interval override; 0 keeps runtime default - bool usePSRAMBuffers = false; // prefer PSRAM for ESPDate-owned config/state text buffers - const char* ntpServer2 = nullptr; // optional secondary NTP server - const char* ntpServer3 = nullptr; // optional tertiary NTP server + float latitude = 0.0f; + float longitude = 0.0f; + const char *timeZone = nullptr; // POSIX TZ string, e.g. "CET-1CEST,M3.5.0/2,M10.5.0/3" + const char *ntpServer = + nullptr; // optional primary NTP server; used with timeZone to call configTzTime + uint32_t ntpSyncIntervalMs = 0; // optional SNTP sync interval override; 0 keeps runtime default + bool usePSRAMBuffers = false; // prefer PSRAM for ESPDate-owned config/state text buffers + const char *ntpServer2 = nullptr; // optional secondary NTP server + const char *ntpServer3 = nullptr; // optional tertiary NTP server }; struct SunCycleResult { - bool ok; - DateTime value; + bool ok; + DateTime value; }; struct MoonPhaseResult { - bool ok; - int angleDegrees; // 0..360 - double illumination; // 0.0..1.0 + bool ok; + int angleDegrees; // 0..360 + double illumination; // 0.0..1.0 }; class ESPDate { - public: - using NtpSyncCallback = void (*)(const DateTime& syncedAtUtc); - using NtpSyncCallable = std::function; - - ESPDate(); - ~ESPDate(); - void init(const ESPDateConfig& config); - void deinit(); - bool isInitialized() const { - return initialized_; - } - // Optional SNTP sync notification. Pass nullptr to clear. - void setNtpSyncCallback(NtpSyncCallback callback); - // Accepts capturing lambdas / std::bind / functors. - // Non-capturing lambdas bind to the function-pointer overload above. - template ::type, NtpSyncCallback>::value, - int>::type = 0> - void setNtpSyncCallback(Callable&& callback) { - setNtpSyncCallbackCallable(NtpSyncCallable(std::forward(callback))); - } - // Adjusts SNTP sync interval in milliseconds. Pass 0 to keep the runtime default. - // Returns false when the runtime does not expose interval control. - bool setNtpSyncIntervalMs(uint32_t intervalMs); - // True after at least one successful SNTP sync callback was received. - bool hasLastNtpSync() const; - // Returns the last SNTP sync timestamp (UTC epoch-backed DateTime). - // When hasLastNtpSync() is false this returns DateTime{}. - DateTime lastNtpSync() const; - // Triggers an immediate NTP sync with the configured server list. - // Returns false when no NTP server is configured or SNTP runtime support is unavailable. - bool syncNTP(); - - DateTime now() const; - DateTime nowUtc() const; // alias of now(), returns the raw system clock (UTC) - LocalDateTime nowLocal() const; - LocalDateTime toLocal(const DateTime& dt) const; - LocalDateTime toLocal(const DateTime& dt, const char* timeZone) const; - DateTime fromUnixSeconds(int64_t seconds) const; - DateTime fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const; - DateTime fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const; - int64_t toUnixSeconds(const DateTime& dt) const; - - // Arithmetic relative to a provided DateTime - DateTime addSeconds(const DateTime& dt, int64_t seconds) const; - DateTime addMinutes(const DateTime& dt, int64_t minutes) const; - DateTime addHours(const DateTime& dt, int64_t hours) const; - DateTime addDays(const DateTime& dt, int32_t days) const; - DateTime addMonths(const DateTime& dt, int32_t months) const; - DateTime addYears(const DateTime& dt, int32_t years) const; - - DateTime subSeconds(const DateTime& dt, int64_t seconds) const; - DateTime subMinutes(const DateTime& dt, int64_t minutes) const; - DateTime subHours(const DateTime& dt, int64_t hours) const; - DateTime subDays(const DateTime& dt, int32_t days) const; - DateTime subMonths(const DateTime& dt, int32_t months) const; - DateTime subYears(const DateTime& dt, int32_t years) const; - - // Convenience arithmetic relative to now() - DateTime addSeconds(int64_t seconds) const; - DateTime addMinutes(int64_t minutes) const; - DateTime addHours(int64_t hours) const; - DateTime addDays(int32_t days) const; - DateTime addMonths(int32_t months) const; - DateTime addYears(int32_t years) const; - - DateTime subSeconds(int64_t seconds) const; - DateTime subMinutes(int64_t minutes) const; - DateTime subHours(int64_t hours) const; - DateTime subDays(int32_t days) const; - DateTime subMonths(int32_t months) const; - DateTime subYears(int32_t years) const; - - // Differences - int64_t differenceInSeconds(const DateTime& a, const DateTime& b) const; - int64_t differenceInMinutes(const DateTime& a, const DateTime& b) const; - int64_t differenceInHours(const DateTime& a, const DateTime& b) const; - int64_t differenceInDays(const DateTime& a, const DateTime& b) const; - - // Comparisons - bool isBefore(const DateTime& a, const DateTime& b) const; - bool isAfter(const DateTime& a, const DateTime& b) const; - bool isEqual(const DateTime& a, const DateTime& b) const; // seconds precision - bool isEqualMinutes(const DateTime& a, const DateTime& b) const; // minutes precision (UTC epoch / 60) - bool isEqualMinutesUtc(const DateTime& a, const DateTime& b) const; // alias for minute-level UTC compare - bool isSameDay(const DateTime& a, const DateTime& b) const; - - // Calendar helpers (UTC) - DateTime startOfDayUtc(const DateTime& dt) const; - DateTime endOfDayUtc(const DateTime& dt) const; - DateTime startOfMonthUtc(const DateTime& dt) const; - DateTime endOfMonthUtc(const DateTime& dt) const; - - int getYearUtc(const DateTime& dt) const; - int getMonthUtc(const DateTime& dt) const; // 1..12 - int getDayUtc(const DateTime& dt) const; // 1..31 - int getWeekdayUtc(const DateTime& dt) const; // 0=Sunday..6=Saturday - - // Local time helpers (respect TZ) - DateTime startOfDayLocal(const DateTime& dt) const; - DateTime endOfDayLocal(const DateTime& dt) const; - DateTime startOfMonthLocal(const DateTime& dt) const; - DateTime endOfMonthLocal(const DateTime& dt) const; - DateTime startOfYearUtc(const DateTime& dt) const; - DateTime startOfYearLocal(const DateTime& dt) const; - - DateTime setTimeOfDayLocal(const DateTime& dt, int hour, int minute, int second) const; - DateTime setTimeOfDayUtc(const DateTime& dt, int hour, int minute, int second) const; - DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime& from) const; - DateTime nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime& from) const; - - int getYearLocal(const DateTime& dt) const; - int getMonthLocal(const DateTime& dt) const; // 1..12 - int getDayLocal(const DateTime& dt) const; // 1..31 - int getWeekdayLocal(const DateTime& dt) const; // 0=Sunday..6=Saturday - - bool isLeapYear(int year) const; - int daysInMonth(int year, int month) const; // month: 1..12 - - // Formatting - bool formatUtc(const DateTime& dt, ESPDateFormat style, char* outBuffer, size_t outSize) const; - bool formatLocal(const DateTime& dt, ESPDateFormat style, char* outBuffer, size_t outSize) const; - bool formatWithPatternUtc(const DateTime& dt, const char* pattern, char* outBuffer, size_t outSize) const; - bool formatWithPatternLocal(const DateTime& dt, const char* pattern, char* outBuffer, size_t outSize) const; - - // String helpers (embedded-safe buffer first, then std::string convenience) - bool dateTimeToStringUtc(const DateTime& dt, - char* outBuffer, - size_t outSize, - ESPDateFormat style = ESPDateFormat::DateTime) const; - bool dateTimeToStringLocal(const DateTime& dt, - char* outBuffer, - size_t outSize, - ESPDateFormat style = ESPDateFormat::DateTime) const; - bool localDateTimeToString(const LocalDateTime& dt, char* outBuffer, size_t outSize) const; - bool nowUtcString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const; - bool nowLocalString(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const; - bool lastNtpSyncStringUtc(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const; - bool lastNtpSyncStringLocal(char* outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime) const; - - std::string dateTimeToStringUtc(const DateTime& dt, ESPDateFormat style = ESPDateFormat::DateTime) const; - std::string dateTimeToStringLocal(const DateTime& dt, ESPDateFormat style = ESPDateFormat::DateTime) const; - std::string localDateTimeToString(const LocalDateTime& dt) const; - std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const; - std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const; - std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const; - std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const; - - struct ParseResult { - bool ok; - DateTime value; - }; - - ParseResult parseIso8601Utc(const char* str) const; - ParseResult parseDateTimeLocal(const char* str) const; - - // Sun cycle using stored configuration (lat/lon/timezone) - SunCycleResult sunrise() const; - SunCycleResult sunset() const; - SunCycleResult sunrise(const DateTime& day) const; - SunCycleResult sunset(const DateTime& day) const; - - // Sun cycle with explicit parameters (timezone in hours, DST flag) - SunCycleResult sunrise(float latitude, float longitude, float timezoneHours, bool isDst) const; - SunCycleResult sunset(float latitude, float longitude, float timezoneHours, bool isDst) const; - SunCycleResult sunrise(float latitude, - float longitude, - float timezoneHours, - bool isDst, - const DateTime& day) const; - SunCycleResult sunset(float latitude, - float longitude, - float timezoneHours, - bool isDst, - const DateTime& day) const; - - // Sun cycle using a POSIX TZ string (auto-DST) instead of numeric offset - SunCycleResult sunrise(float latitude, float longitude, const char* timeZone) const; - SunCycleResult sunset(float latitude, float longitude, const char* timeZone) const; - SunCycleResult sunrise(float latitude, float longitude, const char* timeZone, const DateTime& day) const; - SunCycleResult sunset(float latitude, float longitude, const char* timeZone, const DateTime& day) const; - - // Daylight checks using stored configuration - bool isDay() const; - bool isDay(const DateTime& day) const; - bool isDay(int sunRiseOffsetSec, int sunSetOffsetSec) const; - bool isDay(int sunRiseOffsetSec, int sunSetOffsetSec, const DateTime& day) const; - - // Daylight saving time helpers - bool isDstActive() const; - bool isDstActive(const DateTime& dt) const; - bool isDstActive(const char* timeZone) const; - bool isDstActive(const DateTime& dt, const char* timeZone) const; - - // Moon phase - MoonPhaseResult moonPhase() const; - MoonPhaseResult moonPhase(const DateTime& dt) const; - - // Month names - const char* monthName(int month) const; // 1..12, returns "January" ..."December" or nullptr on invalid - const char* monthName(const DateTime& dt) const; - - private: + public: + using NtpSyncCallback = void (*)(const DateTime &syncedAtUtc); + using NtpSyncCallable = std::function; + + ESPDate(); + ~ESPDate(); + void init(const ESPDateConfig &config); + void deinit(); + bool isInitialized() const { + return initialized_; + } + // Optional SNTP sync notification. Pass nullptr to clear. + void setNtpSyncCallback(NtpSyncCallback callback); + // Accepts capturing lambdas / std::bind / functors. + // Non-capturing lambdas bind to the function-pointer overload above. + template < + typename Callable, + typename std::enable_if< + !std::is_convertible::type, NtpSyncCallback>::value, + int>::type = 0> + void setNtpSyncCallback(Callable &&callback) { + setNtpSyncCallbackCallable(NtpSyncCallable(std::forward(callback))); + } + // Adjusts SNTP sync interval in milliseconds. Pass 0 to keep the runtime default. + // Returns false when the runtime does not expose interval control. + bool setNtpSyncIntervalMs(uint32_t intervalMs); + // True after at least one successful SNTP sync callback was received. + bool hasLastNtpSync() const; + // Returns the last SNTP sync timestamp (UTC epoch-backed DateTime). + // When hasLastNtpSync() is false this returns DateTime{}. + DateTime lastNtpSync() const; + // Triggers an immediate NTP sync with the configured server list. + // Returns false when no NTP server is configured or SNTP runtime support is unavailable. + bool syncNTP(); + + DateTime now() const; + DateTime nowUtc() const; // alias of now(), returns the raw system clock (UTC) + LocalDateTime nowLocal() const; + LocalDateTime toLocal(const DateTime &dt) const; + LocalDateTime toLocal(const DateTime &dt, const char *timeZone) const; + DateTime fromUnixSeconds(int64_t seconds) const; + DateTime + fromUtc(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const; + DateTime + fromLocal(int year, int month, int day, int hour = 0, int minute = 0, int second = 0) const; + int64_t toUnixSeconds(const DateTime &dt) const; + + // Arithmetic relative to a provided DateTime + DateTime addSeconds(const DateTime &dt, int64_t seconds) const; + DateTime addMinutes(const DateTime &dt, int64_t minutes) const; + DateTime addHours(const DateTime &dt, int64_t hours) const; + DateTime addDays(const DateTime &dt, int32_t days) const; + DateTime addMonths(const DateTime &dt, int32_t months) const; + DateTime addYears(const DateTime &dt, int32_t years) const; + + DateTime subSeconds(const DateTime &dt, int64_t seconds) const; + DateTime subMinutes(const DateTime &dt, int64_t minutes) const; + DateTime subHours(const DateTime &dt, int64_t hours) const; + DateTime subDays(const DateTime &dt, int32_t days) const; + DateTime subMonths(const DateTime &dt, int32_t months) const; + DateTime subYears(const DateTime &dt, int32_t years) const; + + // Convenience arithmetic relative to now() + DateTime addSeconds(int64_t seconds) const; + DateTime addMinutes(int64_t minutes) const; + DateTime addHours(int64_t hours) const; + DateTime addDays(int32_t days) const; + DateTime addMonths(int32_t months) const; + DateTime addYears(int32_t years) const; + + DateTime subSeconds(int64_t seconds) const; + DateTime subMinutes(int64_t minutes) const; + DateTime subHours(int64_t hours) const; + DateTime subDays(int32_t days) const; + DateTime subMonths(int32_t months) const; + DateTime subYears(int32_t years) const; + + // Differences + int64_t differenceInSeconds(const DateTime &a, const DateTime &b) const; + int64_t differenceInMinutes(const DateTime &a, const DateTime &b) const; + int64_t differenceInHours(const DateTime &a, const DateTime &b) const; + int64_t differenceInDays(const DateTime &a, const DateTime &b) const; + + // Comparisons + bool isBefore(const DateTime &a, const DateTime &b) const; + bool isAfter(const DateTime &a, const DateTime &b) const; + bool isEqual(const DateTime &a, const DateTime &b) const; // seconds precision + bool isEqualMinutes( + const DateTime &a, const DateTime &b + ) const; // minutes precision (UTC epoch / 60) + bool isEqualMinutesUtc( + const DateTime &a, const DateTime &b + ) const; // alias for minute-level UTC compare + bool isSameDay(const DateTime &a, const DateTime &b) const; + + // Calendar helpers (UTC) + DateTime startOfDayUtc(const DateTime &dt) const; + DateTime endOfDayUtc(const DateTime &dt) const; + DateTime startOfMonthUtc(const DateTime &dt) const; + DateTime endOfMonthUtc(const DateTime &dt) const; + + int getYearUtc(const DateTime &dt) const; + int getMonthUtc(const DateTime &dt) const; // 1..12 + int getDayUtc(const DateTime &dt) const; // 1..31 + int getWeekdayUtc(const DateTime &dt) const; // 0=Sunday..6=Saturday + + // Local time helpers (respect TZ) + DateTime startOfDayLocal(const DateTime &dt) const; + DateTime endOfDayLocal(const DateTime &dt) const; + DateTime startOfMonthLocal(const DateTime &dt) const; + DateTime endOfMonthLocal(const DateTime &dt) const; + DateTime startOfYearUtc(const DateTime &dt) const; + DateTime startOfYearLocal(const DateTime &dt) const; + + DateTime setTimeOfDayLocal(const DateTime &dt, int hour, int minute, int second) const; + DateTime setTimeOfDayUtc(const DateTime &dt, int hour, int minute, int second) const; + DateTime nextDailyAtLocal(int hour, int minute, int second, const DateTime &from) const; + DateTime + nextWeekdayAtLocal(int weekday, int hour, int minute, int second, const DateTime &from) const; + + int getYearLocal(const DateTime &dt) const; + int getMonthLocal(const DateTime &dt) const; // 1..12 + int getDayLocal(const DateTime &dt) const; // 1..31 + int getWeekdayLocal(const DateTime &dt) const; // 0=Sunday..6=Saturday + + bool isLeapYear(int year) const; + int daysInMonth(int year, int month) const; // month: 1..12 + + // Formatting + bool formatUtc(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const; + bool + formatLocal(const DateTime &dt, ESPDateFormat style, char *outBuffer, size_t outSize) const; + bool formatWithPatternUtc( + const DateTime &dt, const char *pattern, char *outBuffer, size_t outSize + ) const; + bool formatWithPatternLocal( + const DateTime &dt, const char *pattern, char *outBuffer, size_t outSize + ) const; + + // String helpers (embedded-safe buffer first, then std::string convenience) + bool dateTimeToStringUtc( + const DateTime &dt, + char *outBuffer, + size_t outSize, + ESPDateFormat style = ESPDateFormat::DateTime + ) const; + bool dateTimeToStringLocal( + const DateTime &dt, + char *outBuffer, + size_t outSize, + ESPDateFormat style = ESPDateFormat::DateTime + ) const; + bool localDateTimeToString(const LocalDateTime &dt, char *outBuffer, size_t outSize) const; + bool nowUtcString( + char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime + ) const; + bool nowLocalString( + char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime + ) const; + bool lastNtpSyncStringUtc( + char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime + ) const; + bool lastNtpSyncStringLocal( + char *outBuffer, size_t outSize, ESPDateFormat style = ESPDateFormat::DateTime + ) const; + + std::string + dateTimeToStringUtc(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const; + std::string + dateTimeToStringLocal(const DateTime &dt, ESPDateFormat style = ESPDateFormat::DateTime) const; + std::string localDateTimeToString(const LocalDateTime &dt) const; + std::string nowUtcString(ESPDateFormat style = ESPDateFormat::DateTime) const; + std::string nowLocalString(ESPDateFormat style = ESPDateFormat::DateTime) const; + std::string lastNtpSyncStringUtc(ESPDateFormat style = ESPDateFormat::DateTime) const; + std::string lastNtpSyncStringLocal(ESPDateFormat style = ESPDateFormat::DateTime) const; + + struct ParseResult { + bool ok; + DateTime value; + }; + + ParseResult parseIso8601Utc(const char *str) const; + ParseResult parseDateTimeLocal(const char *str) const; + + // Sun cycle using stored configuration (lat/lon/timezone) + SunCycleResult sunrise() const; + SunCycleResult sunset() const; + SunCycleResult sunrise(const DateTime &day) const; + SunCycleResult sunset(const DateTime &day) const; + + // Sun cycle with explicit parameters (timezone in hours, DST flag) + SunCycleResult sunrise(float latitude, float longitude, float timezoneHours, bool isDst) const; + SunCycleResult sunset(float latitude, float longitude, float timezoneHours, bool isDst) const; + SunCycleResult sunrise( + float latitude, float longitude, float timezoneHours, bool isDst, const DateTime &day + ) const; + SunCycleResult sunset( + float latitude, float longitude, float timezoneHours, bool isDst, const DateTime &day + ) const; + + // Sun cycle using a POSIX TZ string (auto-DST) instead of numeric offset + SunCycleResult sunrise(float latitude, float longitude, const char *timeZone) const; + SunCycleResult sunset(float latitude, float longitude, const char *timeZone) const; + SunCycleResult + sunrise(float latitude, float longitude, const char *timeZone, const DateTime &day) const; + SunCycleResult + sunset(float latitude, float longitude, const char *timeZone, const DateTime &day) const; + + // Daylight checks using stored configuration + bool isDay() const; + bool isDay(const DateTime &day) const; + bool isDay(int sunRiseOffsetSec, int sunSetOffsetSec) const; + bool isDay(int sunRiseOffsetSec, int sunSetOffsetSec, const DateTime &day) const; + + // Daylight saving time helpers + bool isDstActive() const; + bool isDstActive(const DateTime &dt) const; + bool isDstActive(const char *timeZone) const; + bool isDstActive(const DateTime &dt, const char *timeZone) const; + + // Moon phase + MoonPhaseResult moonPhase() const; + MoonPhaseResult moonPhase(const DateTime &dt) const; + + // Month names + const char * + monthName(int month) const; // 1..12, returns "January" ..."December" or nullptr on invalid + const char *monthName(const DateTime &dt) const; + + private: #if defined(__has_include) -# if __has_include() - static void handleSntpSync(struct timeval* tv); -# endif +#if __has_include() + static void handleSntpSync(struct timeval *tv); +#endif #endif - void setNtpSyncCallbackCallable(const NtpSyncCallable& callback); - bool applyNtpConfig() const; - bool hasAnyNtpServerConfigured() const; - - SunCycleResult sunriseFromConfig(const DateTime& day) const; - SunCycleResult sunsetFromConfig(const DateTime& day) const; - bool isDayWithOffsets(const DateTime& day, int sunRiseOffsetSec, int sunSetOffsetSec) const; - - float latitude_ = 0.0f; - float longitude_ = 0.0f; - DateString timeZone_; - static constexpr size_t kMaxNtpServers = 3; - DateString ntpServers_[kMaxNtpServers]; - uint32_t ntpSyncIntervalMs_ = 0; - bool usePSRAMBuffers_ = false; - DateTime lastNtpSync_{}; - bool hasLastNtpSync_ = false; - NtpSyncCallback ntpSyncCallback_ = nullptr; - NtpSyncCallable ntpSyncCallbackCallable_; - static NtpSyncCallback activeNtpSyncCallback_; - static NtpSyncCallable activeNtpSyncCallbackCallable_; - static ESPDate* activeNtpSyncOwner_; - bool hasLocation_ = false; - bool initialized_ = false; + void setNtpSyncCallbackCallable(const NtpSyncCallable &callback); + bool applyNtpConfig() const; + bool hasAnyNtpServerConfigured() const; + + SunCycleResult sunriseFromConfig(const DateTime &day) const; + SunCycleResult sunsetFromConfig(const DateTime &day) const; + bool isDayWithOffsets(const DateTime &day, int sunRiseOffsetSec, int sunSetOffsetSec) const; + + float latitude_ = 0.0f; + float longitude_ = 0.0f; + DateString timeZone_; + static constexpr size_t kMaxNtpServers = 3; + DateString ntpServers_[kMaxNtpServers]; + uint32_t ntpSyncIntervalMs_ = 0; + bool usePSRAMBuffers_ = false; + DateTime lastNtpSync_{}; + bool hasLastNtpSync_ = false; + NtpSyncCallback ntpSyncCallback_ = nullptr; + NtpSyncCallable ntpSyncCallbackCallable_; + static NtpSyncCallback activeNtpSyncCallback_; + static NtpSyncCallable activeNtpSyncCallbackCallable_; + static ESPDate *activeNtpSyncOwner_; + bool hasLocation_ = false; + bool initialized_ = false; }; diff --git a/src/esp_date/date_allocator.h b/src/esp_date/date_allocator.h index b08c5bf..32923c3 100644 --- a/src/esp_date/date_allocator.h +++ b/src/esp_date/date_allocator.h @@ -17,81 +17,80 @@ #include namespace date_allocator_detail { -inline void* allocate(std::size_t bytes, bool usePSRAMBuffers) noexcept { +inline void *allocate(std::size_t bytes, bool usePSRAMBuffers) noexcept { #if ESP_DATE_HAS_BUFFER_MANAGER - return ESPBufferManager::allocate(bytes, usePSRAMBuffers); + return ESPBufferManager::allocate(bytes, usePSRAMBuffers); #else - (void)usePSRAMBuffers; - return std::malloc(bytes); + (void)usePSRAMBuffers; + return std::malloc(bytes); #endif } -inline void deallocate(void* ptr) noexcept { +inline void deallocate(void *ptr) noexcept { #if ESP_DATE_HAS_BUFFER_MANAGER - ESPBufferManager::deallocate(ptr); + ESPBufferManager::deallocate(ptr); #else - std::free(ptr); + std::free(ptr); #endif } -} // namespace date_allocator_detail - -template -class DateAllocator { - public: - using value_type = T; - - DateAllocator() noexcept = default; - explicit DateAllocator(bool usePSRAMBuffers) noexcept : usePSRAMBuffers_(usePSRAMBuffers) {} - - template - DateAllocator(const DateAllocator& other) noexcept : usePSRAMBuffers_(other.usePSRAMBuffers()) {} - - T* allocate(std::size_t n) { - if (n == 0) { - return nullptr; - } - if (n > (std::numeric_limits::max() / sizeof(T))) { +} // namespace date_allocator_detail + +template class DateAllocator { + public: + using value_type = T; + + DateAllocator() noexcept = default; + explicit DateAllocator(bool usePSRAMBuffers) noexcept : usePSRAMBuffers_(usePSRAMBuffers) { + } + + template + DateAllocator(const DateAllocator &other) noexcept + : usePSRAMBuffers_(other.usePSRAMBuffers()) { + } + + T *allocate(std::size_t n) { + if (n == 0) { + return nullptr; + } + if (n > (std::numeric_limits::max() / sizeof(T))) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } + } - void* memory = date_allocator_detail::allocate(n * sizeof(T), usePSRAMBuffers_); - if (memory == nullptr) { + void *memory = date_allocator_detail::allocate(n * sizeof(T), usePSRAMBuffers_); + if (memory == nullptr) { #if defined(__cpp_exceptions) - throw std::bad_alloc(); + throw std::bad_alloc(); #else - std::abort(); + std::abort(); #endif - } - return static_cast(memory); - } + } + return static_cast(memory); + } - void deallocate(T* ptr, std::size_t) noexcept { - date_allocator_detail::deallocate(ptr); - } + void deallocate(T *ptr, std::size_t) noexcept { + date_allocator_detail::deallocate(ptr); + } - bool usePSRAMBuffers() const noexcept { - return usePSRAMBuffers_; - } + bool usePSRAMBuffers() const noexcept { + return usePSRAMBuffers_; + } - template - bool operator==(const DateAllocator& other) const noexcept { - return usePSRAMBuffers_ == other.usePSRAMBuffers(); - } + template bool operator==(const DateAllocator &other) const noexcept { + return usePSRAMBuffers_ == other.usePSRAMBuffers(); + } - template - bool operator!=(const DateAllocator& other) const noexcept { - return !(*this == other); - } + template bool operator!=(const DateAllocator &other) const noexcept { + return !(*this == other); + } - private: - template - friend class DateAllocator; + private: + template friend class DateAllocator; - bool usePSRAMBuffers_ = false; + bool usePSRAMBuffers_ = false; }; using DateString = std::basic_string, DateAllocator>; diff --git a/src/esp_date/moon.cpp b/src/esp_date/moon.cpp index d0722f7..7c0ce28 100644 --- a/src/esp_date/moon.cpp +++ b/src/esp_date/moon.cpp @@ -7,108 +7,110 @@ namespace { constexpr double kPi = 3.14159265358979323846; constexpr double kDegToRad = kPi / 180.0; -template -T mapValue(T2 val, T2 in_min, T2 in_max, T out_min, T out_max) { - return (val - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; +template T mapValue(T2 val, T2 in_min, T2 in_max, T out_min, T out_max) { + return (val - in_min) * (out_max - out_min) / (in_max - in_min) + out_min; } -bool toUtcTm(const DateTime& dt, tm& out) { - time_t raw = static_cast(dt.epochSeconds); - return gmtime_r(&raw, &out) != nullptr; +bool toUtcTm(const DateTime &dt, tm &out) { + time_t raw = static_cast(dt.epochSeconds); + return gmtime_r(&raw, &out) != nullptr; } -double fractionalHour(const tm& timeinfo) { - const int totalSeconds = timeinfo.tm_min * 60 + timeinfo.tm_sec; - return static_cast(timeinfo.tm_hour) + mapValue(totalSeconds, 0, 3600, 0.0, 1.0); +double fractionalHour(const tm &timeinfo) { + const int totalSeconds = timeinfo.tm_min * 60 + timeinfo.tm_sec; + return static_cast(timeinfo.tm_hour) + + mapValue(totalSeconds, 0, 3600, 0.0, 1.0); } double julianDay(int32_t year, int32_t month, double day) { - int32_t b = 0; - if (month < 3) { - year -= 1; - month += 12; - } - if (year > 1582 || (year == 1582 && month > 10) || (year == 1582 && month == 10 && day > 15)) { - const int32_t a = year / 100; - b = 2 - a + a / 4; - } - const double c = 365.25 * static_cast(year); - const double e = 30.6001 * static_cast(month + 1); - return b + c + e + day + 1720994.5; + int32_t b = 0; + if (month < 3) { + year -= 1; + month += 12; + } + if (year > 1582 || (year == 1582 && month > 10) || (year == 1582 && month == 10 && day > 15)) { + const int32_t a = year / 100; + b = 2 - a + a / 4; + } + const double c = 365.25 * static_cast(year); + const double e = 30.6001 * static_cast(month + 1); + return b + c + e + day + 1720994.5; } double sunPosition(double j) { - double n = 360.0 / 365.2422 * j; - int32_t i = static_cast(n / 360.0); - n = n - static_cast(i) * 360.0; - double x = n - 3.762863; - if (x < 0) { - x += 360.0; - } - x *= kDegToRad; - double e = x; - double dl = 0.0; - do { - dl = e - 0.016718 * std::sin(e) - x; - e = e - dl / (1 - 0.016718 * std::cos(e)); - } while (std::fabs(dl) >= 1e-12); - const double v = 360.0 / kPi * std::atan(1.01686011182 * std::tan(e / 2.0)); - double l = v + 282.596403; - i = static_cast(l / 360.0); - l = l - static_cast(i) * 360.0; - return l; + double n = 360.0 / 365.2422 * j; + int32_t i = static_cast(n / 360.0); + n = n - static_cast(i) * 360.0; + double x = n - 3.762863; + if (x < 0) { + x += 360.0; + } + x *= kDegToRad; + double e = x; + double dl = 0.0; + do { + dl = e - 0.016718 * std::sin(e) - x; + e = e - dl / (1 - 0.016718 * std::cos(e)); + } while (std::fabs(dl) >= 1e-12); + const double v = 360.0 / kPi * std::atan(1.01686011182 * std::tan(e / 2.0)); + double l = v + 282.596403; + i = static_cast(l / 360.0); + l = l - static_cast(i) * 360.0; + return l; } double moonPosition(double j, double ls) { - double ms = 0.985647332099 * j - 3.762863; - if (ms < 0) { - ms += 360.0; - } - double l = 13.176396 * j + 64.975464; - int32_t i = static_cast(l / 360.0); - l = l - static_cast(i) * 360.0; - if (l < 0) { - l += 360.0; - } - double mm = l - 0.1114041 * j - 349.383063; - i = static_cast(mm / 360.0); - mm -= static_cast(i) * 360.0; - const double ev = 1.2739 * std::sin(kDegToRad * (2.0 * (l - ls) - mm)); - const double sms = std::sin(kDegToRad * ms); - const double ae = 0.1858 * sms; - mm += ev - ae - 0.37 * sms; - const double ec = 6.2886 * std::sin(kDegToRad * mm); - l += ev + ec - ae + 0.214 * std::sin(kDegToRad * 2.0 * mm); - l = 0.6583 * std::sin(kDegToRad * 2.0 * (l - ls)) + l; - return l; + double ms = 0.985647332099 * j - 3.762863; + if (ms < 0) { + ms += 360.0; + } + double l = 13.176396 * j + 64.975464; + int32_t i = static_cast(l / 360.0); + l = l - static_cast(i) * 360.0; + if (l < 0) { + l += 360.0; + } + double mm = l - 0.1114041 * j - 349.383063; + i = static_cast(mm / 360.0); + mm -= static_cast(i) * 360.0; + const double ev = 1.2739 * std::sin(kDegToRad * (2.0 * (l - ls) - mm)); + const double sms = std::sin(kDegToRad * ms); + const double ae = 0.1858 * sms; + mm += ev - ae - 0.37 * sms; + const double ec = 6.2886 * std::sin(kDegToRad * mm); + l += ev + ec - ae + 0.214 * std::sin(kDegToRad * 2.0 * mm); + l = 0.6583 * std::sin(kDegToRad * 2.0 * (l - ls)) + l; + return l; } -MoonPhaseResult computeMoonPhase(const DateTime& dt) { - tm t{}; - if (!toUtcTm(dt, t)) { - return MoonPhaseResult{false, 0, 0.0}; - } +MoonPhaseResult computeMoonPhase(const DateTime &dt) { + tm t{}; + if (!toUtcTm(dt, t)) { + return MoonPhaseResult{false, 0, 0.0}; + } - const double hour = fractionalHour(t); - const double j = julianDay(t.tm_year + 1900, t.tm_mon + 1, static_cast(t.tm_mday) + hour / 24.0) - 2444238.5; - const double ls = sunPosition(j); - const double lm = moonPosition(j, ls); - double angle = lm - ls; - if (angle < 0) { - angle += 360.0; - } - if (angle >= 360.0) { - angle = std::fmod(angle, 360.0); - } - const double illumination = (1.0 - std::cos((lm - ls) * kDegToRad)) / 2.0; - return MoonPhaseResult{true, static_cast(angle), illumination}; + const double hour = fractionalHour(t); + const double j = + julianDay(t.tm_year + 1900, t.tm_mon + 1, static_cast(t.tm_mday) + hour / 24.0) - + 2444238.5; + const double ls = sunPosition(j); + const double lm = moonPosition(j, ls); + double angle = lm - ls; + if (angle < 0) { + angle += 360.0; + } + if (angle >= 360.0) { + angle = std::fmod(angle, 360.0); + } + const double illumination = (1.0 - std::cos((lm - ls) * kDegToRad)) / 2.0; + return MoonPhaseResult{true, static_cast(angle), illumination}; } -} // namespace +} // namespace MoonPhaseResult ESPDate::moonPhase() const { - return moonPhase(now()); + return moonPhase(now()); } -MoonPhaseResult ESPDate::moonPhase(const DateTime& dt) const { - return computeMoonPhase(dt); +MoonPhaseResult ESPDate::moonPhase(const DateTime &dt) const { + return computeMoonPhase(dt); } diff --git a/src/esp_date/sun.cpp b/src/esp_date/sun.cpp index eed79b8..c0b2491 100644 --- a/src/esp_date/sun.cpp +++ b/src/esp_date/sun.cpp @@ -8,359 +8,427 @@ using Utils = ESPDateUtils; namespace { bool validCoordinates(float latitude, float longitude) { - return std::isfinite(latitude) && std::isfinite(longitude) && latitude >= -90.0f && latitude <= 90.0f && - longitude >= -180.0f && longitude <= 180.0f; + return std::isfinite(latitude) && std::isfinite(longitude) && latitude >= -90.0f && + latitude <= 90.0f && longitude >= -180.0f && longitude <= 180.0f; } struct LocalDateResult { - int year = 0; - int month = 0; - int day = 0; - bool ok = false; + int year = 0; + int month = 0; + int day = 0; + bool ok = false; }; struct OffsetDateResult { - double offsetMinutes = 0.0; - LocalDateResult date{}; + double offsetMinutes = 0.0; + LocalDateResult date{}; }; -LocalDateResult deriveLocalDateWithOffset(const DateTime& dt, int offsetSeconds) { - int64_t shifted = dt.epochSeconds + static_cast(offsetSeconds); - time_t raw = static_cast(shifted); - tm t{}; - if (gmtime_r(&raw, &t) == nullptr) { - return {}; - } - return LocalDateResult{t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, true}; +LocalDateResult deriveLocalDateWithOffset(const DateTime &dt, int offsetSeconds) { + int64_t shifted = dt.epochSeconds + static_cast(offsetSeconds); + time_t raw = static_cast(shifted); + tm t{}; + if (gmtime_r(&raw, &t) == nullptr) { + return {}; + } + return LocalDateResult{t.tm_year + 1900, t.tm_mon + 1, t.tm_mday, true}; } -OffsetDateResult computeOffsetAndDate(const DateTime& dt, const char* timeZone, bool usePSRAMBuffers) { - Utils::ScopedTz scoped(timeZone, usePSRAMBuffers); - time_t raw = static_cast(dt.epochSeconds); - tm local{}; - if (localtime_r(&raw, &local) == nullptr) { - return {}; - } +OffsetDateResult +computeOffsetAndDate(const DateTime &dt, const char *timeZone, bool usePSRAMBuffers) { + Utils::ScopedTz scoped(timeZone, usePSRAMBuffers); + time_t raw = static_cast(dt.epochSeconds); + tm local{}; + if (localtime_r(&raw, &local) == nullptr) { + return {}; + } - const int64_t offsetSeconds = Utils::timegm64(local) - static_cast(raw); - OffsetDateResult result; - result.offsetMinutes = static_cast(offsetSeconds) / 60.0; - result.date = LocalDateResult{local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, true}; - return result; + const int64_t offsetSeconds = Utils::timegm64(local) - static_cast(raw); + OffsetDateResult result; + result.offsetMinutes = static_cast(offsetSeconds) / 60.0; + result.date = LocalDateResult{local.tm_year + 1900, local.tm_mon + 1, local.tm_mday, true}; + return result; } constexpr double kPi = 3.14159265358979323846; -constexpr double kSunAngle = 90.833; // standard atmospheric refraction angle in degrees +constexpr double kSunAngle = 90.833; // standard atmospheric refraction angle in degrees double radToDeg(double rad) { - return 180.0 * rad / kPi; + return 180.0 * rad / kPi; } double degToRad(double deg) { - return kPi * deg / 180.0; + return kPi * deg / 180.0; } double jDay(int year, int month, int day) { - if (month <= 2) { - year -= 1; - month += 12; - } + if (month <= 2) { + year -= 1; + month += 12; + } - int A = static_cast(std::floor(year / 100.0)); - int B = 2 - A + static_cast(std::floor(A / 4.0)); - return std::floor(365.25 * (year + 4716)) + std::floor(30.6001 * (month + 1)) + day + B - 1524.5; + int A = static_cast(std::floor(year / 100.0)); + int B = 2 - A + static_cast(std::floor(A / 4.0)); + return std::floor(365.25 * (year + 4716)) + std::floor(30.6001 * (month + 1)) + day + B - + 1524.5; } double fractionOfCentury(double jd) { - return (jd - 2451545.0) / 36525.0; + return (jd - 2451545.0) / 36525.0; } double geomMeanLongSun(double t) { - double L0 = 280.46646 + t * (36000.76983 + t * 0.0003032); - while (L0 > 360.0) { - L0 -= 360.0; - } - while (L0 < 0.0) { - L0 += 360.0; - } - return L0; + double L0 = 280.46646 + t * (36000.76983 + t * 0.0003032); + while (L0 > 360.0) { + L0 -= 360.0; + } + while (L0 < 0.0) { + L0 += 360.0; + } + return L0; } double geomMeanAnomalySun(double t) { - return 357.52911 + t * (35999.05029 - 0.0001537 * t); + return 357.52911 + t * (35999.05029 - 0.0001537 * t); } double eccentricityEarthOrbit(double t) { - return 0.016708634 - t * (0.000042037 + 0.0000001267 * t); + return 0.016708634 - t * (0.000042037 + 0.0000001267 * t); } double meanObliquityOfEcliptic(double t) { - double seconds = 21.448 - t * (46.8150 + t * (0.00059 - t * 0.001813)); - return 23.0 + (26.0 + (seconds / 60.0)) / 60.0; + double seconds = 21.448 - t * (46.8150 + t * (0.00059 - t * 0.001813)); + return 23.0 + (26.0 + (seconds / 60.0)) / 60.0; } double obliquityCorrection(double t) { - double e0 = meanObliquityOfEcliptic(t); - double omega = 125.04 - 1934.136 * t; - return e0 + 0.00256 * std::cos(degToRad(omega)); + double e0 = meanObliquityOfEcliptic(t); + double omega = 125.04 - 1934.136 * t; + return e0 + 0.00256 * std::cos(degToRad(omega)); } double sunEqOfCenter(double t) { - double m = geomMeanAnomalySun(t); - double mrad = degToRad(m); - double sinm = std::sin(mrad); - double sin2m = std::sin(mrad * 2.0); - double sin3m = std::sin(mrad * 3.0); - return sinm * (1.914602 - t * (0.004817 + 0.000014 * t)) + sin2m * (0.019993 - 0.000101 * t) + sin3m * 0.000289; + double m = geomMeanAnomalySun(t); + double mrad = degToRad(m); + double sinm = std::sin(mrad); + double sin2m = std::sin(mrad * 2.0); + double sin3m = std::sin(mrad * 3.0); + return sinm * (1.914602 - t * (0.004817 + 0.000014 * t)) + sin2m * (0.019993 - 0.000101 * t) + + sin3m * 0.000289; } double sunTrueLong(double t) { - double l0 = geomMeanLongSun(t); - double c = sunEqOfCenter(t); - return l0 + c; + double l0 = geomMeanLongSun(t); + double c = sunEqOfCenter(t); + return l0 + c; } double sunApparentLong(double t) { - double o = sunTrueLong(t); - double omega = 125.04 - 1934.136 * t; - return o - 0.00569 - 0.00478 * std::sin(degToRad(omega)); + double o = sunTrueLong(t); + double omega = 125.04 - 1934.136 * t; + return o - 0.00569 - 0.00478 * std::sin(degToRad(omega)); } double sunDeclination(double t) { - double e = obliquityCorrection(t); - double lambda = sunApparentLong(t); - double sint = std::sin(degToRad(e)) * std::sin(degToRad(lambda)); - return radToDeg(std::asin(sint)); + double e = obliquityCorrection(t); + double lambda = sunApparentLong(t); + double sint = std::sin(degToRad(e)) * std::sin(degToRad(lambda)); + return radToDeg(std::asin(sint)); } double equationOfTime(double t) { - double epsilon = obliquityCorrection(t); - double l0 = geomMeanLongSun(t); - double e = eccentricityEarthOrbit(t); - double m = geomMeanAnomalySun(t); + double epsilon = obliquityCorrection(t); + double l0 = geomMeanLongSun(t); + double e = eccentricityEarthOrbit(t); + double m = geomMeanAnomalySun(t); - double y = std::tan(degToRad(epsilon) / 2.0); - y *= y; + double y = std::tan(degToRad(epsilon) / 2.0); + y *= y; - double sin2l0 = std::sin(2.0 * degToRad(l0)); - double sinm = std::sin(degToRad(m)); - double cos2l0 = std::cos(2.0 * degToRad(l0)); - double sin4l0 = std::sin(4.0 * degToRad(l0)); - double sin2m = std::sin(2.0 * degToRad(m)); + double sin2l0 = std::sin(2.0 * degToRad(l0)); + double sinm = std::sin(degToRad(m)); + double cos2l0 = std::cos(2.0 * degToRad(l0)); + double sin4l0 = std::sin(4.0 * degToRad(l0)); + double sin2m = std::sin(2.0 * degToRad(m)); - double etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - 0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m; - return radToDeg(etime) * 4.0; + double etime = y * sin2l0 - 2.0 * e * sinm + 4.0 * e * y * sinm * cos2l0 - + 0.5 * y * y * sin4l0 - 1.25 * e * e * sin2m; + return radToDeg(etime) * 4.0; } double hourAngleSunrise(double lat, double solarDec) { - double latRad = degToRad(lat); - double sdRad = degToRad(solarDec); - double haArg = (std::cos(degToRad(kSunAngle)) / (std::cos(latRad) * std::cos(sdRad)) - std::tan(latRad) * std::tan(sdRad)); - return std::acos(haArg); + double latRad = degToRad(lat); + double sdRad = degToRad(solarDec); + double haArg = + (std::cos(degToRad(kSunAngle)) / (std::cos(latRad) * std::cos(sdRad)) - + std::tan(latRad) * std::tan(sdRad)); + return std::acos(haArg); } double sunriseSetUTC(bool isRise, double jday, double latitude, double longitude) { - double t = fractionOfCentury(jday); - double eqTime = equationOfTime(t); - double solarDec = sunDeclination(t); - double hourAngle = hourAngleSunrise(latitude, solarDec); - - hourAngle = isRise ? hourAngle : -hourAngle; - double delta = longitude + radToDeg(hourAngle); - return 720.0 - (4.0 * delta) - eqTime; -} - -int sunriseSetLocalMinutes(bool isRise, int year, int month, int day, double latitude, double longitude, double offsetMinutes) { - double jday = jDay(year, month, day); - double timeUTC = sunriseSetUTC(isRise, jday, latitude, longitude); - - double newJday = jday + timeUTC / (60.0 * 24.0); - double newTimeUTC = sunriseSetUTC(isRise, newJday, latitude, longitude); - - if (!std::isnan(newTimeUTC)) { - double timeLocal = std::round(newTimeUTC + offsetMinutes); - return static_cast(timeLocal); - } - return -1; -} - -SunCycleResult buildSunCycleResult(int minutes, - double offsetMinutes, - const LocalDateResult& date, - const ESPDate& dateHelper) { - SunCycleResult result{false, DateTime{}}; - if (!date.ok || minutes < 0 || minutes >= 1440) { - return result; - } - - const int64_t offsetSeconds = static_cast(std::llround(offsetMinutes * 60.0)); - DateTime midnightUtc = dateHelper.fromUtc(date.year, date.month, date.day, 0, 0, 0); - DateTime localMidnightUtc = dateHelper.subSeconds(midnightUtc, offsetSeconds); - - result.ok = true; - result.value = dateHelper.addSeconds(localMidnightUtc, static_cast(minutes) * Utils::kSecondsPerMinute); - return result; -} -} // namespace + double t = fractionOfCentury(jday); + double eqTime = equationOfTime(t); + double solarDec = sunDeclination(t); + double hourAngle = hourAngleSunrise(latitude, solarDec); + + hourAngle = isRise ? hourAngle : -hourAngle; + double delta = longitude + radToDeg(hourAngle); + return 720.0 - (4.0 * delta) - eqTime; +} + +int sunriseSetLocalMinutes( + bool isRise, + int year, + int month, + int day, + double latitude, + double longitude, + double offsetMinutes +) { + double jday = jDay(year, month, day); + double timeUTC = sunriseSetUTC(isRise, jday, latitude, longitude); + + double newJday = jday + timeUTC / (60.0 * 24.0); + double newTimeUTC = sunriseSetUTC(isRise, newJday, latitude, longitude); + + if (!std::isnan(newTimeUTC)) { + double timeLocal = std::round(newTimeUTC + offsetMinutes); + return static_cast(timeLocal); + } + return -1; +} + +SunCycleResult buildSunCycleResult( + int minutes, double offsetMinutes, const LocalDateResult &date, const ESPDate &dateHelper +) { + SunCycleResult result{false, DateTime{}}; + if (!date.ok || minutes < 0 || minutes >= 1440) { + return result; + } + + const int64_t offsetSeconds = static_cast(std::llround(offsetMinutes * 60.0)); + DateTime midnightUtc = dateHelper.fromUtc(date.year, date.month, date.day, 0, 0, 0); + DateTime localMidnightUtc = dateHelper.subSeconds(midnightUtc, offsetSeconds); + + result.ok = true; + result.value = dateHelper.addSeconds( + localMidnightUtc, + static_cast(minutes) * Utils::kSecondsPerMinute + ); + return result; +} +} // namespace SunCycleResult ESPDate::sunrise() const { - return sunrise(now()); + return sunrise(now()); } SunCycleResult ESPDate::sunset() const { - return sunset(now()); -} - -SunCycleResult ESPDate::sunrise(const DateTime& day) const { - return sunriseFromConfig(day); -} - -SunCycleResult ESPDate::sunset(const DateTime& day) const { - return sunsetFromConfig(day); -} - -SunCycleResult ESPDate::sunrise(float latitude, float longitude, float timezoneHours, bool isDst) const { - return sunrise(latitude, longitude, timezoneHours, isDst, now()); -} - -SunCycleResult ESPDate::sunset(float latitude, float longitude, float timezoneHours, bool isDst) const { - return sunset(latitude, longitude, timezoneHours, isDst, now()); -} - -SunCycleResult ESPDate::sunrise(float latitude, - float longitude, - float timezoneHours, - bool isDst, - const DateTime& day) const { - if (!validCoordinates(latitude, longitude)) { - return SunCycleResult{false, DateTime{}}; - } - const double offsetMinutes = static_cast(timezoneHours) * 60.0 + (isDst ? 60.0 : 0.0); - const int offsetSeconds = static_cast(std::llround(offsetMinutes * Utils::kSecondsPerMinute)); - LocalDateResult localDate = deriveLocalDateWithOffset(day, offsetSeconds); - if (!localDate.ok) { - return SunCycleResult{false, DateTime{}}; - } - const int minutes = sunriseSetLocalMinutes(true, localDate.year, localDate.month, localDate.day, latitude, longitude, offsetMinutes); - return buildSunCycleResult(minutes, offsetMinutes, localDate, *this); -} - -SunCycleResult ESPDate::sunset(float latitude, - float longitude, - float timezoneHours, - bool isDst, - const DateTime& day) const { - if (!validCoordinates(latitude, longitude)) { - return SunCycleResult{false, DateTime{}}; - } - const double offsetMinutes = static_cast(timezoneHours) * 60.0 + (isDst ? 60.0 : 0.0); - const int offsetSeconds = static_cast(std::llround(offsetMinutes * Utils::kSecondsPerMinute)); - LocalDateResult localDate = deriveLocalDateWithOffset(day, offsetSeconds); - if (!localDate.ok) { - return SunCycleResult{false, DateTime{}}; - } - const int minutes = sunriseSetLocalMinutes(false, localDate.year, localDate.month, localDate.day, latitude, longitude, offsetMinutes); - return buildSunCycleResult(minutes, offsetMinutes, localDate, *this); -} - -SunCycleResult ESPDate::sunrise(float latitude, float longitude, const char* timeZone) const { - return sunrise(latitude, longitude, timeZone, now()); -} - -SunCycleResult ESPDate::sunset(float latitude, float longitude, const char* timeZone) const { - return sunset(latitude, longitude, timeZone, now()); -} - -SunCycleResult ESPDate::sunrise(float latitude, float longitude, const char* timeZone, const DateTime& day) const { - if (!validCoordinates(latitude, longitude)) { - return SunCycleResult{false, DateTime{}}; - } - OffsetDateResult data = computeOffsetAndDate(day, timeZone, usePSRAMBuffers_); - if (!data.date.ok) { - return SunCycleResult{false, DateTime{}}; - } - const int minutes = sunriseSetLocalMinutes(true, data.date.year, data.date.month, data.date.day, latitude, longitude, data.offsetMinutes); - return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); -} - -SunCycleResult ESPDate::sunset(float latitude, float longitude, const char* timeZone, const DateTime& day) const { - if (!validCoordinates(latitude, longitude)) { - return SunCycleResult{false, DateTime{}}; - } - OffsetDateResult data = computeOffsetAndDate(day, timeZone, usePSRAMBuffers_); - if (!data.date.ok) { - return SunCycleResult{false, DateTime{}}; - } - const int minutes = sunriseSetLocalMinutes(false, data.date.year, data.date.month, data.date.day, latitude, longitude, data.offsetMinutes); - return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); -} - -SunCycleResult ESPDate::sunriseFromConfig(const DateTime& day) const { - if (!hasLocation_) { - return SunCycleResult{false, DateTime{}}; - } - if (!validCoordinates(latitude_, longitude_)) { - return SunCycleResult{false, DateTime{}}; - } - const char* tz = timeZone_.empty() ? nullptr : timeZone_.c_str(); - OffsetDateResult data = computeOffsetAndDate(day, tz, usePSRAMBuffers_); - if (!data.date.ok) { - return SunCycleResult{false, DateTime{}}; - } - const int minutes = sunriseSetLocalMinutes(true, data.date.year, data.date.month, data.date.day, latitude_, longitude_, data.offsetMinutes); - return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); -} - -SunCycleResult ESPDate::sunsetFromConfig(const DateTime& day) const { - if (!hasLocation_) { - return SunCycleResult{false, DateTime{}}; - } - if (!validCoordinates(latitude_, longitude_)) { - return SunCycleResult{false, DateTime{}}; - } - const char* tz = timeZone_.empty() ? nullptr : timeZone_.c_str(); - OffsetDateResult data = computeOffsetAndDate(day, tz, usePSRAMBuffers_); - if (!data.date.ok) { - return SunCycleResult{false, DateTime{}}; - } - const int minutes = sunriseSetLocalMinutes(false, data.date.year, data.date.month, data.date.day, latitude_, longitude_, data.offsetMinutes); - return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); + return sunset(now()); +} + +SunCycleResult ESPDate::sunrise(const DateTime &day) const { + return sunriseFromConfig(day); +} + +SunCycleResult ESPDate::sunset(const DateTime &day) const { + return sunsetFromConfig(day); +} + +SunCycleResult +ESPDate::sunrise(float latitude, float longitude, float timezoneHours, bool isDst) const { + return sunrise(latitude, longitude, timezoneHours, isDst, now()); +} + +SunCycleResult +ESPDate::sunset(float latitude, float longitude, float timezoneHours, bool isDst) const { + return sunset(latitude, longitude, timezoneHours, isDst, now()); +} + +SunCycleResult ESPDate::sunrise( + float latitude, float longitude, float timezoneHours, bool isDst, const DateTime &day +) const { + if (!validCoordinates(latitude, longitude)) { + return SunCycleResult{false, DateTime{}}; + } + const double offsetMinutes = static_cast(timezoneHours) * 60.0 + (isDst ? 60.0 : 0.0); + const int offsetSeconds = + static_cast(std::llround(offsetMinutes * Utils::kSecondsPerMinute)); + LocalDateResult localDate = deriveLocalDateWithOffset(day, offsetSeconds); + if (!localDate.ok) { + return SunCycleResult{false, DateTime{}}; + } + const int minutes = sunriseSetLocalMinutes( + true, + localDate.year, + localDate.month, + localDate.day, + latitude, + longitude, + offsetMinutes + ); + return buildSunCycleResult(minutes, offsetMinutes, localDate, *this); +} + +SunCycleResult ESPDate::sunset( + float latitude, float longitude, float timezoneHours, bool isDst, const DateTime &day +) const { + if (!validCoordinates(latitude, longitude)) { + return SunCycleResult{false, DateTime{}}; + } + const double offsetMinutes = static_cast(timezoneHours) * 60.0 + (isDst ? 60.0 : 0.0); + const int offsetSeconds = + static_cast(std::llround(offsetMinutes * Utils::kSecondsPerMinute)); + LocalDateResult localDate = deriveLocalDateWithOffset(day, offsetSeconds); + if (!localDate.ok) { + return SunCycleResult{false, DateTime{}}; + } + const int minutes = sunriseSetLocalMinutes( + false, + localDate.year, + localDate.month, + localDate.day, + latitude, + longitude, + offsetMinutes + ); + return buildSunCycleResult(minutes, offsetMinutes, localDate, *this); +} + +SunCycleResult ESPDate::sunrise(float latitude, float longitude, const char *timeZone) const { + return sunrise(latitude, longitude, timeZone, now()); +} + +SunCycleResult ESPDate::sunset(float latitude, float longitude, const char *timeZone) const { + return sunset(latitude, longitude, timeZone, now()); +} + +SunCycleResult +ESPDate::sunrise(float latitude, float longitude, const char *timeZone, const DateTime &day) const { + if (!validCoordinates(latitude, longitude)) { + return SunCycleResult{false, DateTime{}}; + } + OffsetDateResult data = computeOffsetAndDate(day, timeZone, usePSRAMBuffers_); + if (!data.date.ok) { + return SunCycleResult{false, DateTime{}}; + } + const int minutes = sunriseSetLocalMinutes( + true, + data.date.year, + data.date.month, + data.date.day, + latitude, + longitude, + data.offsetMinutes + ); + return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); +} + +SunCycleResult +ESPDate::sunset(float latitude, float longitude, const char *timeZone, const DateTime &day) const { + if (!validCoordinates(latitude, longitude)) { + return SunCycleResult{false, DateTime{}}; + } + OffsetDateResult data = computeOffsetAndDate(day, timeZone, usePSRAMBuffers_); + if (!data.date.ok) { + return SunCycleResult{false, DateTime{}}; + } + const int minutes = sunriseSetLocalMinutes( + false, + data.date.year, + data.date.month, + data.date.day, + latitude, + longitude, + data.offsetMinutes + ); + return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); +} + +SunCycleResult ESPDate::sunriseFromConfig(const DateTime &day) const { + if (!hasLocation_) { + return SunCycleResult{false, DateTime{}}; + } + if (!validCoordinates(latitude_, longitude_)) { + return SunCycleResult{false, DateTime{}}; + } + const char *tz = timeZone_.empty() ? nullptr : timeZone_.c_str(); + OffsetDateResult data = computeOffsetAndDate(day, tz, usePSRAMBuffers_); + if (!data.date.ok) { + return SunCycleResult{false, DateTime{}}; + } + const int minutes = sunriseSetLocalMinutes( + true, + data.date.year, + data.date.month, + data.date.day, + latitude_, + longitude_, + data.offsetMinutes + ); + return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); +} + +SunCycleResult ESPDate::sunsetFromConfig(const DateTime &day) const { + if (!hasLocation_) { + return SunCycleResult{false, DateTime{}}; + } + if (!validCoordinates(latitude_, longitude_)) { + return SunCycleResult{false, DateTime{}}; + } + const char *tz = timeZone_.empty() ? nullptr : timeZone_.c_str(); + OffsetDateResult data = computeOffsetAndDate(day, tz, usePSRAMBuffers_); + if (!data.date.ok) { + return SunCycleResult{false, DateTime{}}; + } + const int minutes = sunriseSetLocalMinutes( + false, + data.date.year, + data.date.month, + data.date.day, + latitude_, + longitude_, + data.offsetMinutes + ); + return buildSunCycleResult(minutes, data.offsetMinutes, data.date, *this); } bool ESPDate::isDay() const { - return isDayWithOffsets(now(), 0, 0); + return isDayWithOffsets(now(), 0, 0); } -bool ESPDate::isDay(const DateTime& day) const { - return isDayWithOffsets(day, 0, 0); +bool ESPDate::isDay(const DateTime &day) const { + return isDayWithOffsets(day, 0, 0); } bool ESPDate::isDay(int sunRiseOffsetSec, int sunSetOffsetSec) const { - return isDayWithOffsets(now(), sunRiseOffsetSec, sunSetOffsetSec); + return isDayWithOffsets(now(), sunRiseOffsetSec, sunSetOffsetSec); } -bool ESPDate::isDay(int sunRiseOffsetSec, int sunSetOffsetSec, const DateTime& day) const { - return isDayWithOffsets(day, sunRiseOffsetSec, sunSetOffsetSec); +bool ESPDate::isDay(int sunRiseOffsetSec, int sunSetOffsetSec, const DateTime &day) const { + return isDayWithOffsets(day, sunRiseOffsetSec, sunSetOffsetSec); } -bool ESPDate::isDayWithOffsets(const DateTime& day, int sunRiseOffsetSec, int sunSetOffsetSec) const { - if (!hasLocation_ || !validCoordinates(latitude_, longitude_)) { - return false; - } +bool ESPDate::isDayWithOffsets( + const DateTime &day, int sunRiseOffsetSec, int sunSetOffsetSec +) const { + if (!hasLocation_ || !validCoordinates(latitude_, longitude_)) { + return false; + } - SunCycleResult rise = sunriseFromConfig(day); - SunCycleResult set = sunsetFromConfig(day); - if (!rise.ok || !set.ok) { - return false; - } + SunCycleResult rise = sunriseFromConfig(day); + SunCycleResult set = sunsetFromConfig(day); + if (!rise.ok || !set.ok) { + return false; + } - DateTime start = addSeconds(rise.value, sunRiseOffsetSec); - DateTime end = addSeconds(set.value, sunSetOffsetSec); - if (isAfter(start, end)) { - return false; - } + DateTime start = addSeconds(rise.value, sunRiseOffsetSec); + DateTime end = addSeconds(set.value, sunSetOffsetSec); + if (isAfter(start, end)) { + return false; + } - const bool afterStart = !isBefore(day, start); - const bool beforeEnd = !isAfter(day, end); - return afterStart && beforeEnd; + const bool afterStart = !isBefore(day, start); + const bool beforeEnd = !isAfter(day, end); + return afterStart && beforeEnd; } diff --git a/src/esp_date/utils.h b/src/esp_date/utils.h index fc0791f..a3a05ef 100644 --- a/src/esp_date/utils.h +++ b/src/esp_date/utils.h @@ -1,176 +1,177 @@ #pragma once -#include "date_allocator.h" #include "date.h" +#include "date_allocator.h" #include #include #include class ESPDateUtils { - public: - static constexpr int64_t kSecondsPerMinute = 60; - static constexpr int64_t kSecondsPerHour = 60 * kSecondsPerMinute; - static constexpr int64_t kSecondsPerDay = 24 * kSecondsPerHour; - - class ScopedTz { - public: - explicit ScopedTz(const char* tz, bool usePSRAMBuffers = false) : previous_(DateAllocator(usePSRAMBuffers)) { - if (!tz) { - return; - } - - const char* current = std::getenv("TZ"); - if (current) { - hadPrevious_ = true; - previous_ = current; - } + public: + static constexpr int64_t kSecondsPerMinute = 60; + static constexpr int64_t kSecondsPerHour = 60 * kSecondsPerMinute; + static constexpr int64_t kSecondsPerDay = 24 * kSecondsPerHour; + + class ScopedTz { + public: + explicit ScopedTz(const char *tz, bool usePSRAMBuffers = false) + : previous_(DateAllocator(usePSRAMBuffers)) { + if (!tz) { + return; + } + + const char *current = std::getenv("TZ"); + if (current) { + hadPrevious_ = true; + previous_ = current; + } #if defined(_WIN32) - _putenv_s("TZ", tz); - _tzset(); + _putenv_s("TZ", tz); + _tzset(); #else - setenv("TZ", tz, 1); - tzset(); + setenv("TZ", tz, 1); + tzset(); #endif - active_ = true; - } + active_ = true; + } - ~ScopedTz() { - if (!active_) { - return; - } + ~ScopedTz() { + if (!active_) { + return; + } - if (hadPrevious_) { + if (hadPrevious_) { #if defined(_WIN32) - _putenv_s("TZ", previous_.c_str()); + _putenv_s("TZ", previous_.c_str()); #else - setenv("TZ", previous_.c_str(), 1); + setenv("TZ", previous_.c_str(), 1); #endif - } else { + } else { #if defined(_WIN32) - _putenv_s("TZ", ""); + _putenv_s("TZ", ""); #else - unsetenv("TZ"); + unsetenv("TZ"); #endif - } + } #if defined(_WIN32) - _tzset(); + _tzset(); #else - tzset(); + tzset(); #endif - } - - private: - bool active_ = false; - bool hadPrevious_ = false; - DateString previous_; - }; - - static bool toUtcTm(const DateTime& dt, tm& out) { - if (dt.epochSeconds > static_cast(std::numeric_limits::max()) || - dt.epochSeconds < static_cast(std::numeric_limits::min())) { - return false; - } - time_t raw = static_cast(dt.epochSeconds); + } + + private: + bool active_ = false; + bool hadPrevious_ = false; + DateString previous_; + }; + + static bool toUtcTm(const DateTime &dt, tm &out) { + if (dt.epochSeconds > static_cast(std::numeric_limits::max()) || + dt.epochSeconds < static_cast(std::numeric_limits::min())) { + return false; + } + time_t raw = static_cast(dt.epochSeconds); #if defined(_WIN32) - return gmtime_s(&out, &raw) == 0; + return gmtime_s(&out, &raw) == 0; #else - return gmtime_r(&raw, &out) != nullptr; + return gmtime_r(&raw, &out) != nullptr; #endif - } - - static bool toLocalTm(const DateTime& dt, tm& out) { - if (dt.epochSeconds > static_cast(std::numeric_limits::max()) || - dt.epochSeconds < static_cast(std::numeric_limits::min())) { - return false; - } - time_t raw = static_cast(dt.epochSeconds); + } + + static bool toLocalTm(const DateTime &dt, tm &out) { + if (dt.epochSeconds > static_cast(std::numeric_limits::max()) || + dt.epochSeconds < static_cast(std::numeric_limits::min())) { + return false; + } + time_t raw = static_cast(dt.epochSeconds); #if defined(_WIN32) - return localtime_s(&out, &raw) == 0; + return localtime_s(&out, &raw) == 0; #else - return localtime_r(&raw, &out) != nullptr; + return localtime_r(&raw, &out) != nullptr; #endif - } - - static DateTime fromUtcTm(const tm& t) { - return DateTime{timegm64(t)}; - } - - static DateTime fromLocalTm(const tm& t) { - tm copy = t; - time_t raw = mktime(©); - return DateTime{static_cast(raw)}; - } - - static int64_t timegm64(const tm& t) { - const int year = t.tm_year + 1900; - const unsigned month = static_cast(t.tm_mon + 1); - const unsigned day = static_cast(t.tm_mday); - - const int64_t days = daysFromCivil(year, month, day); - const int64_t seconds = days * kSecondsPerDay + - static_cast(t.tm_hour) * kSecondsPerHour + - static_cast(t.tm_min) * kSecondsPerMinute + - static_cast(t.tm_sec); - return seconds; - } - - static bool validHms(int hour, int minute, int second) { - return hour >= 0 && hour < 24 && minute >= 0 && minute < 60 && second >= 0 && second < 60; - } - - static int clampDay(int year, int month, int day, const ESPDate& dateHelper) { - const int maxDay = dateHelper.daysInMonth(year, month); - if (maxDay <= 0) { - return day; - } - if (day < 1) { - return 1; - } - if (day > maxDay) { - return maxDay; - } - return day; - } - - static bool isDstActiveFor(const DateTime& dt, const char* timeZone, bool usePSRAMBuffers = false) { - ScopedTz scoped(timeZone, usePSRAMBuffers); - tm local{}; - if (!toLocalTm(dt, local)) { - return false; - } - return local.tm_isdst > 0; - } - - static bool parseIntSlice(const char* str, int length, int min, int max, int& out) { - if (!str || length <= 0) { - return false; - } - - int value = 0; - for (int i = 0; i < length; ++i) { - const char c = str[i]; - if (c < '0' || c > '9') { - return false; - } - value = value * 10 + (c - '0'); - } - - if (value < min || value > max) { - return false; - } - out = value; - return true; - } - - private: - static int64_t daysFromCivil(int year, unsigned month, unsigned day) { - year -= month <= 2; - const int era = (year >= 0 ? year : year - 399) / 400; - const unsigned yoe = static_cast(year - era * 400); - const unsigned doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; - const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; - return era * 146097 + static_cast(doe) - 719468; - } + } + + static DateTime fromUtcTm(const tm &t) { + return DateTime{timegm64(t)}; + } + + static DateTime fromLocalTm(const tm &t) { + tm copy = t; + time_t raw = mktime(©); + return DateTime{static_cast(raw)}; + } + + static int64_t timegm64(const tm &t) { + const int year = t.tm_year + 1900; + const unsigned month = static_cast(t.tm_mon + 1); + const unsigned day = static_cast(t.tm_mday); + + const int64_t days = daysFromCivil(year, month, day); + const int64_t seconds = + days * kSecondsPerDay + static_cast(t.tm_hour) * kSecondsPerHour + + static_cast(t.tm_min) * kSecondsPerMinute + static_cast(t.tm_sec); + return seconds; + } + + static bool validHms(int hour, int minute, int second) { + return hour >= 0 && hour < 24 && minute >= 0 && minute < 60 && second >= 0 && second < 60; + } + + static int clampDay(int year, int month, int day, const ESPDate &dateHelper) { + const int maxDay = dateHelper.daysInMonth(year, month); + if (maxDay <= 0) { + return day; + } + if (day < 1) { + return 1; + } + if (day > maxDay) { + return maxDay; + } + return day; + } + + static bool + isDstActiveFor(const DateTime &dt, const char *timeZone, bool usePSRAMBuffers = false) { + ScopedTz scoped(timeZone, usePSRAMBuffers); + tm local{}; + if (!toLocalTm(dt, local)) { + return false; + } + return local.tm_isdst > 0; + } + + static bool parseIntSlice(const char *str, int length, int min, int max, int &out) { + if (!str || length <= 0) { + return false; + } + + int value = 0; + for (int i = 0; i < length; ++i) { + const char c = str[i]; + if (c < '0' || c > '9') { + return false; + } + value = value * 10 + (c - '0'); + } + + if (value < min || value > max) { + return false; + } + out = value; + return true; + } + + private: + static int64_t daysFromCivil(int year, unsigned month, unsigned day) { + year -= month <= 2; + const int era = (year >= 0 ? year : year - 399) / 400; + const unsigned yoe = static_cast(year - era * 400); + const unsigned doy = (153 * (month + (month > 2 ? -3 : 9)) + 2) / 5 + day - 1; + const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + return era * 146097 + static_cast(doe) - 719468; + } }; diff --git a/test/test_esp_date/test_esp_date.cpp b/test/test_esp_date/test_esp_date.cpp index ba4d80e..22934e4 100644 --- a/test/test_esp_date/test_esp_date.cpp +++ b/test/test_esp_date/test_esp_date.cpp @@ -8,13 +8,13 @@ #include #if defined(__has_include) -# if __has_include() || __has_include() -# define TEST_ESPDATE_HAS_CONFIG_TZ_TIME 1 -# else -# define TEST_ESPDATE_HAS_CONFIG_TZ_TIME 0 -# endif +#if __has_include() || __has_include() +#define TEST_ESPDATE_HAS_CONFIG_TZ_TIME 1 #else -# define TEST_ESPDATE_HAS_CONFIG_TZ_TIME 0 +#define TEST_ESPDATE_HAS_CONFIG_TZ_TIME 0 +#endif +#else +#define TEST_ESPDATE_HAS_CONFIG_TZ_TIME 0 #endif ESPDate date; @@ -23,422 +23,432 @@ static const float kBudapestLon = 19.0402f; static bool expected_sync_result_with_any_ntp_server() { #if TEST_ESPDATE_HAS_CONFIG_TZ_TIME - return true; + return true; #else - return false; + return false; #endif } static void test_deinit_is_safe_before_init() { - ESPDate monitor; - TEST_ASSERT_FALSE(monitor.isInitialized()); + ESPDate monitor; + TEST_ASSERT_FALSE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); } static void test_deinit_is_idempotent() { - ESPDate monitor; - monitor.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); - TEST_ASSERT_TRUE(monitor.isInitialized()); + ESPDate monitor; + monitor.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); + TEST_ASSERT_TRUE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); } static void test_reinit_after_deinit() { - ESPDate monitor; - monitor.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); - TEST_ASSERT_TRUE(monitor.isInitialized()); + ESPDate monitor; + monitor.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); + TEST_ASSERT_TRUE(monitor.isInitialized()); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); - monitor.init(ESPDateConfig{kBudapestLat, kBudapestLon, "CET-1CEST,M3.5.0/2,M10.5.0/3", nullptr}); - TEST_ASSERT_TRUE(monitor.isInitialized()); - TEST_ASSERT_TRUE(monitor.sunrise(monitor.fromUtc(2024, 6, 1)).ok); + monitor.init( + ESPDateConfig{kBudapestLat, kBudapestLon, "CET-1CEST,M3.5.0/2,M10.5.0/3", nullptr} + ); + TEST_ASSERT_TRUE(monitor.isInitialized()); + TEST_ASSERT_TRUE(monitor.sunrise(monitor.fromUtc(2024, 6, 1)).ok); - monitor.deinit(); - TEST_ASSERT_FALSE(monitor.isInitialized()); + monitor.deinit(); + TEST_ASSERT_FALSE(monitor.isInitialized()); } static void test_add_days_and_differences() { - DateTime base = date.fromUnixSeconds(1704067200); // 2024-01-01T00:00:00Z - DateTime plus = date.addDays(base, 1); - DateTime minus = date.subDays(base, 1); - - TEST_ASSERT_EQUAL(2024, date.getYearUtc(plus)); - TEST_ASSERT_EQUAL(1, date.getMonthUtc(plus)); - TEST_ASSERT_EQUAL(2, date.getDayUtc(plus)); - - TEST_ASSERT_EQUAL(2023, date.getYearUtc(minus)); - TEST_ASSERT_EQUAL(12, date.getMonthUtc(minus)); - TEST_ASSERT_EQUAL(31, date.getDayUtc(minus)); - - TEST_ASSERT_EQUAL_INT(1, date.differenceInDays(plus, base)); - TEST_ASSERT_EQUAL_INT(-1, date.differenceInDays(minus, base)); - TEST_ASSERT_TRUE(date.isAfter(plus, base)); - TEST_ASSERT_TRUE(date.isBefore(minus, base)); + DateTime base = date.fromUnixSeconds(1704067200); // 2024-01-01T00:00:00Z + DateTime plus = date.addDays(base, 1); + DateTime minus = date.subDays(base, 1); + + TEST_ASSERT_EQUAL(2024, date.getYearUtc(plus)); + TEST_ASSERT_EQUAL(1, date.getMonthUtc(plus)); + TEST_ASSERT_EQUAL(2, date.getDayUtc(plus)); + + TEST_ASSERT_EQUAL(2023, date.getYearUtc(minus)); + TEST_ASSERT_EQUAL(12, date.getMonthUtc(minus)); + TEST_ASSERT_EQUAL(31, date.getDayUtc(minus)); + + TEST_ASSERT_EQUAL_INT(1, date.differenceInDays(plus, base)); + TEST_ASSERT_EQUAL_INT(-1, date.differenceInDays(minus, base)); + TEST_ASSERT_TRUE(date.isAfter(plus, base)); + TEST_ASSERT_TRUE(date.isBefore(minus, base)); } static void test_add_months_clamps_day_in_leap_year() { - DateTime jan31 = date.fromUnixSeconds(1706659200); // 2024-01-31T00:00:00Z - DateTime feb = date.addMonths(jan31, 1); + DateTime jan31 = date.fromUnixSeconds(1706659200); // 2024-01-31T00:00:00Z + DateTime feb = date.addMonths(jan31, 1); - TEST_ASSERT_EQUAL(2024, date.getYearUtc(feb)); - TEST_ASSERT_EQUAL(2, date.getMonthUtc(feb)); - TEST_ASSERT_EQUAL(29, date.getDayUtc(feb)); // clamps to Feb 29 on leap year + TEST_ASSERT_EQUAL(2024, date.getYearUtc(feb)); + TEST_ASSERT_EQUAL(2, date.getMonthUtc(feb)); + TEST_ASSERT_EQUAL(29, date.getDayUtc(feb)); // clamps to Feb 29 on leap year } static void test_start_and_end_of_day_utc() { - DateTime midday = date.fromUnixSeconds(1709652610); // 2024-03-05T15:30:10Z - DateTime start = date.startOfDayUtc(midday); - DateTime end = date.endOfDayUtc(midday); - - TEST_ASSERT_EQUAL(0, start.hourUtc()); - TEST_ASSERT_EQUAL(0, start.minuteUtc()); - TEST_ASSERT_EQUAL(0, start.secondUtc()); - - TEST_ASSERT_EQUAL(23, end.hourUtc()); - TEST_ASSERT_EQUAL(59, end.minuteUtc()); - TEST_ASSERT_EQUAL(59, end.secondUtc()); - TEST_ASSERT_TRUE(date.isSameDay(midday, start)); - TEST_ASSERT_TRUE(date.isSameDay(midday, end)); + DateTime midday = date.fromUnixSeconds(1709652610); // 2024-03-05T15:30:10Z + DateTime start = date.startOfDayUtc(midday); + DateTime end = date.endOfDayUtc(midday); + + TEST_ASSERT_EQUAL(0, start.hourUtc()); + TEST_ASSERT_EQUAL(0, start.minuteUtc()); + TEST_ASSERT_EQUAL(0, start.secondUtc()); + + TEST_ASSERT_EQUAL(23, end.hourUtc()); + TEST_ASSERT_EQUAL(59, end.minuteUtc()); + TEST_ASSERT_EQUAL(59, end.secondUtc()); + TEST_ASSERT_TRUE(date.isSameDay(midday, start)); + TEST_ASSERT_TRUE(date.isSameDay(midday, end)); } static void test_parse_and_format_iso_utc() { - ESPDate::ParseResult parsed = date.parseIso8601Utc("2025-01-01T00:00:00Z"); - TEST_ASSERT_TRUE(parsed.ok); - TEST_ASSERT_TRUE(date.isEqual(parsed.value, date.fromUnixSeconds(1735689600))); - - char buf[32]; - DateTime moment = date.fromUnixSeconds(1767225570); // 2025-12-31T23:59:30Z - TEST_ASSERT_TRUE(date.formatUtc(moment, ESPDateFormat::Iso8601, buf, sizeof(buf))); - TEST_ASSERT_EQUAL_STRING("2025-12-31T23:59:30Z", buf); + ESPDate::ParseResult parsed = date.parseIso8601Utc("2025-01-01T00:00:00Z"); + TEST_ASSERT_TRUE(parsed.ok); + TEST_ASSERT_TRUE(date.isEqual(parsed.value, date.fromUnixSeconds(1735689600))); + + char buf[32]; + DateTime moment = date.fromUnixSeconds(1767225570); // 2025-12-31T23:59:30Z + TEST_ASSERT_TRUE(date.formatUtc(moment, ESPDateFormat::Iso8601, buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("2025-12-31T23:59:30Z", buf); } static void test_from_utc_clamps_day() { - DateTime dt = date.fromUtc(2025, 2, 30); - TEST_ASSERT_EQUAL(2025, date.getYearUtc(dt)); - TEST_ASSERT_EQUAL(2, date.getMonthUtc(dt)); - TEST_ASSERT_EQUAL(28, date.getDayUtc(dt)); - TEST_ASSERT_TRUE(date.isEqual(dt, date.fromUnixSeconds(1740700800))); // 2025-02-28T00:00:00Z + DateTime dt = date.fromUtc(2025, 2, 30); + TEST_ASSERT_EQUAL(2025, date.getYearUtc(dt)); + TEST_ASSERT_EQUAL(2, date.getMonthUtc(dt)); + TEST_ASSERT_EQUAL(28, date.getDayUtc(dt)); + TEST_ASSERT_TRUE(date.isEqual(dt, date.fromUnixSeconds(1740700800))); // 2025-02-28T00:00:00Z } static void test_start_of_year_helpers() { - DateTime mid = date.fromUnixSeconds(1709652610); // 2024-03-05T15:30:10Z - DateTime startUtc = date.startOfYearUtc(mid); - DateTime startLocal = date.startOfYearLocal(mid); - DateTime expected = date.fromUnixSeconds(1704067200); // 2024-01-01T00:00:00Z - TEST_ASSERT_TRUE(date.isEqual(startUtc, expected)); - TEST_ASSERT_TRUE(date.isEqual(startLocal, expected)); + DateTime mid = date.fromUnixSeconds(1709652610); // 2024-03-05T15:30:10Z + DateTime startUtc = date.startOfYearUtc(mid); + DateTime startLocal = date.startOfYearLocal(mid); + DateTime expected = date.fromUnixSeconds(1704067200); // 2024-01-01T00:00:00Z + TEST_ASSERT_TRUE(date.isEqual(startUtc, expected)); + TEST_ASSERT_TRUE(date.isEqual(startLocal, expected)); } static void test_next_daily_and_weekday_local() { - DateTime before = date.fromUnixSeconds(1741157940); // 2025-03-05T06:59:00Z - DateTime atEight = date.nextDailyAtLocal(8, 0, 0, before); - TEST_ASSERT_TRUE(date.isEqual(atEight, date.fromUnixSeconds(1741161600))); // same day at 08:00 - - DateTime monday930 = date.nextWeekdayAtLocal(1, 9, 30, 0, before); // 1 = Monday - TEST_ASSERT_TRUE(date.isEqual(monday930, date.fromUnixSeconds(1741599000))); // 2025-03-10T09:30:00Z + DateTime before = date.fromUnixSeconds(1741157940); // 2025-03-05T06:59:00Z + DateTime atEight = date.nextDailyAtLocal(8, 0, 0, before); + TEST_ASSERT_TRUE(date.isEqual(atEight, date.fromUnixSeconds(1741161600))); // same day at 08:00 + + DateTime monday930 = date.nextWeekdayAtLocal(1, 9, 30, 0, before); // 1 = Monday + TEST_ASSERT_TRUE( + date.isEqual(monday930, date.fromUnixSeconds(1741599000)) + ); // 2025-03-10T09:30:00Z } static void test_sunrise_config_matches_manual() { - ESPDate configured; - configured.init(ESPDateConfig{kBudapestLat, kBudapestLon, "CET-1CEST,M3.5.0/2,M10.5.0/3"}); - DateTime day = configured.fromUtc(2024, 6, 1); + ESPDate configured; + configured.init(ESPDateConfig{kBudapestLat, kBudapestLon, "CET-1CEST,M3.5.0/2,M10.5.0/3"}); + DateTime day = configured.fromUtc(2024, 6, 1); - SunCycleResult cfgRise = configured.sunrise(day); - SunCycleResult cfgSet = configured.sunset(day); - SunCycleResult manualRise = date.sunrise(kBudapestLat, kBudapestLon, 1.0f, true, day); // CEST - SunCycleResult manualSet = date.sunset(kBudapestLat, kBudapestLon, 1.0f, true, day); + SunCycleResult cfgRise = configured.sunrise(day); + SunCycleResult cfgSet = configured.sunset(day); + SunCycleResult manualRise = date.sunrise(kBudapestLat, kBudapestLon, 1.0f, true, day); // CEST + SunCycleResult manualSet = date.sunset(kBudapestLat, kBudapestLon, 1.0f, true, day); - TEST_ASSERT_TRUE(cfgRise.ok); - TEST_ASSERT_TRUE(cfgSet.ok); - TEST_ASSERT_TRUE(manualRise.ok); - TEST_ASSERT_TRUE(manualSet.ok); + TEST_ASSERT_TRUE(cfgRise.ok); + TEST_ASSERT_TRUE(cfgSet.ok); + TEST_ASSERT_TRUE(manualRise.ok); + TEST_ASSERT_TRUE(manualSet.ok); - int64_t riseDelta = date.differenceInMinutes(cfgRise.value, manualRise.value); - int64_t setDelta = date.differenceInMinutes(cfgSet.value, manualSet.value); + int64_t riseDelta = date.differenceInMinutes(cfgRise.value, manualRise.value); + int64_t setDelta = date.differenceInMinutes(cfgSet.value, manualSet.value); - TEST_ASSERT_TRUE(llabs(riseDelta) <= 2); - TEST_ASSERT_TRUE(llabs(setDelta) <= 2); + TEST_ASSERT_TRUE(llabs(riseDelta) <= 2); + TEST_ASSERT_TRUE(llabs(setDelta) <= 2); - setenv("TZ", "UTC", 1); - tzset(); + setenv("TZ", "UTC", 1); + tzset(); } static void test_is_day_helpers() { - ESPDate solar; - solar.init(ESPDateConfig{kBudapestLat, kBudapestLon, "CET-1CEST,M3.5.0/2,M10.5.0/3"}); - DateTime day = solar.fromUtc(2024, 6, 1); - SunCycleResult rise = solar.sunrise(day); - SunCycleResult set = solar.sunset(day); - TEST_ASSERT_TRUE(rise.ok); - TEST_ASSERT_TRUE(set.ok); - - DateTime morning = solar.addMinutes(rise.value, 30); - DateTime night = solar.subMinutes(rise.value, 30); - TEST_ASSERT_TRUE(solar.isDay(morning)); - TEST_ASSERT_FALSE(solar.isDay(night)); - - int sunriseOffset = -900; // 15 minutes before sunrise counts as day - DateTime preDawn = solar.subMinutes(rise.value, 10); - TEST_ASSERT_TRUE(solar.isDay(sunriseOffset, 0, preDawn)); - TEST_ASSERT_FALSE(solar.isDay(0, 0, preDawn)); - - // Large negative sunset offset should end the day earlier - int sunsetOffset = -3600; // end one hour earlier - DateTime beforeEarlyEnd = solar.subMinutes(set.value, 30); - TEST_ASSERT_TRUE(solar.isDay(0, sunsetOffset, beforeEarlyEnd)); - DateTime afterEarlyEnd = solar.subMinutes(set.value, -10); - TEST_ASSERT_FALSE(solar.isDay(0, sunsetOffset, afterEarlyEnd)); - - setenv("TZ", "UTC", 1); - tzset(); + ESPDate solar; + solar.init(ESPDateConfig{kBudapestLat, kBudapestLon, "CET-1CEST,M3.5.0/2,M10.5.0/3"}); + DateTime day = solar.fromUtc(2024, 6, 1); + SunCycleResult rise = solar.sunrise(day); + SunCycleResult set = solar.sunset(day); + TEST_ASSERT_TRUE(rise.ok); + TEST_ASSERT_TRUE(set.ok); + + DateTime morning = solar.addMinutes(rise.value, 30); + DateTime night = solar.subMinutes(rise.value, 30); + TEST_ASSERT_TRUE(solar.isDay(morning)); + TEST_ASSERT_FALSE(solar.isDay(night)); + + int sunriseOffset = -900; // 15 minutes before sunrise counts as day + DateTime preDawn = solar.subMinutes(rise.value, 10); + TEST_ASSERT_TRUE(solar.isDay(sunriseOffset, 0, preDawn)); + TEST_ASSERT_FALSE(solar.isDay(0, 0, preDawn)); + + // Large negative sunset offset should end the day earlier + int sunsetOffset = -3600; // end one hour earlier + DateTime beforeEarlyEnd = solar.subMinutes(set.value, 30); + TEST_ASSERT_TRUE(solar.isDay(0, sunsetOffset, beforeEarlyEnd)); + DateTime afterEarlyEnd = solar.subMinutes(set.value, -10); + TEST_ASSERT_FALSE(solar.isDay(0, sunsetOffset, afterEarlyEnd)); + + setenv("TZ", "UTC", 1); + tzset(); } static void test_is_dst_active_with_timezone_string() { - DateTime summer = date.fromUtc(2024, 6, 1, 12, 0, 0); - DateTime winter = date.fromUtc(2024, 12, 1, 12, 0, 0); + DateTime summer = date.fromUtc(2024, 6, 1, 12, 0, 0); + DateTime winter = date.fromUtc(2024, 12, 1, 12, 0, 0); - TEST_ASSERT_TRUE(date.isDstActive(summer, "CET-1CEST,M3.5.0/2,M10.5.0/3")); - TEST_ASSERT_FALSE(date.isDstActive(winter, "CET-1CEST,M3.5.0/2,M10.5.0/3")); + TEST_ASSERT_TRUE(date.isDstActive(summer, "CET-1CEST,M3.5.0/2,M10.5.0/3")); + TEST_ASSERT_FALSE(date.isDstActive(winter, "CET-1CEST,M3.5.0/2,M10.5.0/3")); } static void test_is_dst_active_with_configured_timezone() { - ESPDate configured; - configured.init(ESPDateConfig{0.0f, 0.0f, "EST5EDT,M3.2.0/2,M11.1.0/2"}); - DateTime summer = configured.fromUtc(2024, 7, 1, 15, 0, 0); - DateTime winter = configured.fromUtc(2024, 12, 1, 15, 0, 0); + ESPDate configured; + configured.init(ESPDateConfig{0.0f, 0.0f, "EST5EDT,M3.2.0/2,M11.1.0/2"}); + DateTime summer = configured.fromUtc(2024, 7, 1, 15, 0, 0); + DateTime winter = configured.fromUtc(2024, 12, 1, 15, 0, 0); - TEST_ASSERT_TRUE(configured.isDstActive(summer)); - TEST_ASSERT_FALSE(configured.isDstActive(winter)); + TEST_ASSERT_TRUE(configured.isDstActive(summer)); + TEST_ASSERT_FALSE(configured.isDstActive(winter)); } static void test_is_dst_active_with_system_timezone() { - const char* current = getenv("TZ"); - std::string previous = current ? current : ""; - setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1); - tzset(); - - DateTime summer = date.fromUtc(2024, 6, 1, 12, 0, 0); - DateTime winter = date.fromUtc(2024, 1, 15, 12, 0, 0); - - TEST_ASSERT_TRUE(date.isDstActive(summer)); - TEST_ASSERT_FALSE(date.isDstActive(winter)); - - if (previous.empty()) { - unsetenv("TZ"); - } else { - setenv("TZ", previous.c_str(), 1); - } - tzset(); + const char *current = getenv("TZ"); + std::string previous = current ? current : ""; + setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1); + tzset(); + + DateTime summer = date.fromUtc(2024, 6, 1, 12, 0, 0); + DateTime winter = date.fromUtc(2024, 1, 15, 12, 0, 0); + + TEST_ASSERT_TRUE(date.isDstActive(summer)); + TEST_ASSERT_FALSE(date.isDstActive(winter)); + + if (previous.empty()) { + unsetenv("TZ"); + } else { + setenv("TZ", previous.c_str(), 1); + } + tzset(); } static void test_to_local_breakdown() { - setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1); - tzset(); - - DateTime winter = date.fromUtc(2024, 12, 1, 20, 45, 0); // 21:45 CET - LocalDateTime winterLocal = date.toLocal(winter); - TEST_ASSERT_TRUE(winterLocal.ok); - TEST_ASSERT_EQUAL(2024, winterLocal.year); - TEST_ASSERT_EQUAL(12, winterLocal.month); - TEST_ASSERT_EQUAL(1, winterLocal.day); - TEST_ASSERT_EQUAL(21, winterLocal.hour); - TEST_ASSERT_EQUAL(45, winterLocal.minute); - TEST_ASSERT_EQUAL(60, winterLocal.offsetMinutes); // CET = UTC+1 - - DateTime summer = date.fromUtc(2024, 6, 1, 19, 30, 0); // 21:30 CEST - LocalDateTime summerLocal = date.toLocal(summer); - TEST_ASSERT_TRUE(summerLocal.ok); - TEST_ASSERT_EQUAL(2024, summerLocal.year); - TEST_ASSERT_EQUAL(6, summerLocal.month); - TEST_ASSERT_EQUAL(1, summerLocal.day); - TEST_ASSERT_EQUAL(21, summerLocal.hour); - TEST_ASSERT_EQUAL(30, summerLocal.minute); - TEST_ASSERT_EQUAL(120, summerLocal.offsetMinutes); // CEST = UTC+2 + setenv("TZ", "CET-1CEST,M3.5.0/2,M10.5.0/3", 1); + tzset(); + + DateTime winter = date.fromUtc(2024, 12, 1, 20, 45, 0); // 21:45 CET + LocalDateTime winterLocal = date.toLocal(winter); + TEST_ASSERT_TRUE(winterLocal.ok); + TEST_ASSERT_EQUAL(2024, winterLocal.year); + TEST_ASSERT_EQUAL(12, winterLocal.month); + TEST_ASSERT_EQUAL(1, winterLocal.day); + TEST_ASSERT_EQUAL(21, winterLocal.hour); + TEST_ASSERT_EQUAL(45, winterLocal.minute); + TEST_ASSERT_EQUAL(60, winterLocal.offsetMinutes); // CET = UTC+1 + + DateTime summer = date.fromUtc(2024, 6, 1, 19, 30, 0); // 21:30 CEST + LocalDateTime summerLocal = date.toLocal(summer); + TEST_ASSERT_TRUE(summerLocal.ok); + TEST_ASSERT_EQUAL(2024, summerLocal.year); + TEST_ASSERT_EQUAL(6, summerLocal.month); + TEST_ASSERT_EQUAL(1, summerLocal.day); + TEST_ASSERT_EQUAL(21, summerLocal.hour); + TEST_ASSERT_EQUAL(30, summerLocal.minute); + TEST_ASSERT_EQUAL(120, summerLocal.offsetMinutes); // CEST = UTC+2 } static void test_moon_phase_full_and_new_moon() { - MoonPhaseResult full = date.moonPhase(date.fromUtc(2024, 3, 25, 0, 0, 0)); // full moon - TEST_ASSERT_TRUE(full.ok); - TEST_ASSERT_TRUE(full.illumination > 0.95); - TEST_ASSERT_TRUE(full.angleDegrees >= 170 && full.angleDegrees <= 190); - - MoonPhaseResult solarEclipseNew = date.moonPhase(date.fromUtc(2024, 4, 8, 18, 0, 0)); // near new moon - TEST_ASSERT_TRUE(solarEclipseNew.ok); - TEST_ASSERT_TRUE(solarEclipseNew.illumination < 0.05); - TEST_ASSERT_TRUE(solarEclipseNew.angleDegrees < 10 || solarEclipseNew.angleDegrees > 350); + MoonPhaseResult full = date.moonPhase(date.fromUtc(2024, 3, 25, 0, 0, 0)); // full moon + TEST_ASSERT_TRUE(full.ok); + TEST_ASSERT_TRUE(full.illumination > 0.95); + TEST_ASSERT_TRUE(full.angleDegrees >= 170 && full.angleDegrees <= 190); + + MoonPhaseResult solarEclipseNew = + date.moonPhase(date.fromUtc(2024, 4, 8, 18, 0, 0)); // near new moon + TEST_ASSERT_TRUE(solarEclipseNew.ok); + TEST_ASSERT_TRUE(solarEclipseNew.illumination < 0.05); + TEST_ASSERT_TRUE(solarEclipseNew.angleDegrees < 10 || solarEclipseNew.angleDegrees > 350); } static void test_sync_ntp_requires_server_config() { - ESPDate unconfigured; - TEST_ASSERT_FALSE(unconfigured.syncNTP()); + ESPDate unconfigured; + TEST_ASSERT_FALSE(unconfigured.syncNTP()); - ESPDate onlyTimezone; - onlyTimezone.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); - TEST_ASSERT_FALSE(onlyTimezone.syncNTP()); + ESPDate onlyTimezone; + onlyTimezone.init(ESPDateConfig{0.0f, 0.0f, "UTC0", nullptr}); + TEST_ASSERT_FALSE(onlyTimezone.syncNTP()); } static void test_sync_ntp_accepts_secondary_or_tertiary_server_only() { - ESPDate secondaryOnly; - ESPDateConfig secondaryCfg{0.0f, 0.0f, "UTC0", nullptr}; - secondaryCfg.ntpServer2 = "time.google.com"; - secondaryOnly.init(secondaryCfg); - TEST_ASSERT_EQUAL(expected_sync_result_with_any_ntp_server(), secondaryOnly.syncNTP()); - - ESPDate tertiaryOnly; - ESPDateConfig tertiaryCfg{0.0f, 0.0f, "UTC0", nullptr}; - tertiaryCfg.ntpServer3 = "time.cloudflare.com"; - tertiaryOnly.init(tertiaryCfg); - TEST_ASSERT_EQUAL(expected_sync_result_with_any_ntp_server(), tertiaryOnly.syncNTP()); + ESPDate secondaryOnly; + ESPDateConfig secondaryCfg{0.0f, 0.0f, "UTC0", nullptr}; + secondaryCfg.ntpServer2 = "time.google.com"; + secondaryOnly.init(secondaryCfg); + TEST_ASSERT_EQUAL(expected_sync_result_with_any_ntp_server(), secondaryOnly.syncNTP()); + + ESPDate tertiaryOnly; + ESPDateConfig tertiaryCfg{0.0f, 0.0f, "UTC0", nullptr}; + tertiaryCfg.ntpServer3 = "time.cloudflare.com"; + tertiaryOnly.init(tertiaryCfg); + TEST_ASSERT_EQUAL(expected_sync_result_with_any_ntp_server(), tertiaryOnly.syncNTP()); } static void test_sync_ntp_with_three_servers_matches_single_server_behavior() { - ESPDate singleServer; - singleServer.init(ESPDateConfig{0.0f, 0.0f, "UTC0", "pool.ntp.org"}); - const bool singleResult = singleServer.syncNTP(); - TEST_ASSERT_EQUAL(expected_sync_result_with_any_ntp_server(), singleResult); - - ESPDate threeServers; - ESPDateConfig threeServerCfg{0.0f, 0.0f, "UTC0", "pool.ntp.org"}; - threeServerCfg.ntpServer2 = "time.google.com"; - threeServerCfg.ntpServer3 = "time.cloudflare.com"; - threeServers.init(threeServerCfg); - TEST_ASSERT_EQUAL(singleResult, threeServers.syncNTP()); + ESPDate singleServer; + singleServer.init(ESPDateConfig{0.0f, 0.0f, "UTC0", "pool.ntp.org"}); + const bool singleResult = singleServer.syncNTP(); + TEST_ASSERT_EQUAL(expected_sync_result_with_any_ntp_server(), singleResult); + + ESPDate threeServers; + ESPDateConfig threeServerCfg{0.0f, 0.0f, "UTC0", "pool.ntp.org"}; + threeServerCfg.ntpServer2 = "time.google.com"; + threeServerCfg.ntpServer3 = "time.cloudflare.com"; + threeServers.init(threeServerCfg); + TEST_ASSERT_EQUAL(singleResult, threeServers.syncNTP()); } struct NtpSyncTestObserver { - int callCount = 0; - int64_t lastEpoch = 0; + int callCount = 0; + int64_t lastEpoch = 0; - void onSync(const DateTime& syncedAtUtc) { - ++callCount; - lastEpoch = syncedAtUtc.epochSeconds; - } + void onSync(const DateTime &syncedAtUtc) { + ++callCount; + lastEpoch = syncedAtUtc.epochSeconds; + } }; static void test_ntp_callback_registration_supports_member_binding() { - NtpSyncTestObserver observer; - date.setNtpSyncCallback(std::bind(&NtpSyncTestObserver::onSync, &observer, std::placeholders::_1)); - date.setNtpSyncCallback(static_cast(nullptr)); - TEST_ASSERT_EQUAL(0, observer.callCount); // registration-only API coverage + NtpSyncTestObserver observer; + date.setNtpSyncCallback( + std::bind(&NtpSyncTestObserver::onSync, &observer, std::placeholders::_1) + ); + date.setNtpSyncCallback(static_cast(nullptr)); + TEST_ASSERT_EQUAL(0, observer.callCount); // registration-only API coverage } static void test_ntp_sync_interval_setter_accepts_default() { - TEST_ASSERT_TRUE(date.setNtpSyncIntervalMs(0)); + TEST_ASSERT_TRUE(date.setNtpSyncIntervalMs(0)); } static void test_last_ntp_sync_defaults_to_empty() { - ESPDate tracker; - TEST_ASSERT_FALSE(tracker.hasLastNtpSync()); - TEST_ASSERT_EQUAL_INT64(0, tracker.lastNtpSync().epochSeconds); - - char lastSyncBuf[32]; - TEST_ASSERT_FALSE(tracker.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))); - TEST_ASSERT_FALSE(tracker.lastNtpSyncStringUtc(lastSyncBuf, sizeof(lastSyncBuf))); - TEST_ASSERT_TRUE(tracker.lastNtpSyncStringLocal().empty()); - TEST_ASSERT_TRUE(tracker.lastNtpSyncStringUtc().empty()); + ESPDate tracker; + TEST_ASSERT_FALSE(tracker.hasLastNtpSync()); + TEST_ASSERT_EQUAL_INT64(0, tracker.lastNtpSync().epochSeconds); + + char lastSyncBuf[32]; + TEST_ASSERT_FALSE(tracker.lastNtpSyncStringLocal(lastSyncBuf, sizeof(lastSyncBuf))); + TEST_ASSERT_FALSE(tracker.lastNtpSyncStringUtc(lastSyncBuf, sizeof(lastSyncBuf))); + TEST_ASSERT_TRUE(tracker.lastNtpSyncStringLocal().empty()); + TEST_ASSERT_TRUE(tracker.lastNtpSyncStringUtc().empty()); } static void test_string_helpers_for_datetime_and_local_datetime() { - DateTime dt = date.fromUtc(2025, 1, 2, 3, 4, 5); + DateTime dt = date.fromUtc(2025, 1, 2, 3, 4, 5); - char utcBuf[32]; - TEST_ASSERT_TRUE(date.dateTimeToStringUtc(dt, utcBuf, sizeof(utcBuf))); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", utcBuf); + char utcBuf[32]; + TEST_ASSERT_TRUE(date.dateTimeToStringUtc(dt, utcBuf, sizeof(utcBuf))); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", utcBuf); - char directUtcBuf[32]; - TEST_ASSERT_TRUE(dt.utcString(directUtcBuf, sizeof(directUtcBuf))); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directUtcBuf); + char directUtcBuf[32]; + TEST_ASSERT_TRUE(dt.utcString(directUtcBuf, sizeof(directUtcBuf))); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directUtcBuf); - std::string utcString = date.dateTimeToStringUtc(dt); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", utcString.c_str()); - std::string directUtcString = dt.utcString(); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directUtcString.c_str()); + std::string utcString = date.dateTimeToStringUtc(dt); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", utcString.c_str()); + std::string directUtcString = dt.utcString(); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directUtcString.c_str()); - LocalDateTime local = date.toLocal(dt, "UTC0"); - TEST_ASSERT_TRUE(local.ok); + LocalDateTime local = date.toLocal(dt, "UTC0"); + TEST_ASSERT_TRUE(local.ok); - char localBuf[32]; - TEST_ASSERT_TRUE(date.localDateTimeToString(local, localBuf, sizeof(localBuf))); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", localBuf); + char localBuf[32]; + TEST_ASSERT_TRUE(date.localDateTimeToString(local, localBuf, sizeof(localBuf))); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", localBuf); - char directLocalBuf[32]; - TEST_ASSERT_TRUE(local.localString(directLocalBuf, sizeof(directLocalBuf))); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directLocalBuf); + char directLocalBuf[32]; + TEST_ASSERT_TRUE(local.localString(directLocalBuf, sizeof(directLocalBuf))); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directLocalBuf); - std::string localString = date.localDateTimeToString(local); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", localString.c_str()); - std::string directLocalString = local.localString(); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directLocalString.c_str()); + std::string localString = date.localDateTimeToString(local); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", localString.c_str()); + std::string directLocalString = local.localString(); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", directLocalString.c_str()); - char smallBuf[8]; - TEST_ASSERT_FALSE(date.localDateTimeToString(local, smallBuf, sizeof(smallBuf))); - TEST_ASSERT_FALSE(local.localString(smallBuf, sizeof(smallBuf))); + char smallBuf[8]; + TEST_ASSERT_FALSE(date.localDateTimeToString(local, smallBuf, sizeof(smallBuf))); + TEST_ASSERT_FALSE(local.localString(smallBuf, sizeof(smallBuf))); - char nowBuf[32]; - TEST_ASSERT_TRUE(date.nowUtcString(nowBuf, sizeof(nowBuf))); - std::string nowLocal = date.nowLocalString(); - TEST_ASSERT_TRUE(!nowLocal.empty()); + char nowBuf[32]; + TEST_ASSERT_TRUE(date.nowUtcString(nowBuf, sizeof(nowBuf))); + std::string nowLocal = date.nowLocalString(); + TEST_ASSERT_TRUE(!nowLocal.empty()); - DateTime lastYear = date.subYears(1); - char lastYearLocalBuf[32]; - TEST_ASSERT_TRUE(lastYear.localString(lastYearLocalBuf, sizeof(lastYearLocalBuf))); + DateTime lastYear = date.subYears(1); + char lastYearLocalBuf[32]; + TEST_ASSERT_TRUE(lastYear.localString(lastYearLocalBuf, sizeof(lastYearLocalBuf))); } static void test_psram_buffer_policy_toggle_is_safe() { - ESPDate psramDate; - ESPDateConfig cfg{}; - cfg.timeZone = "UTC0"; - cfg.usePSRAMBuffers = true; - psramDate.init(cfg); - - DateTime dt = psramDate.fromUtc(2025, 1, 2, 3, 4, 5); - char buf[32]; - TEST_ASSERT_TRUE(psramDate.formatUtc(dt, ESPDateFormat::DateTime, buf, sizeof(buf))); - TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", buf); + ESPDate psramDate; + ESPDateConfig cfg{}; + cfg.timeZone = "UTC0"; + cfg.usePSRAMBuffers = true; + psramDate.init(cfg); + + DateTime dt = psramDate.fromUtc(2025, 1, 2, 3, 4, 5); + char buf[32]; + TEST_ASSERT_TRUE(psramDate.formatUtc(dt, ESPDateFormat::DateTime, buf, sizeof(buf))); + TEST_ASSERT_EQUAL_STRING("2025-01-02 03:04:05", buf); } -void setUp() {} -void tearDown() {} +void setUp() { +} +void tearDown() { +} void setup() { - setenv("TZ", "UTC", 1); - tzset(); - delay(2000); - UNITY_BEGIN(); - RUN_TEST(test_deinit_is_safe_before_init); - RUN_TEST(test_deinit_is_idempotent); - RUN_TEST(test_reinit_after_deinit); - RUN_TEST(test_add_days_and_differences); - RUN_TEST(test_add_months_clamps_day_in_leap_year); - RUN_TEST(test_start_and_end_of_day_utc); - RUN_TEST(test_parse_and_format_iso_utc); - RUN_TEST(test_from_utc_clamps_day); - RUN_TEST(test_start_of_year_helpers); - RUN_TEST(test_next_daily_and_weekday_local); - RUN_TEST(test_sunrise_config_matches_manual); - RUN_TEST(test_is_day_helpers); - RUN_TEST(test_is_dst_active_with_timezone_string); - RUN_TEST(test_is_dst_active_with_configured_timezone); - RUN_TEST(test_is_dst_active_with_system_timezone); - RUN_TEST(test_to_local_breakdown); - RUN_TEST(test_moon_phase_full_and_new_moon); - RUN_TEST(test_sync_ntp_requires_server_config); - RUN_TEST(test_sync_ntp_accepts_secondary_or_tertiary_server_only); - RUN_TEST(test_sync_ntp_with_three_servers_matches_single_server_behavior); - RUN_TEST(test_ntp_callback_registration_supports_member_binding); - RUN_TEST(test_ntp_sync_interval_setter_accepts_default); - RUN_TEST(test_last_ntp_sync_defaults_to_empty); - RUN_TEST(test_string_helpers_for_datetime_and_local_datetime); - RUN_TEST(test_psram_buffer_policy_toggle_is_safe); - UNITY_END(); + setenv("TZ", "UTC", 1); + tzset(); + delay(2000); + UNITY_BEGIN(); + RUN_TEST(test_deinit_is_safe_before_init); + RUN_TEST(test_deinit_is_idempotent); + RUN_TEST(test_reinit_after_deinit); + RUN_TEST(test_add_days_and_differences); + RUN_TEST(test_add_months_clamps_day_in_leap_year); + RUN_TEST(test_start_and_end_of_day_utc); + RUN_TEST(test_parse_and_format_iso_utc); + RUN_TEST(test_from_utc_clamps_day); + RUN_TEST(test_start_of_year_helpers); + RUN_TEST(test_next_daily_and_weekday_local); + RUN_TEST(test_sunrise_config_matches_manual); + RUN_TEST(test_is_day_helpers); + RUN_TEST(test_is_dst_active_with_timezone_string); + RUN_TEST(test_is_dst_active_with_configured_timezone); + RUN_TEST(test_is_dst_active_with_system_timezone); + RUN_TEST(test_to_local_breakdown); + RUN_TEST(test_moon_phase_full_and_new_moon); + RUN_TEST(test_sync_ntp_requires_server_config); + RUN_TEST(test_sync_ntp_accepts_secondary_or_tertiary_server_only); + RUN_TEST(test_sync_ntp_with_three_servers_matches_single_server_behavior); + RUN_TEST(test_ntp_callback_registration_supports_member_binding); + RUN_TEST(test_ntp_sync_interval_setter_accepts_default); + RUN_TEST(test_last_ntp_sync_defaults_to_empty); + RUN_TEST(test_string_helpers_for_datetime_and_local_datetime); + RUN_TEST(test_psram_buffer_policy_toggle_is_safe); + UNITY_END(); } -void loop() {} +void loop() { +}