最近碰到一個需求,是要去將一個代表日期、時間的字串轉換成 std::chrono 的 time_point。
查了一下後,在 C++20 的時候,針對 chrono 其實又提供了包含時區在內很多追加功能;其中就有一個 parse() 的函式、是用來針對自定義的格式分析字串、轉換成 time_point 的(文件)。下面就是一個實際的例子:
std::string s = "2022-11-11T11:11:11.111Z"; std::istringstream iss(s); std::chrono::system_clock::time_point tp; iss >> std::chrono::parse("%Y-%m-%dT%H:%M:%S", tp); if (iss.fail()) std::cerr << "failed" << "\n"; else std::cout << tp << "\n";
parse() 這個函式會產生一個可以接受 input stream 的物件,這邊就是要透過他產生的物件、來讀取來源字串(s);所以這邊就需要先透過 iss 這個 istringstream 來存取字串。
而 parse() 的第一個函式則是日期、時間的格式,這邊是針對來源設定成「“%Y-%m-%dT%H:%M:%S”」;詳細的定義、還有其他可以用的參數由於東西很多、所以就請自己參考文件的列表了。
由於這類型的轉換其實都有可能失敗,所以它也有提供錯誤偵測的機制。如果轉換失敗的話,它會去設定來源的 input stream 的 fail bit(參考),所以可以透過 input stream 的 fail() 這個函式來判斷是否成功。
時區的問題
不過這邊要注意的是,這邊 parse() 所產生的結果會是和 std::chrono::system_clock::now() 取得的結果一樣、都是 UTC+0 的時間。
所以如果要處理的日期字串本來就是 UTC+8 的台灣時區的話,就得注意了!實際上,Heresy 之前會寫《C++20 Chrono 的時區功能》這篇文章,也是因為碰到這邊的問題的關係才開始研究的。
比如說下面的例子:
#include <chrono> #include <iostream> #include <sstream> int main(int argc, char** argv) { auto tpNow = std::chrono::system_clock::now(); auto locTpNow = std::chrono::current_zone()->to_local(tpNow); std::cout << "Now is " << tpNow << "\nLocal: " << locTpNow << "\n\n"; std::string s = "2022-12-27T10:00:00"; std::istringstream iss(s); std::chrono::system_clock::time_point tp; iss >> std::chrono::parse("%Y-%m-%dT%H:%M:%S", tp); if (iss.fail()) std::cerr << "failed" << "\n"; else std::cout << "Input is " << tp << "\n"; if (tp < tpNow) std::cout << "input < now" << "\n"; else std::cout << "input > now" << "\n"; }
如果在當天 10:25 多執行的話,直覺上會覺得輸入的 10:00 應該會比較小吧?
但是實際執行的話,會是:
Now is 2022-12-27 02:25:26.2055624 Local: 2022-12-27 10:25:26.2055624 Input is 2022-12-27 10:00:00.0000000 input > now
這是因為實際上系統的時間會從 UTC+8 的 10:25 轉換成 UTC+0 的 2:25,然後拿來和 UTC+0 的 10:00 做比較,所以會變成輸入的 10:00 比較大的狀況。
而解決的方法呢?一個就是把 tp
的型別改成 local_time
,然再丟給 parse()
這個函式。
像是上面的程式是一個簡單的修改:
std::string s = "2022-12-27T10:00:00"; std::istringstream iss(s); std::chrono::local_time<std::chrono::system_clock::duration> tp; iss >> std::chrono::parse("%Y-%m-%dT%H:%M:%S", tp);
這樣他 tp
得到的時間就會是代表 GMT+8 的台灣時間 10:00 了!
不過也由於型別不一樣,所以也會不能和 tpNow
直接做比較,而是得和 locTpNow
來做比較。如果想要轉回 UTC+0 的 time_point
的話,則可以用下面的程式來做轉換:
std::chrono::system_clock::time_point tpSys = std::chrono::current_zone()->to_sys(tp);
這樣就可以拿來和 tpNow
做比較了。
而下面的例子,應該可以更清楚地看出 system_clock::time_point
和 local_time<>
兩者的差異:
#include <chrono> #include <iostream> #include <sstream> template<typename TTime> void pareTime(std::string sInput) { std::istringstream iss(sInput); TTime tp; iss >> std::chrono::parse("%Y-%m-%dT%H:%M:%S", tp); if (iss.fail()) std::cerr << "failed" << "\n"; else std::cout << "Input is " << tp << "\n"; std::chrono::zoned_time tpZone(std::chrono::current_zone(), tp); std::cout << "Zoned time " << tpZone << "\n"; } int main(int argc, char** argv) { std::string s = "2022-12-27T10:00:00"; std::cout << "Parse " << s << " as system time\n"; pareTime<std::chrono::system_clock::time_point>(s); std::cout << "\n"; std::cout << "Parse " << s << " as local time\n"; pareTime<std::chrono::local_time<std::chrono::system_clock::duration>>(s); }
他執行的結果會是:
Parse 2022-12-27T10:00:00 as system time Input is 2022-12-27 10:00:00.0000000 Zoned time 2022-12-27 18:00:00.0000000 GMT+8 Parse 2022-12-27T10:00:00 as local time Input is 2022-12-27 10:00:00.0000000 Zoned time 2022-12-27 10:00:00.0000000 GMT+8
所以,要處理時間的話,真的得注意一下時區的問題了…
g++ 還不支援:改用 std::get_time()
本來以為這樣就解決了,結過後來很悲情地發現,這樣的程式雖然在 Visual C++ 可以正常編譯,但是 g++ 12.1 卻不支援這功能… orz
又找了一下資料後,後來則是決定用 C++11 時加入的 std::get_time() 這個函式(文件)來實作。
他的用法其實和 std::chrono::parse() 幾乎一樣(考慮到時間,其實應該是反過來就是了),只是她轉換的結果是 std::tm 的格式(參考);而如果要轉換成 time_point 的話,比較方便的方法似乎是先透過 std::mktime() 產生 time_t、然後再透過 from_time_t() 這個函式來轉換。
整個程式大概可以寫成下面的樣子:
std::string s = "2022-11-11T11:11:11.111Z"; std::istringstream iss(s); std::chrono::system_clock::time_point tp; std::tm t = {}; iss >> std::get_time(&t, "%Y-%m-%dT%H:%M:%S"); if (iss.fail()) std::cerr << "failed" << "\n"; else { tp = std::chrono::system_clock::from_time_t(std::mktime(&t)); std::cout << tp << "\n"; }
理論上應該解決了吧?但是這樣的程式輸出的結果卻會是「2022-11-11 03:11:11.0000000」,和預期的答案不一樣、少了八個小時…
而這邊的問題,是由於 std::mktime() 這個函式(文件)會把輸入的時間當作區域時間,輸出則是 GMT(+0)的時間的關係…這點看來又和 chrono 的處理方法不一樣了。 orz
而且因為 std::mktime() 本身沒辦法去控制時區,所以如果要無視時區轉換,就會得很麻煩… orz
感覺上,還是直接等 g++13 有支援 chrono 的相關功能再說算了?
參考: