4
\$\begingroup\$

In modern C++, there is no class like datetime in the standard which can be used in the program. However, with additional of modules like <regex> and <chrono>, we can write the general purpose class like datetime.

Here is my current implementation for this:

#include<iostream>
#include<string>
#include<chrono>
#include<ctime>
#include<regex>
#include<array>
#include<utility>
#include<cstdlib>

// Month Enum which starts with 1.
enum Month
{
    Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
};

// Datastructure for string to num conversion in month(.i.e."Mar" Month to 3)
std::array<std::pair<std::string, int>, 12> monthinfo = {
    std::make_pair(std::string("Jan"), Month::Jan),
    std::make_pair(std::string("Feb"), Month::Feb),
    std::make_pair(std::string("Mar"), Month::Mar),
    std::make_pair(std::string("Apr"), Month::Apr),
    std::make_pair(std::string("May"), Month::May),
    std::make_pair(std::string("Jun"), Month::Jun),
    std::make_pair(std::string("Jul"), Month::Jul),
    std::make_pair(std::string("Aug"), Month::Aug),
    std::make_pair(std::string("Sep"), Month::Sep),
    std::make_pair(std::string("Oct"), Month::Oct),
    std::make_pair(std::string("Nov"), Month::Nov),
    std::make_pair(std::string("Dec"), Month::Dec),
};

// concrete daytime structure to store the data
template<typename T1, typename T2 = std::string>
struct DayTime
{
    T1 day   = T1();
    T1 month = T1();
    T1 year  = T1();
    T1 hour  = T1();
    T1 min   = T1();
    T1 second = T1();
    T2 daystr = T2();
    T2 dtstring = T2();
};


// main class which would fetech/parse the current time and provide to the client
class CurrentDateTime
{
public:
    CurrentDateTime();
    ~CurrentDateTime()      = default;
    int GetDay()            const { return dt.day; }
    int GetMonth()          const { return dt.month; }
    int GetYear()           const { return dt.year; }
    int GetHour()           const { return dt.hour; }
    int GetMin()            const { return dt.min; }
    int GetSecond()         const { return dt.second; }
    std::string GetDayStr() const { return dt.daystr; }
private:
    DayTime<std::string> ParseDateTime(const std::string&);
    void StrToNumber(const DayTime<std::string>&);
    int GetMonth(const std::string&);

    DayTime<int>  dt;
};


CurrentDateTime::CurrentDateTime()
{
    //fetch/store current local-daytime information
    auto tp = std::chrono::system_clock::now();
    time_t cstyle_t = std::chrono::system_clock::to_time_t(tp);
    char* cstyleinfo = ::ctime(&cstyle_t);
    // copy(deep) the data into the std::string as ::ctime() provides static data 
    // which might be overwritten in case someone call it again.
    std::string currentinfo{ cstyleinfo };

    //parse/store  the information
    auto dtstr = ParseDateTime(currentinfo);
    StrToNumber(dtstr);
}


DayTime<std::string> CurrentDateTime::ParseDateTime(const std::string& information)
{
    DayTime<std::string> info;
    std::regex dtimeregex{ R"(^(\w{3}) (\w{3}) (\d{2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$)" };
    std::smatch match;

    if (std::regex_search(information, match, dtimeregex)) {
        // Match the group and subgroups by regex parser.
        auto index = 0;
        info.dtstring = match[index++];
        info.daystr = match[index++];
        info.month = match[index++];
        info.day = match[index++];
        info.hour = match[index++];
        info.min = match[index++];
        info.second = match[index++];
        info.year = match[index++];
    }
    return info;
}


void CurrentDateTime::StrToNumber(const DayTime<std::string>& information)
{
    dt.dtstring = information.dtstring;
    dt.daystr = information.daystr;
    dt.month = GetMonth(information.month);
    dt.day = ::atoi(information.day.c_str());
    dt.hour = ::atoi(information.hour.c_str());
    dt.min = ::atoi(information.min.c_str());
    dt.second = ::atoi(information.second.c_str());
    dt.year = ::atoi(information.year.c_str());
}


int CurrentDateTime::GetMonth(const std::string& input) {
    for (const auto& itr : monthinfo) {
        if (itr.first == input) return itr.second;
    }
}



int main()
{
    CurrentDateTime current;

    std::cout << "Current Day:"    << current.GetDayStr() << "\n";
    std::cout << "Current Date:"   << current.GetDay() << "\n";
    std::cout << "Current Month:"  << current.GetMonth() << "\n";
    std::cout << "Current Year:"   << current.GetYear() << "\n";
    std::cout << "Current Hour:"   << current.GetHour() << "\n";
    std::cout << "Current Min:"    << current.GetMin() << "\n";
    std::cout << "Current Second:" << current.GetSecond() << "\n";

    return 0;
}

Output

Current Day:Tue
Current Date:4
Current Month:11
Current Year:2014
Current Hour:10
Current Min:43
Current Second:35

I would like to get the feedback/review comment for the above implementation.

The intent is to write a concrete, efficient, and portable class like datetime using modern C++ style/idioms.

\$\endgroup\$
  • 5
    \$\begingroup\$ Bad idea to break up the time into different parts. Better to store it as seconds since the epoch then convert as needed. Try doing the difference between two Dates. \$\endgroup\$ – Martin York Nov 4 '14 at 6:47
  • 2
    \$\begingroup\$ Take some inspiration from C# DateTime - it is using 64bit integer internally (100-nanosecond intervals since 1.1.1 0:00 + two bits are used to store the kind - UTC/Local/Unknown). \$\endgroup\$ – user52292 Nov 4 '14 at 7:20
  • 1
    \$\begingroup\$ @LokiAstari Not to mention leap years, time zones, DST policies, and leap seconds (in increasing order of obscurity). \$\endgroup\$ – Rhymoid Nov 4 '14 at 13:38
  • 1
    \$\begingroup\$ @Rhymoid: If you still have a pinch of happiness to spare after leapseconds, local time discontinuities will finally demoralize you: stackoverflow.com/questions/6841333/… \$\endgroup\$ – phresnel Nov 4 '14 at 14:40
  • \$\begingroup\$ @phresnel: all the more reason to use TAI internally, and leave these calendrical screwups to extensions. \$\endgroup\$ – Rhymoid Nov 4 '14 at 14:47
2
\$\begingroup\$

Improvements in the code and style:

Little bit of a personal preference here, but I think it makes sense putting a space between the #include and the filename. E.g.:

#include <iostream>

Consider making enum Month an enum class. This will enclose the loose month constants into a scope. Less chance of name conflict.

monthinfo should be const, unless you plan on changing it somewhere. You should not apply the explicit std::string casts to the month names. This will generate an additional unnecessary copy of the string. std::string is implicitly constructible from a char array.

Also, in monthinfo you have used and int for the month number. It should have been the Month enum instead.

Edited code:

enum class Month
{
    Jan = 1, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec
};

const std::array<std::pair<std::string, Month>, 12> monthinfo {
    std::make_pair("Jan", Month::Jan),
    std::make_pair("Feb", Month::Feb),
    std::make_pair("Mar", Month::Mar),
    std::make_pair("Apr", Month::Apr),
    std::make_pair("May", Month::May),
    std::make_pair("Jun", Month::Jun),
    std::make_pair("Jul", Month::Jul),
    std::make_pair("Aug", Month::Aug),
    std::make_pair("Sep", Month::Sep),
    std::make_pair("Oct", Month::Oct),
    std::make_pair("Nov", Month::Nov),
    std::make_pair("Dec", Month::Dec)  // <--- don't put a ',' in the last value
};

Align similar assignment/initialization blocks. There are a few instances of those in your code, such as in DayTime and ParseDateTime():

T1 day      = T1();
T1 month    = T1();
T1 year     = T1();
T1 hour     = T1();
T1 min      = T1();
T1 second   = T1();
T2 daystr   = T2();
T2 dtstring = T2();

...

info.dtstring = match[index++];
info.daystr   = match[index++];
info.month    = match[index++];
info.day      = match[index++];
info.hour     = match[index++];
info.min      = match[index++];
info.second   = match[index++];
info.year     = match[index++];

There's also a similar block that should be aligned in StrToNumber().


I don't see any particular reason for declaring the default destructor in CurrentDateTime. If it doesn't need any special cleanup, don't provide a destructor at all. The default would only make sense if this was a default virtual destructor.


The ctime() function, when including <ctime>, is a member of namespace std. So call it using the std:: prefix, not the global namespace :: prefix.

atoi() is a C library function that was deprecated in C++ in favor of the new stoi() function introduced by C++11.


GetMonth() is arguably broken. It lacks a default return path. If the input string is not a valid month name, due to a typo in the call site or plain misuse, it would return garbage.

It should also be a const function since it is not changing any member state. But since it doesn't access any member state at all, it could arguably be static.

Its return type is the enum Month, not int.

Month CurrentDateTime::GetMonth(const std::string& input) const {
    for (const auto& itr : monthinfo) {
        if (itr.first == input) {
            return itr.second;
        }
    }
    assert(false && "Invalid month name");
    // Or return a default month if it makes sense.
    // Another possibility is extending the enum Month to 
    // add an invalid dummy value, throwing an exception, etc.
}
\$\endgroup\$
6
\$\begingroup\$

Some feedback (not a full list):

Design

Your class is not a datetime class but a "current date time" class (i.e. "how would I represent 18.feb.2028 using your code?")

You should instead create a DateTime class (change the name), and add a static or friend function similar to this:

DateTime GetCurrent();

or (as - I believe - .NET libraries do):

DateTime Now();

Your class should:

  • be a regular type, by implementing having an identity (operator == and operator != should be implemented with expected semantics and a complete sort order (same for operators <, >, <= and >=).

  • support initialization with other dates and times (than the current one) and validate these values in the init. process.

  • support stream input and output (probably with custom stream formatting adapter) so that you could write code like this:

    std::istringstream buffer { "12.feb.2014 12:04" };
    DateTime dtm;
    buffer >> format("dd.mm.yyyy hh:mm") >> dtm;
    // fmt:   ^^^^^^^^^^^^^^^^^^^^^^^^^^
    
  • support operations with deltas (for example, "add two days to this date" or "add 10000 seconds to this date").

Implementation

int CurrentDateTime::GetMonth(const std::string& input) {
    for (const auto& itr : monthinfo) {
        if (itr.first == input) return itr.second;
    }
}

My compiler would say: "warning: not all control paths return a value".

\$\endgroup\$
3
\$\begingroup\$

The three letter coded month names are depending on the locale. Same goes for date formats like yyyy-MM-dd or MMM dd/yyy. You have to keep that in mind. The are some ISO standars for time formats.

Also DateTime types require operations to add days, hours and the like. Best way would be to implement types for Date and Time first and the just combine them. You might want to also implement a TimeSpan type for certain operations like substracting dates.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.