emufossum13
emufossum13

Reputation: 407

How to parse and validate a date in std::string in C++?

I'm working on a project where I have to read a date to make sure that it's a valid date. For example, February 29th is only a valid date on leap years, or June 31st is not a valid date, so the computer would output that information based on the input. My issue is that I can't figure out how to parse the string so that the user can enter "05/11/1996" as a date (for example) and then take that and put it into seperate integers. I was thinking about trying to do something with a while loop and string stream, but I'm a little stuck. If someone could help me with this, I would really appreciate it.

Upvotes: 10

Views: 42174

Answers (5)

Howard Hinnant
Howard Hinnant

Reputation: 218780

Adding an answer for C++20's <chrono> facilities:

One can use std::chrono::parse and std::chrono::year_month_day to parse and validate a date from std::cin like this:

#include <chrono>
#include <iostream>

int
main()
{
    std::cout << "Enter date mm/dd/yyyy : ";
    std::chrono::year_month_day ymd;
    std::cin >> std::chrono::parse("%m/%d/%Y", ymd);
    if (std::cin.fail())
        std::cout << "date is invalid\n";
    else
        std::cout << "date is valid\n";
}

If "05/11/1996" is entered, this program will output "date is valid".

Note that it is not clear from the question if the format "%m/%d/%Y" is desired, or the format "%d/%m/%Y". Adjust as necessary.

One can parse from any stream (fstream, stringstream, etc.) with the same syntax. Invalid dates, either with incorrect syntax, or values for the year, month and day that don't match a date in the civil calendar, will set failbit in the stream.

See this table for a complete list of the parse flags that are available for use.

One could also parse into a std::chrono::sys_days. year_month_day and sys_days are both "date types". They are just different data structures for holding the same thing (analogous to vector and list both holding a sequence).

year_month_day is a {year, month, day} data structure. It is good at retrieving the year, month and day fields, and calendrical month and year arithmetic.

sys_days is a {count of days from 1970-01-01} data structure. It is good at day-precision arithmetic, and converting to "date time" types such as system_clock::time_point.

year_month_day and sys_days will convert to one another with implicit conversion syntax.

Upvotes: 1

Joe
Joe

Reputation: 346

Another option is to use std::get_time from the <iomanip> header (available since C++11). A good example of its use can be found here.

Upvotes: 8

sehe
sehe

Reputation: 393084

I'd prefer to use Boost DateTime:

See it Live on Coliru

#include <iostream>
#include <boost/date_time/local_time/local_time.hpp>

struct dateparser
{
    dateparser(std::string fmt)
    {
        // set format
        using namespace boost::local_time;
        local_time_input_facet* input_facet = new local_time_input_facet();
        input_facet->format(fmt.c_str());
        ss.imbue(std::locale(ss.getloc(), input_facet));
    }

    bool operator()(std::string const& text)
    {
        ss.clear();
        ss.str(text);

        bool ok = ss >> pt;

        if (ok)
        {
            auto tm = to_tm(pt);
            year    = tm.tm_year;
            month   = tm.tm_mon + 1; // for 1-based (1:jan, .. 12:dec)
            day     = tm.tm_mday;
        }

        return ok;
    }

    boost::posix_time::ptime pt;
    unsigned year, month, day;

  private:
    std::stringstream ss;
};

int main(){
    dateparser parser("%d/%m/%Y"); // not thread safe

    // parse
    for (auto&& txt : { "05/11/1996", "30/02/1983", "29/02/2000", "29/02/2001" })
    {
        if (parser(txt))
            std::cout << txt << " -> " << parser.pt << " is the " 
                 << parser.day      << "th of "
                 << std::setw(2)    << std::setfill('0') << parser.month
                 << " in the year " << parser.year       << "\n";
        else
            std::cout << txt << " is not a valid date\n";
    }
}

Outputs:

05/11/1996 -> 1996-Nov-05 00:00:00 is the 5th of 11 in the year 96
30/02/1983 is not a valid date
29/02/2000 -> 2000-Feb-29 00:00:00 is the 29th of 02 in the year 100
29/02/2001 is not a valid date

Upvotes: 7

LihO
LihO

Reputation: 42083

A possible solution might be also based on strptime, however note that this function only validates whether the day is from the interval <1;31> and month from <1;12>, i.e. "30/02/2013" is valid still:

#include <iostream>
#include <ctime>

int main() {
    struct tm tm;
    std::string s("32/02/2013");
    if (strptime(s.c_str(), "%d/%m/%Y", &tm))
        std::cout << "date is valid" << std::endl;
    else
        std::cout << "date is invalid" << std::endl;
}
But since strptime is not always available and additional validation would be nice, here's what you could do:
  1. extract day, month, year
  2. fill struct tm
  3. normalize it
  4. check whether normalized date is still the same as retrieved day, month, year

i.e.:

#include <iostream>
#include <sstream>
#include <ctime>

// function expects the string in format dd/mm/yyyy:
bool extractDate(const std::string& s, int& d, int& m, int& y){
    std::istringstream is(s);
    char delimiter;
    if (is >> d >> delimiter >> m >> delimiter >> y) {
        struct tm t = {0};
        t.tm_mday = d;
        t.tm_mon = m - 1;
        t.tm_year = y - 1900;
        t.tm_isdst = -1;

        // normalize:
        time_t when = mktime(&t);
        const struct tm *norm = localtime(&when);
        // the actual date would be:
        // m = norm->tm_mon + 1;
        // d = norm->tm_mday;
        // y = norm->tm_year;
        // e.g. 29/02/2013 would become 01/03/2013

        // validate (is the normalized date still the same?):
        return (norm->tm_mday == d    &&
                norm->tm_mon  == m - 1 &&
                norm->tm_year == y - 1900);
    }
    return false;
}

used as:

int main() {

    std::string s("29/02/2013");
    int d,m,y;

    if (extractDate(s, d, m, y))
        std::cout << "date " 
                  << d << "/" << m << "/" << y
                  << " is valid" << std::endl;
    else
        std::cout << "date is invalid" << std::endl;
}

which in this case would output date is invalid since normalization would detect that 29/02/2013 has been normalized to 01/03/2013.

Upvotes: 19

DrM
DrM

Reputation: 154

If the format is like in your example, you could take out the integer like this:

int day, month, year;
sscanf(buffer, "%2d/%2d/%4d",
    &month,
    &day,
    &year);

where of course in buffer you have the date ("05/11/1996" )

Upvotes: 5

Related Questions