1. 关键词
C++ 时间处理 日期时间类 跨平台
2. 问题
为什么C++就没有一个方便好用的表示日期时间的类?
同样是高级语言,Java中有Date,C#中有DateTime,Python中有datetime,为什么C++就没有一个方便好用的表示日期时间的类?我觉得这是C++ STL的遗憾,在这个点上做的是挺失败的。C++11之前,处理日期时间一般都会用C语言函数,如:localtime, gmtime, mktime, gettimeofday, put_time等。C++11之后,引入了chrono库,做到了时间处理的跨平台化和标准化,时间精度也支持到了纳秒级,但是chrono库的用法非常累赘,一点也不简洁,如:要以毫秒为单位获取当前时间戳,他的实现要写以下这么一长串的代码。
auto now = std::chrono::system_clock::now();
auto timestamp_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
auto ms = static_cast<uint64_t>(timestamp_ms);
那~ 有没有更简洁、漂亮的实现方式呢?
答案是:自己写一个!
3. 设计理念
- 极简
- 易用
- 跨平台
4. 支持的能力
- 获取当前时间
- 获取UTC时间
- 格式化时间
- 从字符串解析时间
- 时间差计算
- 时间精度:毫秒级
5. 代码实现
5.1. datetime.h
#pragma once
#include <cstdint>
#include <string>
#include <iostream>
#include <regex>
#include <vector>
#include <utility>
namespace cutl
{
/**
* @brief the string datetime format for parsing and formatting
*
*/
enum class datetime_format
{
/** YYYY-MM-DD HH:MM:SS.sss */
datetime_format_a,
/** YYYY.MM.DD HH:MM:SS */
datetime_format_b,
/** YYYY/MM/DD HH:MM:SS */
datetime_format_c,
/** YYYYMMDD HH:MM:SS */
datetime_format_d,
};
/**
* @brief A simple, feature-rich modern C++ date-time class
*
*/
class datetime
{
public:
/**
* @brief Constants value: second, expressed in milliseconds.
*
*/
static constexpr int second = 1000;
/**
* @brief Constants value: min, expressed in milliseconds.
*
*/
static constexpr int min = 60 * second;
/**
* @brief Constants value: hour, expressed in milliseconds.
*
*/
static constexpr int hour = 60 * min;
/**
* @brief Constants value: day, expressed in milliseconds.
*
*/
static constexpr int day = 24 * hour;
public:
/**
* @brief Construct a new datetime object
*
* @param ms a timestamp in milliseconds for initialize the datetime object
*/
datetime(uint64_t ms);
/**
* @brief Construct a new datetime object
*
* @param other other datetime object to copy
*/
datetime(const datetime &other);
/**
* @brief Destroy the datetime object
*
*/
~datetime();
private:
datetime();
public:
/**
* @brief Get a datetime object for the current system time
*
* @return datetime object for the current system time
*/
static datetime now();
/**
* @brief Constructs a datetime object from a local time string.
*
* Only the following time formats are supported:
* - YYYY-MM-DD HH:MM:SS.sss
* - YYYY-MM-DD HH:MM:SS
* - YYYY.MM.DD HH:MM:SS.sss
* - YYYY.MM.DD HH:MM:SS
* - YYYY/MM/DD HH:MM:SS.sss
* - YYYY/MM/DD HH:MM:SS
* - YYYYMMDD HH:MM:SS.sss
* - YYYYMMDD HH:MM:SS
* @param time_text local time string, use the below formats to construct a datetime object.
* @param isdst the setting of daylight saving time, -1 means system automatically determine, 0 means not in daylight saving time, 1 means in daylight saving time
* @return datetime object constructed from the local time string
*/
static datetime get(const std::string &time_text, int isdst = -1);
public:
/**
* @brief Get the timestamp in milliseconds
*
* @return the timestamp in milliseconds
*/
uint64_t timestamp() const;
/**
* @brief format the datetime object to a string
*
* @param dfmt datetime format, a value of datetime_format enum, default is datetime_format_a
* @param local whether to use local time, default is true. if true means output in local time, otherwise, output in UTC time.
* @param show_milliseconds whether to show milliseconds, default is true. if true means show milliseconds, otherwise, not show milliseconds.
* @return formatted datetime string described by std::string
*/
std::string format(datetime_format dfmt = datetime_format::datetime_format_a, bool local = true, bool show_milliseconds = true) const;
// fmt, usages like std::put_time
/**
* @brief Format the datetime object to a string with a custom format string
*
* @param fmt datetime format string, default is "%Y-%m-%d %H:%M:%S.%f". usages like std::put_time, reference to https://en.cppreference.com/w/cpp/io/manip/put_time
* @param local whether to use local time, default is true. if true means output in local time, otherwise, output in UTC time.
* @param show_milliseconds whether to show milliseconds, default is true. if true means show milliseconds, otherwise, not show milliseconds.
* @return formatted datetime string described by std::string
*/
std::string format(const std::string &fmt, bool local = true, bool show_milliseconds = true) const;
/**
* @brief Get the string of UTC time described by datetime object
*
* @return the string of UTC time in format "YYYY-MM-DD HH:MM:SS.sss"
*/
std::string utctime() const
{
return format(datetime_format::datetime_format_a, false);
}
/**
* @brief Define the assignment operator
*
* @param other other datetime object to copy
* @return datetime& the reference of the current datetime object
*/
datetime &operator=(const datetime &other);
/**
* @brief Define the addition operator
*
* @param ms milliseconds to add
* @return datetime object after adding milliseconds
*/
datetime operator+(uint64_t ms);
/**
* @brief Define the subtraction operator
*
* @param ms milliseconds to subtract
* @return datetime object after subtracting milliseconds
*/
datetime operator-(uint64_t ms);
/**
* @brief Define the addition and assignment operator
*
* @param ms milliseconds to add
* @return datetime& the reference of the current datetime object after adding milliseconds
*/
datetime &operator+=(uint64_t ms);
/**
* @brief Define the subtraction and assignment operator
*
* @param ms milliseconds to subtract
* @return datetime& the reference of the current datetime object after subtracting milliseconds
*/
datetime &operator-=(uint64_t ms);
/**
* @brief Define the subtraction operator between two datetime objects
*
* @param other datetime object to subtract
* @return the duration in milliseconds between current and other datetime objects
*/
int64_t operator-(const datetime &other) const;
private:
using time_regex_type = std::pair<std::string, std::regex>;
using time_regex_vec_type = std::vector<time_regex_type>;
static std::string supported_time_formats(const time_regex_vec_type &fmtlist);
static bool verify_time(const struct tm &time);
private:
uint64_t timestamp_ms_;
};
/**
* @brief Define the output stream operator for datetime object
*
* @param os the std::ostream object
* @param dt the datetime object to be output
* @return std::ostream& the reference of the std::ostream object after outputting the datetime object
*/
std::ostream &operator<<(std::ostream &os, const datetime &dt);
} // namespace
5.2. timecount.cpp
#include "datetime.h"
#include "timeutil.h"
#include "strfmt.h"
#include "inner/logger.h"
namespace cutl
{
datetime::datetime(const datetime &other)
{
timestamp_ms_ = other.timestamp_ms_;
}
datetime::datetime()
{
timestamp_ms_ = 0;
}
datetime::datetime(uint64_t ms) : timestamp_ms_(ms)
{
}
datetime::~datetime()
{
}
datetime datetime::now()
{
return datetime(cutl::timestamp(timeunit::ms));
}
datetime datetime::get(const std::string &time_text, int isdst)
{
std::smatch matchRes;
bool result = false;
static time_regex_vec_type fmt_list = {
// 0/1, 2/3, 4/5, 6/7的顺序不能反,因为不含毫秒数的时间会被优先匹配到
std::make_pair("YYYY-MM-DD HH:MM:SS.sss", std::regex(R"((\d{4})-(\d{2})-(\d{2})[ ](\d{2}):(\d{2}):(\d{2}).(\d{3}))")),
std::make_pair("YYYY-MM-DD HH:MM:SS", std::regex(R"((\d{4})-(\d{2})-(\d{2})[ ](\d{2}):(\d{2}):(\d{2}))")),
std::make_pair("YYYY.MM.DD HH:MM:SS.sss", std::regex(R"((\d{4}).(\d{2}).(\d{2})[ ](\d{2}):(\d{2}):(\d{2}).(\d{3}))")),
std::make_pair("YYYY.MM.DD HH:MM:SS", std::regex(R"((\d{4}).(\d{2}).(\d{2})[ ](\d{2}):(\d{2}):(\d{2}))")),
std::make_pair("YYYY/MM/DD HH:MM:SS.sss", std::regex(R"((\d{4})/(\d{2})/(\d{2})[ ](\d{2}):(\d{2}):(\d{2}).(\d{3}))")),
std::make_pair("YYYY/MM/DD HH:MM:SS", std::regex(R"((\d{4})/(\d{2})/(\d{2})[ ](\d{2}):(\d{2}):(\d{2}))")),
std::make_pair("YYYYMMDD HH:MM:SS.sss", std::regex(R"((\d{4})(\d{2})(\d{2})[ ](\d{2}):(\d{2}):(\d{2}).(\d{3}))")),
std::make_pair("YYYYMMDD HH:MM:SS", std::regex(R"((\d{4})(\d{2})(\d{2})[ ](\d{2}):(\d{2}):(\d{2}))")),
};
for (size_t i = 0; i < fmt_list.size(); i++)
{
auto &fmt_text = fmt_list[i].first;
auto &fmt_pattern = fmt_list[i].second;
result = std::regex_search(time_text, matchRes, fmt_pattern);
if (result)
{
CUTL_DEBUG("matched regex: " + fmt_text);
break;
}
}
if (!result || matchRes.size() < 7)
{
auto time_fmts = supported_time_formats(fmt_list);
CUTL_ERROR("Only the following time formats are supported:\n" + time_fmts);
return datetime();
}
CUTL_DEBUG("matchRes size:" + std::to_string(matchRes.size()) + ", res:" + matchRes[0].str());
// 解析毫秒值
int ms = 0;
if (matchRes.size() == 8)
{
ms = std::stoi(matchRes[7].str());
}
// 解析tm结构的时间
struct tm time = {};
if (matchRes.size() >= 7)
{
for (size_t i = 1; i < 7; i++)
{
time.tm_year = std::stoi(matchRes[1]);
time.tm_mon = std::stoi(matchRes[2]);
time.tm_mday = std::stoi(matchRes[3]);
time.tm_hour = std::stoi(matchRes[4]);
time.tm_min = std::stoi(matchRes[5]);
time.tm_sec = std::stoi(matchRes[6]);
time.tm_isdst = isdst;
}
}
if (!verify_time(time))
{
return datetime();
}
// 转换为时间戳
time.tm_year -= 1900;
time.tm_mon -= 1;
auto ret = mktime(&time);
if (ret == -1)
{
CUTL_ERROR("mktime() failed");
return datetime();
}
auto s = static_cast<uint64_t>(ret);
return datetime(s2ms(s) + ms);
}
uint64_t datetime::timestamp() const
{
return timestamp_ms_;
}
std::string get_format_str(datetime_format fmt)
{
std::string text;
switch (fmt)
{
case datetime_format::datetime_format_a: // YYYY-MM-DD HH:MM:SS
text = "%Y-%m-%d %H:%M:%S";
break;
case datetime_format::datetime_format_b: // YYYY.MM.DD HH:MM:SS
text = "%Y.%m.%d %H:%M:%S";
break;
case datetime_format::datetime_format_c: // YYYY/MM/DD HH:MM:SS
text = "%Y/%m/%d %H:%M:%S";
break;
case datetime_format::datetime_format_d: // YYYYMMDD HH:MM:SS
text = "%Y%m%d %H:%M:%S";
break;
default:
break;
}
return text;
}
std::string datetime::format(datetime_format fmt, bool local, bool show_milliseconds) const
{
auto fmtstr = get_format_str(fmt);
auto s = timestamp_ms_ / 1000;
auto ms = timestamp_ms_ % 1000;
auto text = fmt_timestamp(s, local, fmtstr);
if (show_milliseconds)
{
text += "." + fmt_uint(ms, 3);
}
return text;
}
std::string datetime::format(const std::string &fmt, bool local, bool show_milliseconds) const
{
auto s = timestamp_ms_ / 1000;
auto ms = timestamp_ms_ % 1000;
auto text = fmt_timestamp(s, local, fmt);
if (show_milliseconds)
{
text += "." + fmt_uint(ms, 3);
}
return text;
}
datetime &datetime::operator=(const datetime &other)
{
if (this == &other)
{
return *this;
}
timestamp_ms_ = other.timestamp_ms_;
return *this;
}
datetime datetime::operator+(uint64_t ms)
{
datetime dt(*this);
dt.timestamp_ms_ += ms;
return dt;
}
datetime datetime::operator-(uint64_t ms)
{
datetime dt(*this);
dt.timestamp_ms_ -= ms;
return dt;
}
datetime &datetime::operator+=(uint64_t ms)
{
timestamp_ms_ += ms;
return *this;
}
datetime &datetime::operator-=(uint64_t ms)
{
timestamp_ms_ -= ms;
return *this;
}
int64_t datetime::operator-(const datetime &other) const
{
int64_t diff = timestamp_ms_ - other.timestamp_ms_;
return diff;
}
std::string datetime::supported_time_formats(const time_regex_vec_type &fmtlist)
{
std::string time_fmts;
for (size_t i = 0; i < fmtlist.size(); i++)
{
time_fmts += fmtlist[i].first + "\n";
}
return time_fmts;
}
bool datetime::verify_time(const struct tm &time)
{
// 校验年
if (time.tm_year < 1900)
{
CUTL_ERROR("the year should be >= 1900");
return false;
}
// 校验月
if (time.tm_mon < 1 || time.tm_mon > 12)
{
CUTL_ERROR("the month should be between 1 and 12");
return false;
}
// 校验日
std::vector<int> large_month = {1, 3, 5, 7, 8, 10, 12};
if (std::find(large_month.begin(), large_month.end(), time.tm_mon) != large_month.end() && (time.tm_mday < 1 || time.tm_mday > 31))
{
CUTL_ERROR("the day should be between 1 and 31 for " + std::to_string(time.tm_mon) + " month");
return false;
}
std::vector<int> small_month = {4, 6, 9, 11};
if (std::find(small_month.begin(), small_month.end(), time.tm_mon) != small_month.end() && (time.tm_mday < 1 || time.tm_mday > 30))
{
CUTL_ERROR("the day should be between 1 and 30 for " + std::to_string(time.tm_mon) + " month");
return false;
}
if (time.tm_mon == 2)
{
auto is_leap_year = [](int year)
{ return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); };
if (is_leap_year(time.tm_year) && (time.tm_mday < 1 || time.tm_mday > 29))
{
CUTL_ERROR("the day should be between 1 and 29 for " + std::to_string(time.tm_year) + "-" + fmt_uint(time.tm_mon, 2));
return false;
}
if (!is_leap_year(time.tm_year) && (time.tm_mday < 1 || time.tm_mday > 28))
{
CUTL_ERROR("the day should be between 1 and 28 for " + std::to_string(time.tm_year) + "-" + fmt_uint(time.tm_mon, 2));
return false;
}
}
// 校验时分秒
if (time.tm_hour < 0 || time.tm_hour > 23)
{
CUTL_ERROR("the hour should be between 0 and 23");
return false;
}
if (time.tm_min < 0 || time.tm_min > 59)
{
CUTL_ERROR("the minute should be between 0 and 59");
return false;
}
if (time.tm_sec < 0 || time.tm_sec > 59)
{
CUTL_ERROR("the second should be between 0 and 59");
return false;
}
return true;
}
std::ostream &operator<<(std::ostream &os, const datetime &dt)
{
os << dt.format();
return os;
}
} // namespace
6. 测试代码
#pragma once
#include <iostream>
#include "datetime.h"
#include "common.hpp"
void TestDatetimeBasicUsage()
{
PrintSubTitle("TestDatetimeBasicUsage");
auto now = cutl::datetime::now();
std::cout << "current timestamp(ms): " << now.timestamp() << std::endl;
std::cout << "current time(UTC time): " << now.utctime() << std::endl;
std::cout << "current time(local time): " << now.format() << std::endl;
std::cout << "current time(UTC time) format b: " << now.format(cutl::datetime_format::datetime_format_b, false, true) << std::endl;
std::cout << "current time(UTC time) format b, don't show milliseconds: " << now.format(cutl::datetime_format::datetime_format_b, false, false) << std::endl;
std::cout << "current time(UTC time) format c: " << now.format(cutl::datetime_format::datetime_format_c, false, true) << std::endl;
std::cout << "current time(UTC time) format d: " << now.format(cutl::datetime_format::datetime_format_d, false, true) << std::endl;
std::cout << "current time(UTC time) custom format 1: " << now.format("%c %Z", false, true) << std::endl;
std::cout << "current time(UTC time) custom format 2: " << now.format("%m/%d/%Y/ %H:%M:%S", false, false) << std::endl;
}
void TestDatetimeOperator()
{
// 运算符重载
PrintSubTitle("TestDatetimeOperator");
std::cout << "one day == " << cutl::datetime::day << "ms" << std::endl;
std::cout << "one hour == " << cutl::datetime::hour << "ms" << std::endl;
std::cout << "one minute == " << cutl::datetime::min << "ms" << std::endl;
auto now = cutl::datetime::now();
std::cout << "current time: " << now << std::endl;
auto dt1 = now - cutl::datetime::min;
std::cout << "before one minute: " << dt1 << std::endl;
// std::cout << "current time 1: " << now << std::endl;
auto dt2 = now + cutl::datetime::min;
std::cout << "after one minute: " << dt2 << std::endl;
// std::cout << "current time 2: " << now << std::endl;
now -= (2 * cutl::datetime::hour);
std::cout << "before two hours: " << now << std::endl;
now += (4 * cutl::datetime::hour);
std::cout << "after two hours: " << now << std::endl;
auto dt3 = cutl::datetime::get("2024-03-01 10:00:00");
auto dt4 = cutl::datetime::get("2024-03-30 14:18:44");
auto duration1 = dt4 - dt3;
std::cout << "the difference between " << dt3 << " and " << dt4 << " is: " << duration1 << "ms, formatted: " << cutl::fmt_timeduration_ms(duration1) << std::endl;
auto duration2 = dt3 - dt4;
std::cout << "the difference between " << dt4 << " and " << dt3 << " is: " << duration2 << "ms" << std::endl;
}
void TestDatetimeParseString()
{
// 字符串解析成时间
PrintSubTitle("TestDatetimeParseString");
auto dt0 = cutl::datetime::get(" 2024-03-02 14:18:44 ");
std::cout << "dt0: " << dt0 << std::endl;
auto dt1 = cutl::datetime::get(" 2024-03-02 14:18:44.023 ");
std::cout << "dt1: " << dt1 << std::endl;
auto dt2 = cutl::datetime::get(" 2024.03.12 14:18:44");
std::cout << "dt2: " << dt2 << std::endl;
auto dt3 = cutl::datetime::get(" 2024.03.12 14:18:44.003");
std::cout << "dt3: " << dt3 << std::endl;
auto dt4 = cutl::datetime::get("2024/03/22 14:18:44 ");
std::cout << "dt4: " << dt4 << std::endl;
auto dt5 = cutl::datetime::get("2024/03/22 14:18:44.200 ");
std::cout << "dt5: " << dt5 << std::endl;
auto dt6 = cutl::datetime::get("2024/03/23 09:28:04");
std::cout << "dt6: " << dt6 << std::endl;
auto dt7 = cutl::datetime::get("2024/03/23 09:28:04.276");
std::cout << "dt7: " << dt7 << std::endl;
// format error
auto dt8 = cutl::datetime::get(" 2024-0322 14:18:44 ");
std::cout << "dt8: " << dt8 << std::endl;
// mounth error
auto dt9 = cutl::datetime::get(" 2024-13-02 14:18:44 ");
std::cout << "dt9: " << dt9 << std::endl;
// leap year error
auto dt10 = cutl::datetime::get(" 2023-02-29 14:18:44 ");
std::cout << "dt10: " << dt10 << std::endl;
// day error
auto dt11 = cutl::datetime::get(" 2024-03-42 14:18:44 ");
std::cout << "dt11: " << dt11 << std::endl;
// year > 2038
auto dt12 = cutl::datetime::get(" 2044-03-02 14:18:44 ");
std::cout << "dt12: " << dt12 << std::endl;
std::cout << "dt12 timestamp: " << dt12.timestamp() << std::endl;
}
void TestDatetime()
{
PrintTitle("datetime");
TestDatetimeBasicUsage();
TestDatetimeOperator();
TestDatetimeParseString();
}
7. 运行结果
==============================================datetime==============================================
---------------------------------------TestDatetimeBasicUsage---------------------------------------
current timestamp(ms): 1716129275853
current time(UTC time): 2024-05-19 14:34:35.853
current time(local time): 2024-05-19 22:34:35.853
current time(UTC time) format b: 2024.05.19 14:34:35.853
current time(UTC time) format b, don't show milliseconds: 2024.05.19 14:34:35
current time(UTC time) format c: 2024/05/19 14:34:35.853
current time(UTC time) format d: 20240519 14:34:35.853
current time(UTC time) custom format 1: Sun May 19 14:34:35 2024 UTC.853
current time(UTC time) custom format 2: 05/19/2024/ 14:34:35
----------------------------------------TestDatetimeOperator----------------------------------------
one day == 86400000ms
one hour == 3600000ms
one minute == 60000ms
current time: 2024-05-19 22:34:35.854
before one minute: 2024-05-19 22:33:35.854
after one minute: 2024-05-19 22:35:35.854
before two hours: 2024-05-19 20:34:35.854
after two hours: 2024-05-20 00:34:35.854
the difference between 2024-03-01 10:00:00.000 and 2024-03-30 14:18:44.000 is: 2521124000ms, formatted: 29d:04h:18m:44s.000ms
the difference between 2024-03-30 14:18:44.000 and 2024-03-01 10:00:00.000 is: -2521124000ms
--------------------------------------TestDatetimeParseString---------------------------------------
dt0: 2024-03-02 14:18:44.000
dt1: 2024-03-02 14:18:44.023
dt2: 2024-03-12 14:18:44.000
dt3: 2024-03-12 14:18:44.003
dt4: 2024-03-22 14:18:44.000
dt5: 2024-03-22 14:18:44.200
dt6: 2024-03-23 09:28:04.000
dt7: 2024-03-23 09:28:04.276
[2024-05-19 22:34:35.857][E]]0x7ff844a9b100](cutl) [datetime.cpp:79:get] Only the following time formats are supported:
YYYY-MM-DD HH:MM:SS.sss
YYYY-MM-DD HH:MM:SS
YYYY.MM.DD HH:MM:SS.sss
YYYY.MM.DD HH:MM:SS
YYYY/MM/DD HH:MM:SS.sss
YYYY/MM/DD HH:MM:SS
YYYYMMDD HH:MM:SS.sss
YYYYMMDD HH:MM:SS
dt8: 1970-01-01 08:00:00.000
[2024-05-19 22:34:35.857][E]]0x7ff844a9b100](cutl) [datetime.cpp:241:verify_time] the month should be between 1 and 12
dt9: 1970-01-01 08:00:00.000
[2024-05-19 22:34:35.857][E]]0x7ff844a9b100](cutl) [datetime.cpp:268:verify_time] the day should be between 1 and 28 for 2023-02
dt10: 1970-01-01 08:00:00.000
[2024-05-19 22:34:35.858][E]]0x7ff844a9b100](cutl) [datetime.cpp:248:verify_time] the day should be between 1 and 31 for 3 month
dt11: 1970-01-01 08:00:00.000
dt12: 2044-03-02 14:18:44.000
dt12 timestamp: 2340512324000
8. 源码地址
更多详细代码,请查看本人写的C++ 通用工具库: common_util, 本项目已开源,代码简洁,且有详细的文档和Demo。