1
0
mirror of https://github.com/esphome/esphome.git synced 2026-02-08 00:31:58 +00:00
This commit is contained in:
J. Nick Koston
2026-01-29 22:28:09 -06:00
parent 53fb876738
commit 973105f2e5
2 changed files with 241 additions and 52 deletions

View File

@@ -2,7 +2,6 @@
#include "helpers.h"
#include <algorithm>
#include <cinttypes>
namespace esphome {
@@ -67,58 +66,120 @@ std::string ESPTime::strftime(const char *format) {
std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
int num;
const int ilen = static_cast<int>(len);
if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, // NOLINT
&second, &num) == 6 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
&hour, // NOLINT
&minute, &num) == 5 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = second;
} else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
num == ilen) {
esp_time.hour = hour;
esp_time.minute = minute;
esp_time.second = 0;
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
num == ilen) {
esp_time.year = year;
esp_time.month = month;
esp_time.day_of_month = day;
} else {
return false;
// Helper to parse exactly N digits, returns false if not enough digits
static bool parse_digits(const char *&p, const char *end, int count, uint16_t &value) {
value = 0;
for (int i = 0; i < count; i++) {
if (p >= end || *p < '0' || *p > '9')
return false;
value = value * 10 + (*p - '0');
p++;
}
return true;
}
// Helper to check for expected character
static bool expect_char(const char *&p, const char *end, char expected) {
if (p >= end || *p != expected)
return false;
p++;
return true;
}
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
// Supported formats:
// YYYY-MM-DD HH:MM:SS (19 chars)
// YYYY-MM-DD HH:MM (16 chars)
// YYYY-MM-DD (10 chars)
// HH:MM:SS (8 chars)
// HH:MM (5 chars)
const char *p = time_to_parse;
const char *end = time_to_parse + len;
uint16_t v1, v2, v3, v4, v5, v6;
// Try date formats first (start with 4-digit year)
if (len >= 10 && time_to_parse[4] == '-') {
// YYYY-MM-DD...
if (!parse_digits(p, end, 4, v1))
return false;
if (!expect_char(p, end, '-'))
return false;
if (!parse_digits(p, end, 2, v2))
return false;
if (!expect_char(p, end, '-'))
return false;
if (!parse_digits(p, end, 2, v3))
return false;
esp_time.year = v1;
esp_time.month = v2;
esp_time.day_of_month = v3;
if (p == end) {
// YYYY-MM-DD (date only)
return true;
}
if (!expect_char(p, end, ' '))
return false;
// Continue with time part: HH:MM[:SS]
if (!parse_digits(p, end, 2, v4))
return false;
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v5))
return false;
esp_time.hour = v4;
esp_time.minute = v5;
if (p == end) {
// YYYY-MM-DD HH:MM
esp_time.second = 0;
return true;
}
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v6))
return false;
esp_time.second = v6;
return p == end; // YYYY-MM-DD HH:MM:SS
}
// Try time-only formats (HH:MM[:SS])
if (len >= 5 && time_to_parse[2] == ':') {
if (!parse_digits(p, end, 2, v1))
return false;
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v2))
return false;
esp_time.hour = v1;
esp_time.minute = v2;
if (p == end) {
// HH:MM
esp_time.second = 0;
return true;
}
if (!expect_char(p, end, ':'))
return false;
if (!parse_digits(p, end, 2, v3))
return false;
esp_time.second = v3;
return p == end; // HH:MM:SS
}
return false;
}
void ESPTime::increment_second() {
this->timestamp++;
if (!increment_time_value(this->second, 0, 60))

View File

@@ -1,11 +1,11 @@
// Tests for the POSIX TZ parser implementation
// This verifies our custom parser produces identical results to libc's
// tzset()/localtime() implementation. The custom parser avoids pulling in scanf (~7.6KB).
// Tests for the POSIX TZ parser and ESPTime::strptime implementations
// These custom parsers avoid pulling in scanf (~9.8KB on ESP32-IDF).
#include <gtest/gtest.h>
#include <cstdlib>
#include <ctime>
#include "esphome/components/time/posix_tz.h"
#include "esphome/core/time.h"
namespace esphome::time::testing {
@@ -721,3 +721,131 @@ TEST(PosixTzParser, DstBoundaryJustBeforeFallBack) {
}
} // namespace esphome::time::testing
// ============================================================================
// ESPTime::strptime tests (replaces sscanf-based parsing)
// ============================================================================
namespace esphome::testing {
TEST(ESPTimeStrptime, FullDateTime) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("2026-03-15 14:30:45", 19, t));
EXPECT_EQ(t.year, 2026);
EXPECT_EQ(t.month, 3);
EXPECT_EQ(t.day_of_month, 15);
EXPECT_EQ(t.hour, 14);
EXPECT_EQ(t.minute, 30);
EXPECT_EQ(t.second, 45);
}
TEST(ESPTimeStrptime, DateTimeNoSeconds) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("2026-03-15 14:30", 16, t));
EXPECT_EQ(t.year, 2026);
EXPECT_EQ(t.month, 3);
EXPECT_EQ(t.day_of_month, 15);
EXPECT_EQ(t.hour, 14);
EXPECT_EQ(t.minute, 30);
EXPECT_EQ(t.second, 0);
}
TEST(ESPTimeStrptime, DateOnly) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("2026-03-15", 10, t));
EXPECT_EQ(t.year, 2026);
EXPECT_EQ(t.month, 3);
EXPECT_EQ(t.day_of_month, 15);
}
TEST(ESPTimeStrptime, TimeWithSeconds) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("14:30:45", 8, t));
EXPECT_EQ(t.hour, 14);
EXPECT_EQ(t.minute, 30);
EXPECT_EQ(t.second, 45);
}
TEST(ESPTimeStrptime, TimeNoSeconds) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("14:30", 5, t));
EXPECT_EQ(t.hour, 14);
EXPECT_EQ(t.minute, 30);
EXPECT_EQ(t.second, 0);
}
TEST(ESPTimeStrptime, Midnight) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("00:00:00", 8, t));
EXPECT_EQ(t.hour, 0);
EXPECT_EQ(t.minute, 0);
EXPECT_EQ(t.second, 0);
}
TEST(ESPTimeStrptime, EndOfDay) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("23:59:59", 8, t));
EXPECT_EQ(t.hour, 23);
EXPECT_EQ(t.minute, 59);
EXPECT_EQ(t.second, 59);
}
TEST(ESPTimeStrptime, LeapYearDate) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("2024-02-29", 10, t));
EXPECT_EQ(t.year, 2024);
EXPECT_EQ(t.month, 2);
EXPECT_EQ(t.day_of_month, 29);
}
TEST(ESPTimeStrptime, NewYearsEve) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("2026-12-31 23:59:59", 19, t));
EXPECT_EQ(t.year, 2026);
EXPECT_EQ(t.month, 12);
EXPECT_EQ(t.day_of_month, 31);
EXPECT_EQ(t.hour, 23);
EXPECT_EQ(t.minute, 59);
EXPECT_EQ(t.second, 59);
}
TEST(ESPTimeStrptime, EmptyStringFails) {
ESPTime t{};
EXPECT_FALSE(ESPTime::strptime("", 0, t));
}
TEST(ESPTimeStrptime, InvalidFormatFails) {
ESPTime t{};
EXPECT_FALSE(ESPTime::strptime("not-a-date", 10, t));
}
TEST(ESPTimeStrptime, PartialDateFails) {
ESPTime t{};
EXPECT_FALSE(ESPTime::strptime("2026-03", 7, t));
}
TEST(ESPTimeStrptime, PartialTimeFails) {
ESPTime t{};
EXPECT_FALSE(ESPTime::strptime("14:", 3, t));
}
TEST(ESPTimeStrptime, ExtraCharactersFails) {
ESPTime t{};
// Full datetime with extra characters should fail
EXPECT_FALSE(ESPTime::strptime("2026-03-15 14:30:45x", 20, t));
}
TEST(ESPTimeStrptime, WrongSeparatorFails) {
ESPTime t{};
EXPECT_FALSE(ESPTime::strptime("2026/03/15", 10, t));
}
TEST(ESPTimeStrptime, LeadingZeroTime) {
ESPTime t{};
ASSERT_TRUE(ESPTime::strptime("01:05:09", 8, t));
EXPECT_EQ(t.hour, 1);
EXPECT_EQ(t.minute, 5);
EXPECT_EQ(t.second, 9);
}
} // namespace esphome::testing