mirror of
https://github.com/esphome/esphome.git
synced 2026-02-08 00:31:58 +00:00
tweak
This commit is contained in:
@@ -245,6 +245,80 @@ bool parse_dst_rule(const char *&p, DSTRule &rule) {
|
||||
return true;
|
||||
}
|
||||
|
||||
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) {
|
||||
int month, day;
|
||||
|
||||
switch (rule.type) {
|
||||
case DSTRuleType::MONTH_WEEK_DAY: {
|
||||
// Find the nth occurrence of day_of_week in the given month
|
||||
int first_day_of_month = day_of_week(year, rule.month, 1);
|
||||
|
||||
// Days until first occurrence of target day
|
||||
int days_until_first = (rule.day_of_week - first_day_of_month + 7) % 7;
|
||||
int first_occurrence = 1 + days_until_first;
|
||||
|
||||
if (rule.week == 5) {
|
||||
// "Last" occurrence - find the last one in the month
|
||||
int days_in_m = days_in_month(year, rule.month);
|
||||
day = first_occurrence;
|
||||
while (day + 7 <= days_in_m) {
|
||||
day += 7;
|
||||
}
|
||||
} else {
|
||||
// nth occurrence
|
||||
day = first_occurrence + (rule.week - 1) * 7;
|
||||
}
|
||||
month = rule.month;
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTRuleType::JULIAN_NO_LEAP:
|
||||
// J format: day 1-365, Feb 29 not counted
|
||||
julian_to_month_day(rule.day, month, day);
|
||||
break;
|
||||
|
||||
case DSTRuleType::DAY_OF_YEAR:
|
||||
// Plain format: day 0-365, Feb 29 counted
|
||||
day_of_year_to_month_day(rule.day, year, month, day);
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate days from epoch to this date
|
||||
int64_t days = 0;
|
||||
for (int y = 1970; y < year; y++) {
|
||||
days += is_leap_year(y) ? 366 : 365;
|
||||
}
|
||||
for (int m = 1; m < month; m++) {
|
||||
days += days_in_month(year, m);
|
||||
}
|
||||
days += day - 1;
|
||||
|
||||
// Convert to epoch and add transition time and base offset
|
||||
return days * 86400 + rule.time_seconds + base_offset_seconds;
|
||||
}
|
||||
|
||||
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) {
|
||||
if (!tz.has_dst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int year = epoch_to_year(utc_epoch);
|
||||
|
||||
// Calculate DST start and end for this year
|
||||
// DST start transition happens in standard time
|
||||
time_t dst_start = calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds);
|
||||
// DST end transition happens in daylight time
|
||||
time_t dst_end = calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds);
|
||||
|
||||
if (dst_start < dst_end) {
|
||||
// Northern hemisphere: DST is between start and end
|
||||
return (utc_epoch >= dst_start && utc_epoch < dst_end);
|
||||
} else {
|
||||
// Southern hemisphere: DST is outside the range (wraps around year)
|
||||
return (utc_epoch >= dst_start || utc_epoch < dst_end);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) {
|
||||
@@ -314,87 +388,13 @@ bool parse_posix_tz(const char *tz_string, ParsedTimezone &result) {
|
||||
return true;
|
||||
}
|
||||
|
||||
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds) {
|
||||
int month, day;
|
||||
|
||||
switch (rule.type) {
|
||||
case DSTRuleType::MONTH_WEEK_DAY: {
|
||||
// Find the nth occurrence of day_of_week in the given month
|
||||
int first_day_of_month = internal::day_of_week(year, rule.month, 1);
|
||||
|
||||
// Days until first occurrence of target day
|
||||
int days_until_first = (rule.day_of_week - first_day_of_month + 7) % 7;
|
||||
int first_occurrence = 1 + days_until_first;
|
||||
|
||||
if (rule.week == 5) {
|
||||
// "Last" occurrence - find the last one in the month
|
||||
int days_in_m = internal::days_in_month(year, rule.month);
|
||||
day = first_occurrence;
|
||||
while (day + 7 <= days_in_m) {
|
||||
day += 7;
|
||||
}
|
||||
} else {
|
||||
// nth occurrence
|
||||
day = first_occurrence + (rule.week - 1) * 7;
|
||||
}
|
||||
month = rule.month;
|
||||
break;
|
||||
}
|
||||
|
||||
case DSTRuleType::JULIAN_NO_LEAP:
|
||||
// J format: day 1-365, Feb 29 not counted
|
||||
internal::julian_to_month_day(rule.day, month, day);
|
||||
break;
|
||||
|
||||
case DSTRuleType::DAY_OF_YEAR:
|
||||
// Plain format: day 0-365, Feb 29 counted
|
||||
internal::day_of_year_to_month_day(rule.day, year, month, day);
|
||||
break;
|
||||
}
|
||||
|
||||
// Calculate days from epoch to this date
|
||||
int64_t days = 0;
|
||||
for (int y = 1970; y < year; y++) {
|
||||
days += internal::is_leap_year(y) ? 366 : 365;
|
||||
}
|
||||
for (int m = 1; m < month; m++) {
|
||||
days += internal::days_in_month(year, m);
|
||||
}
|
||||
days += day - 1;
|
||||
|
||||
// Convert to epoch and add transition time and base offset
|
||||
return days * 86400 + rule.time_seconds + base_offset_seconds;
|
||||
}
|
||||
|
||||
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz) {
|
||||
if (!tz.has_dst) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int year = internal::epoch_to_year(utc_epoch);
|
||||
|
||||
// Calculate DST start and end for this year
|
||||
// DST start transition happens in standard time
|
||||
time_t dst_start = calculate_dst_transition(year, tz.dst_start, tz.std_offset_seconds);
|
||||
// DST end transition happens in daylight time
|
||||
time_t dst_end = calculate_dst_transition(year, tz.dst_end, tz.dst_offset_seconds);
|
||||
|
||||
if (dst_start < dst_end) {
|
||||
// Northern hemisphere: DST is between start and end
|
||||
return (utc_epoch >= dst_start && utc_epoch < dst_end);
|
||||
} else {
|
||||
// Southern hemisphere: DST is outside the range (wraps around year)
|
||||
return (utc_epoch >= dst_start || utc_epoch < dst_end);
|
||||
}
|
||||
}
|
||||
|
||||
bool epoch_to_local_tm(time_t utc_epoch, const ParsedTimezone &tz, struct tm *out_tm) {
|
||||
if (!out_tm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Determine DST status once (avoids duplicate is_in_dst calculation)
|
||||
bool in_dst = is_in_dst(utc_epoch, tz);
|
||||
bool in_dst = internal::is_in_dst(utc_epoch, tz);
|
||||
int32_t offset = in_dst ? tz.dst_offset_seconds : tz.std_offset_seconds;
|
||||
|
||||
// Apply offset (POSIX offset is positive west, so subtract to get local)
|
||||
|
||||
@@ -45,19 +45,6 @@ struct ParsedTimezone {
|
||||
/// @return true if parsing succeeded, false on error
|
||||
bool parse_posix_tz(const char *tz_string, ParsedTimezone &result);
|
||||
|
||||
/// Calculate the epoch timestamp for a DST transition in a given year.
|
||||
/// @param year The year (e.g., 2026)
|
||||
/// @param rule The DST rule (month, week, day_of_week, time)
|
||||
/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context)
|
||||
/// @return Unix epoch timestamp of the transition
|
||||
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds);
|
||||
|
||||
/// Check if a given UTC epoch falls within DST for the parsed timezone.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
/// @param tz The parsed timezone
|
||||
/// @return true if DST is in effect at the given time
|
||||
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz);
|
||||
|
||||
/// Convert a UTC epoch to local time using the parsed timezone.
|
||||
/// This replaces libc's localtime() to avoid scanf dependency.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
@@ -112,6 +99,19 @@ bool is_leap_year(int year);
|
||||
/// Convert epoch to year/month/day/hour/min/sec (UTC)
|
||||
void epoch_to_tm_utc(time_t epoch, struct tm *out_tm);
|
||||
|
||||
/// Calculate the epoch timestamp for a DST transition in a given year.
|
||||
/// @param year The year (e.g., 2026)
|
||||
/// @param rule The DST rule (month, week, day_of_week, time)
|
||||
/// @param base_offset_seconds The timezone offset to apply (std or dst depending on context)
|
||||
/// @return Unix epoch timestamp of the transition
|
||||
time_t calculate_dst_transition(int year, const DSTRule &rule, int32_t base_offset_seconds);
|
||||
|
||||
/// Check if a given UTC epoch falls within DST for the parsed timezone.
|
||||
/// @param utc_epoch Unix timestamp in UTC
|
||||
/// @param tz The parsed timezone
|
||||
/// @return true if DST is in effect at the given time
|
||||
bool is_in_dst(time_t utc_epoch, const ParsedTimezone &tz);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace esphome::time
|
||||
|
||||
@@ -369,7 +369,7 @@ TEST(PosixTzParser, DstStartUSEastern2026) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
time_t dst_start = calculate_dst_transition(2026, tz.dst_start, tz.std_offset_seconds);
|
||||
time_t dst_start = internal::calculate_dst_transition(2026, tz.dst_start, tz.std_offset_seconds);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(dst_start, &tm);
|
||||
|
||||
@@ -385,7 +385,7 @@ TEST(PosixTzParser, DstEndUSEastern2026) {
|
||||
ParsedTimezone tz;
|
||||
parse_posix_tz("EST5EDT,M3.2.0/2,M11.1.0/2", tz);
|
||||
|
||||
time_t dst_end = calculate_dst_transition(2026, tz.dst_end, tz.dst_offset_seconds);
|
||||
time_t dst_end = internal::calculate_dst_transition(2026, tz.dst_end, tz.dst_offset_seconds);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(dst_end, &tm);
|
||||
|
||||
@@ -404,7 +404,7 @@ TEST(PosixTzParser, LastSundayOfMarch2026) {
|
||||
rule.week = 5;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 2 * 3600;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
time_t transition = internal::calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
EXPECT_EQ(tm.tm_mday, 29);
|
||||
@@ -419,7 +419,7 @@ TEST(PosixTzParser, LastSundayOfOctober2026) {
|
||||
rule.week = 5;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 3 * 3600;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
time_t transition = internal::calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
EXPECT_EQ(tm.tm_mday, 25);
|
||||
@@ -434,7 +434,7 @@ TEST(PosixTzParser, FirstSundayOfApril2026) {
|
||||
rule.week = 1;
|
||||
rule.day_of_week = 0;
|
||||
rule.time_seconds = 0;
|
||||
time_t transition = calculate_dst_transition(2026, rule, 0);
|
||||
time_t transition = internal::calculate_dst_transition(2026, rule, 0);
|
||||
struct tm tm;
|
||||
internal::epoch_to_tm_utc(transition, &tm);
|
||||
EXPECT_EQ(tm.tm_mday, 5);
|
||||
@@ -451,7 +451,7 @@ TEST(PosixTzParser, IsInDstUSEasternSummer) {
|
||||
|
||||
// July 4, 2026 12:00 UTC - definitely in DST
|
||||
time_t summer = make_utc(2026, 7, 4, 12);
|
||||
EXPECT_TRUE(is_in_dst(summer, tz));
|
||||
EXPECT_TRUE(internal::is_in_dst(summer, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, IsInDstUSEasternWinter) {
|
||||
@@ -460,7 +460,7 @@ TEST(PosixTzParser, IsInDstUSEasternWinter) {
|
||||
|
||||
// January 15, 2026 12:00 UTC - definitely not in DST
|
||||
time_t winter = make_utc(2026, 1, 15, 12);
|
||||
EXPECT_FALSE(is_in_dst(winter, tz));
|
||||
EXPECT_FALSE(internal::is_in_dst(winter, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, IsInDstNoDstTimezone) {
|
||||
@@ -469,7 +469,7 @@ TEST(PosixTzParser, IsInDstNoDstTimezone) {
|
||||
|
||||
// July 15, 2026 12:00 UTC
|
||||
time_t epoch = make_utc(2026, 7, 15, 12);
|
||||
EXPECT_FALSE(is_in_dst(epoch, tz));
|
||||
EXPECT_FALSE(internal::is_in_dst(epoch, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, SouthernHemisphereDstSummer) {
|
||||
@@ -478,7 +478,7 @@ TEST(PosixTzParser, SouthernHemisphereDstSummer) {
|
||||
|
||||
// December 15, 2025 12:00 UTC - summer in NZ, should be in DST
|
||||
time_t nz_summer = make_utc(2025, 12, 15, 12);
|
||||
EXPECT_TRUE(is_in_dst(nz_summer, tz));
|
||||
EXPECT_TRUE(internal::is_in_dst(nz_summer, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, SouthernHemisphereDstWinter) {
|
||||
@@ -487,7 +487,7 @@ TEST(PosixTzParser, SouthernHemisphereDstWinter) {
|
||||
|
||||
// July 15, 2026 12:00 UTC - winter in NZ, should NOT be in DST
|
||||
time_t nz_winter = make_utc(2026, 7, 15, 12);
|
||||
EXPECT_FALSE(is_in_dst(nz_winter, tz));
|
||||
EXPECT_FALSE(internal::is_in_dst(nz_winter, tz));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -607,11 +607,11 @@ TEST(PosixTzParser, DstBoundaryJustBeforeSpringForward) {
|
||||
|
||||
// March 8, 2026 06:59:59 UTC = 01:59:59 EST (1 second before spring forward)
|
||||
time_t before_epoch = make_utc(2026, 3, 8, 6, 59, 59);
|
||||
EXPECT_FALSE(is_in_dst(before_epoch, tz));
|
||||
EXPECT_FALSE(internal::is_in_dst(before_epoch, tz));
|
||||
|
||||
// March 8, 2026 07:00:00 UTC = 02:00:00 EST -> 03:00:00 EDT (DST started)
|
||||
time_t after_epoch = make_utc(2026, 3, 8, 7);
|
||||
EXPECT_TRUE(is_in_dst(after_epoch, tz));
|
||||
EXPECT_TRUE(internal::is_in_dst(after_epoch, tz));
|
||||
}
|
||||
|
||||
TEST(PosixTzParser, DstBoundaryJustBeforeFallBack) {
|
||||
@@ -621,11 +621,11 @@ TEST(PosixTzParser, DstBoundaryJustBeforeFallBack) {
|
||||
|
||||
// November 1, 2026 05:59:59 UTC = 01:59:59 EDT (1 second before fall back)
|
||||
time_t before_epoch = make_utc(2026, 11, 1, 5, 59, 59);
|
||||
EXPECT_TRUE(is_in_dst(before_epoch, tz));
|
||||
EXPECT_TRUE(internal::is_in_dst(before_epoch, tz));
|
||||
|
||||
// November 1, 2026 06:00:00 UTC = 02:00:00 EDT -> 01:00:00 EST (DST ended)
|
||||
time_t after_epoch = make_utc(2026, 11, 1, 6);
|
||||
EXPECT_FALSE(is_in_dst(after_epoch, tz));
|
||||
EXPECT_FALSE(internal::is_in_dst(after_epoch, tz));
|
||||
}
|
||||
|
||||
} // namespace esphome::time::testing
|
||||
|
||||
Reference in New Issue
Block a user