From 59b499037b25824e9d76d25c55ba9889fd794593 Mon Sep 17 00:00:00 2001 From: NeilClack Date: Tue, 29 Sep 2026 08:43:59 -0500 Subject: [PATCH] Kiosk: show all user-visible times as 12-hour AM/PM Add datetime::formatHm12h/formatTime12h and use them for the Home footer clock, the Timezone page's local-time sample, and the log viewer timestamps. Storage and parsing remain 24-hour HH:MM. Extends the date-format tests with the midnight/noon edge cases. --- src/date_format.cpp | 33 ++++++++++++++++++++++ src/date_format.h | 9 ++++++ src/home.cpp | 2 +- src/log_viewer_page.cpp | 7 ++++- src/timezone_page.cpp | 9 +++++- tests/test_date_format.cpp | 56 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/date_format.cpp b/src/date_format.cpp index 717ef45..95ada58 100644 --- a/src/date_format.cpp +++ b/src/date_format.cpp @@ -1,5 +1,7 @@ #include "date_format.h" +#include +#include #include namespace radiantwave::datetime { @@ -34,4 +36,35 @@ std::string formatTime(std::time_t t) { return formatDate(t, "%H:%M:%S"); } +std::string formatHm12h(const std::string& hm24) { + // Require exactly HH:MM with two decimal digits each. + if (hm24.size() != 5 || hm24[2] != ':' || + !std::isdigit(static_cast(hm24[0])) || + !std::isdigit(static_cast(hm24[1])) || + !std::isdigit(static_cast(hm24[3])) || + !std::isdigit(static_cast(hm24[4]))) { + return hm24; + } + + const int hour24 = (hm24[0] - '0') * 10 + (hm24[1] - '0'); + const int minute = (hm24[3] - '0') * 10 + (hm24[4] - '0'); + if (hour24 > 23 || minute > 59) { + return hm24; + } + + int hour12 = hour24 % 12; + if (hour12 == 0) { + hour12 = 12; + } + const char* period = hour24 < 12 ? "AM" : "PM"; + + char buffer[16]; + std::snprintf(buffer, sizeof(buffer), "%d:%02d %s", hour12, minute, period); + return std::string(buffer); +} + +std::string formatTime12h(std::time_t t) { + return formatHm12h(formatDate(t, "%H:%M")); +} + } // namespace radiantwave::datetime diff --git a/src/date_format.h b/src/date_format.h index 719b117..e904195 100644 --- a/src/date_format.h +++ b/src/date_format.h @@ -26,4 +26,13 @@ std::string formatDate(std::time_t t, const std::string& pattern); // Render `t` as a 24-hour clock: HH:MM:SS. std::string formatTime(std::time_t t); +// Convert a 24-hour clock `HH:MM` string to a 12-hour display label, e.g. +// "00:00" -> "12:00 AM", "13:05" -> "1:05 PM". Storage and parsing remain +// 24-hour; this is display-only. Returns `hm24` unchanged when it is not a +// valid 24-hour time. +std::string formatHm12h(const std::string& hm24); + +// Render `t` in local time as a 12-hour clock label, e.g. "7:05 PM". +std::string formatTime12h(std::time_t t); + } // namespace radiantwave::datetime diff --git a/src/home.cpp b/src/home.cpp index ad9e384..9ddfe65 100644 --- a/src/home.cpp +++ b/src/home.cpp @@ -249,7 +249,7 @@ void Home::render(SDL_Renderer *renderer) internet_ ? std::string("Connected") : std::string("Offline"), audioStatus(), radiantwave::datetime::formatDate(now, dateFormat_), - radiantwave::datetime::formatTime(now), + radiantwave::datetime::formatTime12h(now), }; std::vector valueColors(values.size(), footerColor); std::vector valueBold(values.size(), false); diff --git a/src/log_viewer_page.cpp b/src/log_viewer_page.cpp index 6d25549..bb74706 100644 --- a/src/log_viewer_page.cpp +++ b/src/log_viewer_page.cpp @@ -1,5 +1,6 @@ #include "log_viewer_page.h" #include "app_state.h" +#include "date_format.h" #include "timezone_helper.h" #include "logical_size.h" #include @@ -256,7 +257,11 @@ void LogViewerPage::rebuildWrapped(float maxLineWidth) { using namespace radiantwave::logger; for (const auto& entry : rawEntries_) { - std::string timeStr = radiantwave::timezone::formatUtcToTimezone(entry.timestamp, currentTzCode_, "%Y-%m-%d %H:%M:%S"); + std::string timeStr = + radiantwave::timezone::formatUtcToTimezone(entry.timestamp, currentTzCode_, "%Y-%m-%d") + + " " + + radiantwave::datetime::formatHm12h( + radiantwave::timezone::formatUtcToTimezone(entry.timestamp, currentTzCode_, "%H:%M")); std::string fullLineStr = "[" + timeStr + "] [" + std::string(logLevelToString(entry.level)) + "] [" + entry.component + "] " + entry.message; diff --git a/src/timezone_page.cpp b/src/timezone_page.cpp index 6a01063..e6f77b2 100644 --- a/src/timezone_page.cpp +++ b/src/timezone_page.cpp @@ -1,5 +1,6 @@ #include "timezone_page.h" #include "app_state.h" +#include "date_format.h" #include "logical_size.h" #include #include @@ -237,7 +238,13 @@ void TimeZonePage::render(SDL_Renderer* renderer) { } time_t now = std::time(nullptr); - std::string localSample = "Local Time: " + radiantwave::timezone::formatUtcToTimezone(now, currentTzCode_); + // Show the configured zone's wall-clock time with an AM/PM label; storage + // and the rest of the app stay 24-hour. + std::string localSample = "Local Time: " + + radiantwave::timezone::formatUtcToTimezone(now, currentTzCode_, "%Y-%m-%d") + " " + + radiantwave::datetime::formatHm12h( + radiantwave::timezone::formatUtcToTimezone(now, currentTzCode_, "%H:%M")) + + " " + radiantwave::timezone::formatUtcToTimezone(now, currentTzCode_, "%Z"); sampleText_.ensure(renderer, fontHeader_, localSample, green); if (sampleText_.texture) { SDL_FRect dst{700.0f, currentY, sampleText_.width, sampleText_.height}; diff --git a/tests/test_date_format.cpp b/tests/test_date_format.cpp index a61e108..54d4c57 100644 --- a/tests/test_date_format.cpp +++ b/tests/test_date_format.cpp @@ -59,3 +59,59 @@ TEST_F(DateFormatTest, OptionsIncludeDefault) { EXPECT_EQ(options[0].pattern, std::string(DEFAULT_DATE_FORMAT)); EXPECT_EQ(options[0].name, "Month/Day/Year"); } + +TEST_F(DateFormatTest, Converts24HourTo12HourLabels) { + EXPECT_EQ(formatHm12h("00:00"), "12:00 AM"); + EXPECT_EQ(formatHm12h("00:05"), "12:05 AM"); + EXPECT_EQ(formatHm12h("11:59"), "11:59 AM"); + EXPECT_EQ(formatHm12h("12:00"), "12:00 PM"); + EXPECT_EQ(formatHm12h("12:30"), "12:30 PM"); + EXPECT_EQ(formatHm12h("13:00"), "1:00 PM"); + EXPECT_EQ(formatHm12h("23:59"), "11:59 PM"); +} + +TEST_F(DateFormatTest, LeavesMalformedInputUnchanged) { + EXPECT_EQ(formatHm12h(""), ""); + EXPECT_EQ(formatHm12h("7:05"), "7:05"); + EXPECT_EQ(formatHm12h("24:00"), "24:00"); + EXPECT_EQ(formatHm12h("12:60"), "12:60"); + EXPECT_EQ(formatHm12h("ab:cd"), "ab:cd"); +} + +TEST_F(DateFormatTest, Formats12HourClock) { + std::tm morning = {}; + morning.tm_year = 2026 - 1900; + morning.tm_mon = 8 - 1; + morning.tm_mday = 4; + morning.tm_hour = 7; + morning.tm_min = 5; + morning.tm_sec = 0; + morning.tm_isdst = -1; + time_t tMorning = mktime(&morning); + ASSERT_NE(tMorning, static_cast(-1)); + EXPECT_EQ(formatTime12h(tMorning), "7:05 AM"); + + std::tm midnight = {}; + midnight.tm_year = 2026 - 1900; + midnight.tm_mon = 8 - 1; + midnight.tm_mday = 4; + midnight.tm_hour = 0; + midnight.tm_min = 0; + midnight.tm_sec = 0; + midnight.tm_isdst = -1; + time_t tMidnight = mktime(&midnight); + ASSERT_NE(tMidnight, static_cast(-1)); + EXPECT_EQ(formatTime12h(tMidnight), "12:00 AM"); + + std::tm evening = {}; + evening.tm_year = 2026 - 1900; + evening.tm_mon = 8 - 1; + evening.tm_mday = 4; + evening.tm_hour = 19; + evening.tm_min = 5; + evening.tm_sec = 0; + evening.tm_isdst = -1; + time_t tEvening = mktime(&evening); + ASSERT_NE(tEvening, static_cast(-1)); + EXPECT_EQ(formatTime12h(tEvening), "7:05 PM"); +}