I'm working on an application that will make extensive use of dates, there will be various forms of input such as a GUI or text files and the data will be stored in a MySQL database.
The planned use for this class is in Models, Model View and Model to Database interface classes that require dates. One of the models will have at least 6 dates involved. Almost all of the models will have at least one date (creation date of the record in the database).
The class is pretty basic, it needs to be able to convert dates to multiple formats.
I searched for existing libraries before implementing this, that is when I discovered std::put_time and std::get_time. If you are aware of any libraries that provide this functionality, please comment as well.
Development Environment:
Ubuntu 22.04
GCC 12
CMake 3.31
C++23 (should work in previous version, since C++17).
The Code:
DateData.h
#ifndef DATEDATA_H_
#define DATEDATA_H_
/*
* Contains Date data as year, month and day. Dates are stored as actual dates
* and not as an offset from January 1, 1900.
* Provides various formats for input and output. The primary form of output is
* a MySQL date format. Currently only supports USA English formating of dates
* for both input and output.
*/
#include <ctime>
#include <string>
class DateData
{
public:
DateData() : year{0}, month{0}, day{0} {};
DateData(unsigned int yearIn, unsigned int monthIn, unsigned int dayIn)
: year{yearIn}, month{monthIn}, day{dayIn} {};
DateData(std::string dateString);
DateData(std::time_t currentTime);
~DateData() = default;
std::string getDBDateValue() { return USEnglishDateUsingFormat("%Y-%Om-%Od"); };
std::string getFormalUSEnglishDate() { return USEnglishDateUsingFormat("%B %d, %Y"); };
std::string getUsEnglishDateWithSlashes() { return USEnglishDateUsingFormat("%m/%d/%Y"); };
std::string getUsEnglishDateWithDashes() { return USEnglishDateUsingFormat("%m-%d-%Y"); };
private:
void translateFromStdTM(std::tm someDate);
std::tm translateToStdTM();
std::string USEnglishDateUsingFormat(const char* format);
unsigned int year;
unsigned int month;
unsigned int day;
};
#endif // DATEDATA_H_#include "DateData.h"
DateData.cpp
#include <ctime>
#include <iomanip>
#include <iostream>
#include <locale>
#include <sstream>
#include <vector>
DateData::DateData(std::string dateString)
: year{0}, month{0}, day{0}
{
std::locale usEnglish("en_US.UTF-8");
std::vector<std::string> legalFormats = {
{"%B %d, %Y"},
{"%m/%d/%Y"},
{"%m-%d-%Y"},
{"%Y-%m-%d"}
};
for (auto legalFormat: legalFormats)
{
std::tm dateFromString{};
std::istringstream ss(dateString);
ss.imbue(usEnglish);
ss >> std::get_time(&dateFromString, legalFormat.c_str());
if (!ss.fail())
{
translateFromStdTM(dateFromString);
return;
}
}
}
DateData::DateData(std::time_t currentTime)
{
std::tm* now = std::localtime(¤tTime);
translateFromStdTM(*now);
}
void DateData::translateFromStdTM(std::tm someDate)
{
year = someDate.tm_year + 1900;
month = someDate.tm_mon + 1;
day = someDate.tm_mday;
}
std::tm DateData::translateToStdTM()
{
std::tm LinuxDate{};
LinuxDate.tm_year = year - 1900;
LinuxDate.tm_mon = month -1;
LinuxDate.tm_mday = day;
return LinuxDate;
}
std::string DateData::USEnglishDateUsingFormat(const char *format)
{
std::locale usEnglish("en_US.UTF-8");
std::tm dateConverter = translateToStdTM();
std::stringstream ss;
ss.imbue(usEnglish);
ss << std::put_time(&dateConverter, format);
return ss.str();
}
main.cpp
#include "DateData.h"
#include <iostream>
#include <string>
#include <vector>
void outputTest(DateData& test)
{
std::cout << "\tTo database format:\t" << test.getDBDateValue() << "\n";
std::cout << "\tTo Formal US English format:\t" << test.getFormalUSEnglishDate() << "\n";
std::cout << "\tTo US with Slashes format:\t" << test.getUsEnglishDateWithSlashes() << "\n";
std::cout << "\tTo US with Dashes format:\t" << test.getUsEnglishDateWithDashes() << "\n\n";
}
void testInputAndOutput(std::string &input)
{
DateData test(input);
std::cout << "testing " << input << ":\n";
outputTest(test);
}
void testInputAndOutput(unsigned int year, unsigned int month, unsigned int day)
{
DateData test(year, month, day);
std::cout << "testing: year = " << year << " month = " << month << " day = " << day << ":\n";
outputTest(test);
}
int main()
{
std::vector<std::string> testStrings = {
"May 13, 2025",
"5/13/2025",
"5-13-2025",
"2025年4月5日"
};
for (auto testString: testStrings)
{
testInputAndOutput(testString);
}
testInputAndOutput(1995, 5, 13);
testInputAndOutput(1977, 7, 7);
std::time_t currentTime = std::time(0);
DateData today(currentTime);
std::cout << "Testing the TM interface:\n";
outputTest(today);
}
CMakeLists.txt
cmake_minimum_required(VERSION 3.31)
project(TestDateData LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_COMPILE_WARNING_AS_ERROR ON)
if(POLICY CMP0167)
cmake_policy(SET CMP0167 NEW)
endif()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Wextra -pedantic")
add_executable(TestDateData
main.cpp
DateData.h DateData.cpp
)
3 Answers 3
Make better use of the standard library
C++20 comes with extensive facilities to deal with dates. Instead of storing year, month and day as integers, use std::chrono::year_month_day
. Also, it has facilities to format to string and parse from string using arbitrary date formats. This type also avoid most of the issues J_H mentioned.
In fact, I would not create a class DateData
at all, and rather just create functions that do whatever conversions you need that don't have a simple counterpart in the standard library, like parsing a string from "any of the legal" formats.
Also, the less you use old C types and the more you use std::chrono
types, the simpler your code will be.
It's very US-centric
Your code only deals with ISO and US date formats. However, most other countries in the world use a different (some would say a more sensible) order for day, month and year. So this makes your code less usable outside the US. It would be nice if you can avoid hardcoding the date formats and the locale used.
Also, even if you just deal with the US, what about timezones?
-
1\$\begingroup\$ And this is pretty much what I needed to know. \$\endgroup\$2025年05月14日 19:58:29 +00:00Commented May 14 at 19:58
-
1\$\begingroup\$ std::chrono::parse() is not in my version of gcc. :( \$\endgroup\$2025年05月15日 21:08:42 +00:00Commented May 15 at 21:08
invariant
* ... Dates are stored as actual dates
Thank you for explaining that class invariant, it's helpful. We can write tests against it.
DateData() : year{0}, month{0}, day{0} {};
Ouch, the no-arg default ctor just violated that constraint!
What comes before the year 1 A.D.?
Not zero.
And we can't represent 1 B.C. using unsigned
.
Just as some floats are not-a-number, this date data is not-a-year.
Similarly for the other two fields, as e.g. month zero does not precede January.
The class documentation should call out this special sentinel value. I assume there's a requirement for class methods to signal error if they are ever passed not-a-date.
date range
unsigned int year;
unsigned int month;
unsigned int day;
Certainly this works. But as a consumer of this library I would be happier knowing whether this fits within uint32_t. For example, are years after \65,535ドル\$ prohibited? Are month and day always sane, compatible, and fit within a byte? I find it distressing that the second constructor will accept (2025, 2, 31) and similar nonsense. A proper validator would need to understand the concept of Gregorian leap years to figure out if a given Februrary 29th exists.
proleptic Gregorian calendar
By choosing to support a representation for dates that are prior to 1900, you open a whole can of worms. Your documentation should explain to historians and astronomers that the library may not fit some of their use cases. We are skipping over the whole business of old style vs. new style.
Consider adopting a much simpler time representation: seconds since the epoch. (Where January 1970 might be a convenient epoch. Or perhaps January 1900, with number of seconds stored as uint64_t.) Then you can break out {y, m, d} from such a timestamp on each method call, and rest assured they won't form a nonsensical 3-tuple of values, like February 31st.
date vs. time
std::tm LinuxDate{};
This looks more like a LinuxTime
to me, representing an instant in time.
I don't see anywhere that {h, m, s} are forced to zero (midnight).
And the more conventional name would be UnixTime
.
repeated locale use
std::locale usEnglish("en_US.UTF-8");
...
ss.imbue(usEnglish);
Recommend you benchmark and profile a workload that uses this library. I don't know how that locale lookup performs, but I worry that it might involve filesystem interactions on every call, since we're not caching it across calls.
unit tests
for (auto testString: testStrings)
{
testInputAndOutput(testString);
}
testInputAndOutput(1995, 5, 13);
testInputAndOutput(1977, 7, 7);
Thank you, thank you for showing how to exercise the library. That's very nice, and it's helpful documentation. But it's not an automated unit test. It doesn't "know" the right answer; it won't report Red / Green status.
This library is an especially good fit for automated testing, since round-tripping a date between internal and text format should be the identity function. So a simple equality test ("did the date survive?") lets you report whether the test passed.
BTW, thank you for specifying the exact environment and flags this code is targeting.
-
1\$\begingroup\$ A previous version of the code had
if (year == 0 || month == 0 || day == 0)
error condition, but that version didn't use std::put_time to output the database string. \$\endgroup\$2025年05月14日 19:14:31 +00:00Commented May 14 at 19:14 -
1\$\begingroup\$ Re: "date range"... Just checked for my own interest. OP's ultimate goal (MySQL) only deals with YYYY (4 digits) in its "Date" datatype, according to a Google search... Just FYI... \$\endgroup\$user272752– user2727522025年05月14日 21:20:23 +00:00Commented May 14 at 21:20
-
1\$\begingroup\$ You didn't mention that there is no negative path testing. All good points. \$\endgroup\$2025年05月15日 12:57:42 +00:00Commented May 15 at 12:57
-
\$\begingroup\$ For a Date class do NOT use seconds since an epoc you do noit need the complexities of leap seconds, time zones or the extra storage of times. Store a number of days since an epoch e.g. Julian Daye - for modern times best use Modified JD or MJD2000 - or the number of days to match Excel \$\endgroup\$mmmmmm– mmmmmm2025年05月17日 20:14:52 +00:00Commented May 17 at 20:14
Human dates are highly convoluted.
- With daylight saving time (DST) the notation of one specific hour disappears (is illegal), and a half year later there are two hours with the "same" notation.
- Twenty four hours later may be one a different hour. One day later might not be 24 hours.
My conclusion is that internally one should opt for UTC internally, in database and logs, and human zoned date time on the front-end.
Your Do-It-Yeurself Date Time Library
Having a simple, reduced library is very alluring, promising faster development, shorter code. However I would refrain from it.
You have to deal with already well established things.
- Sorting on time parts probably needs YYYYMMDD (with %02d) and 24 hour time.
- Time zones. International foreign communication.
It would be better to create a documented collection of code snippets "How to Work with Time" in unit tests for many solutions - based on an existing library.
The work (functionality) is the same. The effort now a bit higher. The tool set will hold longer - new developers might already be acquainted with the library, and become acquainted with a more ubiquitous library.
That said
You might do a wrapping library in the style of inline functions, but mainly creating code snippets for all kind of needed functionality.
- Using dates in the database (UTC?)
- Representing dates, time zones
- Time calculation
- Sorting
- Business Knowledge, working days
This means you search for working solutions in an existing environment, instead of inventing new conventions. "Refrain from writing your own classes."
-
2\$\begingroup\$ Business Knowledge such as working days, starting hour and ending hour are stored elsewhere, specifically in a user profile. Sorting will be handled in the larger model that contains the dates. I did a Google search for C++ date libraries before I started coding. The application is already using 2 Boost libraries,
asio
andmysql
. \$\endgroup\$2025年05月15日 13:06:12 +00:00Commented May 15 at 13:06 -
2\$\begingroup\$ Okay with
boost
you got boos. Evidently you are not starting from scratch. And that was my fear: a fast built lib on a huge open-ended field.. \$\endgroup\$Joop Eggen– Joop Eggen2025年05月15日 13:56:35 +00:00Commented May 15 at 13:56
date
. \$\endgroup\$int
values". Handy if you needdatediff()
ordayofweek()
things... \$\endgroup\$