From deba28755662a14f0e3f2ac4a85e2fb6571b2f8d Mon Sep 17 00:00:00 2001 From: bitmap4 Date: Wed, 9 Apr 2025 22:20:13 +0000 Subject: [PATCH 1/4] [PowerRename][Feature] Add new date/time formatting patterns to GetDatedFileName --- src/modules/powerrename/lib/Helpers.cpp | 33 ++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/modules/powerrename/lib/Helpers.cpp b/src/modules/powerrename/lib/Helpers.cpp index aeadd8b68373..cd9e24d9f3b2 100644 --- a/src/modules/powerrename/lib/Helpers.cpp +++ b/src/modules/powerrename/lib/Helpers.cpp @@ -248,7 +248,19 @@ HRESULT GetTransformedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR sour bool isFileTimeUsed(_In_ PCWSTR source) { bool used = false; - static const std::array patterns = { std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$Y" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$M" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$D" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$h" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$m" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$s" }, std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$f" } }; + static const std::array patterns = { + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$Y" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$M" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$D" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$h" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$m" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$s" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$f" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$H" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$T" }, + std::wregex{ L"(([^\\$]|^)(\\$\\$)*)\\$t" } + }; + for (size_t i = 0; !used && i < patterns.size(); i++) { if (std::regex_search(source, patterns[i])) @@ -275,6 +287,13 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY StringCchCopy(localeName, LOCALE_NAME_MAX_LENGTH, L"en_US"); } + int hour12 = (fileTime.wHour % 12); + if (hour12 == 0) + { + hour12 = 12; + } + const wchar_t* ampm = (fileTime.wHour < 12) ? L"AM" : L"PM"; + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm); @@ -316,6 +335,18 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", fileTime.wDay); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$D"), replaceTerm); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", hour12); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$HH"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", ampm); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm); + + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"am" : L"pm"); + res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$tt"), replaceTerm); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%02d"), L"$01", fileTime.wHour); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$hh"), replaceTerm); From 31a33659d476e2f1dabc9be8453c849333c9c1ad Mon Sep 17 00:00:00 2001 From: bitmap4 Date: Wed, 9 Apr 2025 22:20:24 +0000 Subject: [PATCH 2/4] [PowerRename][UI] Add date/time shortcut patterns to cheat sheet --- .../PowerRenameXAML/MainWindow.xaml.cpp | 11 ++++++++-- .../Strings/en-us/Resources.resw | 20 +++++++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp index 9d5d8497d319..1a6269a8bf07 100644 --- a/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp +++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameXAML/MainWindow.xaml.cpp @@ -195,8 +195,15 @@ namespace winrt::PowerRenameUI::implementation m_dateTimeShortcuts.Append(winrt::make(L"$DDD", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_DayNameAbbr").ValueAsString())); m_dateTimeShortcuts.Append(winrt::make(L"$DD", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_DayDigitLZero").ValueAsString())); m_dateTimeShortcuts.Append(winrt::make(L"$D", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_DayDigit").ValueAsString())); - m_dateTimeShortcuts.Append(winrt::make(L"$hh", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_HoursLZero").ValueAsString())); - m_dateTimeShortcuts.Append(winrt::make(L"$h", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_Hours").ValueAsString())); + + m_dateTimeShortcuts.Append(winrt::make(L"$HH", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_Hours12LZero").ValueAsString())); + m_dateTimeShortcuts.Append(winrt::make(L"$H", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_Hours12").ValueAsString())); + m_dateTimeShortcuts.Append(winrt::make(L"$TT", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_AMPMUpperCase").ValueAsString())); + m_dateTimeShortcuts.Append(winrt::make(L"$tt", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_AMPMLowerCase").ValueAsString())); + + m_dateTimeShortcuts.Append(winrt::make(L"$hh", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_Hours24LZero").ValueAsString())); + m_dateTimeShortcuts.Append(winrt::make(L"$h", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_Hours24").ValueAsString())); + m_dateTimeShortcuts.Append(winrt::make(L"$mm", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_MinutesLZero").ValueAsString())); m_dateTimeShortcuts.Append(winrt::make(L"$m", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_Minutes").ValueAsString())); m_dateTimeShortcuts.Append(winrt::make(L"$ss", manager.MainResourceMap().GetValue(L"Resources/DateTimeCheatSheet_SecondsLZero").ValueAsString())); diff --git a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw index c1d55917f7ff..d5289611489e 100644 --- a/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw +++ b/src/modules/powerrename/PowerRenameUILib/Strings/en-us/Resources.resw @@ -219,11 +219,23 @@ Day of the month as digits without leading zeros for single-digit days. - - Hours with leading zeros for single-digit hours. + + Hours in 12-hour format (01-12) with leading zero. - - Hours without leading zeros for single-digit hours. + + Hours in 12-hour format (1-12) without leading zero. + + + AM/PM indicator in uppercase (AM or PM). + + + AM/PM indicator in lowercase (am or pm). + + + Hours in 24-hour format (00-23) with leading zero. + + + Hours in 24-hour format (0-23) without leading zero. Minutes with leading zeros for single-digit minutes. From 232dfe9121af72869227b897430c037fb10a1f4f Mon Sep 17 00:00:00 2001 From: bitmap4 Date: Thu, 10 Apr 2025 13:39:54 +0000 Subject: [PATCH 3/4] [PowerRename][Tests] Add tests for new date/time formatting patterns --- .../unittests/PowerRenameRegExBoostTests.cpp | 48 +++++++++++++++++++ .../unittests/PowerRenameRegExTests.cpp | 48 +++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp b/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp index 2bff1a4b9c3c..491852ff88d7 100644 --- a/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp +++ b/src/modules/powerrename/unittests/PowerRenameRegExBoostTests.cpp @@ -127,5 +127,53 @@ TEST_METHOD(VerifyLookbehind) CoTaskMemFree(result); } } + +TEST_METHOD (Verify12and24HourTimeFormats) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurrences | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + struct TimeTestCase { + SYSTEMTIME time; // Input time + PCWSTR formatString; // Format pattern + PCWSTR expectedResult; // Expected output + PCWSTR description; // Description of what we're testing + }; + + struct TimeTestCase testCases[] = { + // Midnight (00:00 / 12:00 AM) + { { 2025, 4, 4, 10, 0, 0, 0, 0 }, L"[$hh:$mm] [$H:$mm $tt]", L"[00:00] [12:00 am]", L"Midnight formatting" }, + + // Noon (12:00 / 12:00 PM) + { { 2025, 4, 4, 10, 12, 0, 0, 0 }, L"[$hh:$mm] [$H:$mm $tt]", L"[12:00] [12:00 pm]", L"Noon formatting" }, + + // 1:05 AM + { { 2025, 4, 4, 10, 1, 5, 0, 0 }, L"[$h:$m] [$H:$m $tt] [$hh:$mm] [$HH:$mm $TT]", + L"[1:5] [1:5 am] [01:05] [01:05 AM]", L"1 AM with various formats" }, + + // 11 PM + { { 2025, 4, 4, 10, 23, 45, 0, 0 }, L"[$h:$m] [$H:$m $tt] [$hh:$mm] [$HH:$mm $TT]", + L"[23:45] [11:45 pm] [23:45] [11:45 PM]", L"11 PM with various formats" }, + + // Mixed formats in complex pattern + { { 2025, 4, 4, 10, 14, 30, 0, 0 }, L"Date: $YYYY-$MM-$DD Time: $hh:$mm (24h) / $H:$mm $tt (12h)", + L"Date: 2025-04-10 Time: 14:30 (24h) / 2:30 pm (12h)", L"Complex combined format" }, + }; + + for (int i = 0; i < ARRAYSIZE(testCases); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"test") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(testCases[i].formatString) == S_OK); + Assert::IsTrue(renameRegEx->PutFileTime(testCases[i].time) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"test", &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, testCases[i].expectedResult) == 0, + (std::wstring(L"Failed test case: ") + testCases[i].description).c_str()); + CoTaskMemFree(result); + } +} }; } diff --git a/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp b/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp index 5270f193e260..18a679995a70 100644 --- a/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp +++ b/src/modules/powerrename/unittests/PowerRenameRegExTests.cpp @@ -207,5 +207,53 @@ TEST_METHOD(VerifyLookbehindFails) } } +TEST_METHOD (Verify12and24HourTimeFormats) +{ + CComPtr renameRegEx; + Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK); + DWORD flags = MatchAllOccurrences | UseRegularExpressions; + Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK); + + struct TimeTestCase { + SYSTEMTIME time; // Input time + PCWSTR formatString; // Format pattern + PCWSTR expectedResult; // Expected output + PCWSTR description; // Description of what we're testing + }; + + struct TimeTestCase testCases[] = { + // Midnight (00:00 / 12:00 AM) + { { 2025, 4, 4, 10, 0, 0, 0, 0 }, L"[$hh:$mm] [$H:$mm $tt]", L"[00:00] [12:00 am]", L"Midnight formatting" }, + + // Noon (12:00 / 12:00 PM) + { { 2025, 4, 4, 10, 12, 0, 0, 0 }, L"[$hh:$mm] [$H:$mm $tt]", L"[12:00] [12:00 pm]", L"Noon formatting" }, + + // 1:05 AM + { { 2025, 4, 4, 10, 1, 5, 0, 0 }, L"[$h:$m] [$H:$m $tt] [$hh:$mm] [$HH:$mm $TT]", + L"[1:5] [1:5 am] [01:05] [01:05 AM]", L"1 AM with various formats" }, + + // 11 PM + { { 2025, 4, 4, 10, 23, 45, 0, 0 }, L"[$h:$m] [$H:$m $tt] [$hh:$mm] [$HH:$mm $TT]", + L"[23:45] [11:45 pm] [23:45] [11:45 PM]", L"11 PM with various formats" }, + + // Mixed formats in complex pattern + { { 2025, 4, 4, 10, 14, 30, 0, 0 }, L"Date: $YYYY-$MM-$DD Time: $hh:$mm (24h) / $H:$mm $tt (12h)", + L"Date: 2025-04-10 Time: 14:30 (24h) / 2:30 pm (12h)", L"Complex combined format" }, + }; + + for (int i = 0; i < ARRAYSIZE(testCases); i++) + { + PWSTR result = nullptr; + Assert::IsTrue(renameRegEx->PutSearchTerm(L"test") == S_OK); + Assert::IsTrue(renameRegEx->PutReplaceTerm(testCases[i].formatString) == S_OK); + Assert::IsTrue(renameRegEx->PutFileTime(testCases[i].time) == S_OK); + unsigned long index = {}; + Assert::IsTrue(renameRegEx->Replace(L"test", &result, index) == S_OK); + Assert::IsTrue(wcscmp(result, testCases[i].expectedResult) == 0, + (std::wstring(L"Failed test case: ") + testCases[i].description).c_str()); + CoTaskMemFree(result); + } +} + }; } From 5c9676a1ffbf7a594433eed2713aef28dedd68b8 Mon Sep 17 00:00:00 2001 From: bitmap4 Date: Thu, 10 Apr 2025 14:28:05 +0000 Subject: [PATCH 4/4] [PowerRename] [Refactor] Simplify AM/PM string handling in time patterns --- src/modules/powerrename/lib/Helpers.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/modules/powerrename/lib/Helpers.cpp b/src/modules/powerrename/lib/Helpers.cpp index cd9e24d9f3b2..2b6527593e47 100644 --- a/src/modules/powerrename/lib/Helpers.cpp +++ b/src/modules/powerrename/lib/Helpers.cpp @@ -292,7 +292,6 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY { hour12 = 12; } - const wchar_t* ampm = (fileTime.wHour < 12) ? L"AM" : L"PM"; StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%04d"), L"$01", fileTime.wYear); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$YYYY"), replaceTerm); @@ -341,7 +340,7 @@ HRESULT GetDatedFileName(_Out_ PWSTR result, UINT cchMax, _In_ PCWSTR source, SY StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%d"), L"$01", hour12); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$H"), replaceTerm); - StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", ampm); + StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"AM" : L"PM"); res = regex_replace(res, std::wregex(L"(([^\\$]|^)(\\$\\$)*)\\$TT"), replaceTerm); StringCchPrintf(replaceTerm, MAX_PATH, TEXT("%s%s"), L"$01", (fileTime.wHour < 12) ? L"am" : L"pm");