8

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.

LihO
  • 41,190
  • 11
  • 99
  • 167
emufossum13
  • 377
  • 1
  • 10
  • 21
  • You should probably revisit the choice of selected answers. The current accepted answer demonstrates how to do things incorrectly. – jww Mar 26 '19 at 03:28

4 Answers4

17

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.

LihO
  • 41,190
  • 11
  • 99
  • 167
  • nice answer. Google search brought me here when trying to find a way to validate an entered date in C. Your method also handles leap years and other edge-cases which is great. – DaV Feb 28 '14 at 19:01
  • There's a catch: If you enter a year lower than 1970 (epoch year), it will get normalized to 1970 and the function will fail – dario_ramos Jan 30 '17 at 06:04
8

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.

Joe
  • 346
  • 4
  • 15
7

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
sehe
  • 374,641
  • 47
  • 450
  • 633
  • I've just refactored my answer to show (a) how to efficiently reuse the imbued stream for parsing (b) that it validates inputs – sehe Oct 20 '13 at 21:46
  • 1
    I think this is the best answer. Other solutions say `31/09/2020` is a valid date, while it isn't. Looks like they only parse characters. – hudac Oct 22 '20 at 11:13
5

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

DrM
  • 154
  • 7
  • is there a way that this can work with a string? I put this into some code and set a variable buffer as "05/20/13", unfortunately it's saying that there is no suitable conversion between string and const char. – emufossum13 Oct 20 '13 at 20:47
  • Yes, if you use a std::string, than buffer.c_str() is what you need there. – DrM Oct 20 '13 at 21:02
  • This is how I have the variable decleration set up std::string buffer = buffer.c_str(); buffer = "05/10/1996"; is that what your talking about? – emufossum13 Oct 20 '13 at 21:08
  • Not really. If you have std::string buffer = "05/20/13" then in sscanf you should use buffer.c_str(). Check documentation for sscanf here: http://www.cplusplus.com/reference/cstdio/sscanf/ – DrM Oct 20 '13 at 21:12
  • 1
    @emufossum13: I guess `strptime` is not available on your system (maybe Windows?). See edit of my answer, it also validates the date :) – LihO Oct 20 '13 at 21:16
  • You should check the return value of `sscanf`. See the [`sscanf(3)` man page](https://linux.die.net/man/3/sscanf). In the bigger picture, why would you [incorrectly] use a dangerous C function when you have C++? – jww Mar 26 '19 at 03:27