C++ 字串轉 chrono 的 time_point

最近碰到一個需求,是要去將一個代表日期、時間的字串轉換成 std::chronotime_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_pointlocal_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 的相關功能再說算了?


參考:

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *