6
\$\begingroup\$

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(&currentTime);
 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
)
asked May 14 at 16:22
\$\endgroup\$
4
  • 1
    \$\begingroup\$ Apart from one tag, there appears to be nothing in this question regarding hh.mm.ss... It might 'stave off' some inappropriate answers (work) if you would be explicit about the "granularity" of the data your project will be processing... Are timezones and things like DST of any concern to you? \$\endgroup\$ Commented May 15 at 14:13
  • 1
    \$\begingroup\$ @Fe2O3 Good question, time zones are definitely not included in the application at all, hours and minutes are beyond the scope of this particular piece of the code, which is only concerned with dates. Time stamps are used in other areas. I didn't see a tag that was only date. \$\endgroup\$ Commented May 15 at 14:20
  • 1
    \$\begingroup\$ If you chose to roll yer own, I suggest sifting through CR and SO answers provided by @chux (to questions regarding 'dates'). Seems ol' @chux has formulated some really clever code for things like "two-way conversions between consecutive YYYYMMDD and consecutive int values". Handy if you need datediff() or dayofweek() things... \$\endgroup\$ Commented May 15 at 21:45
  • 1
    \$\begingroup\$ (Hesitant to make this suggestion...) The boys who have developed Excel (Google Sheets) have incorporated a LOT of functionality for both dates (and datetimes and times), AND database operations on 'tables' and 'recordsets', AND "UI i/o formatting". Just something to consider for your needs... (You can code DIY functions behind the sheet, too.) \$\endgroup\$ Commented May 15 at 22:02

3 Answers 3

8
\$\begingroup\$

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?

answered May 14 at 19:30
\$\endgroup\$
2
  • 1
    \$\begingroup\$ And this is pretty much what I needed to know. \$\endgroup\$ Commented May 14 at 19:58
  • 1
    \$\begingroup\$ std::chrono::parse() is not in my version of gcc. :( \$\endgroup\$ Commented May 15 at 21:08
6
\$\begingroup\$

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.

answered May 14 at 18:01
\$\endgroup\$
4
  • 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\$ Commented 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\$ Commented May 14 at 21:20
  • 1
    \$\begingroup\$ You didn't mention that there is no negative path testing. All good points. \$\endgroup\$ Commented 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\$ Commented May 17 at 20:14
5
\$\begingroup\$

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."

answered May 15 at 9:58
\$\endgroup\$
2
  • 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 and mysql . \$\endgroup\$ Commented 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\$ Commented May 15 at 13:56

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.