Title: C++ boost date_input_facet seems to parse dates unexpectedly with incorrect formats passed to the facet constructor
Tags: c++;date;parsing;boost
Question: Toy code in coliru I am using for testing:
http://coliru.stacked-crooked.com/a/4039865d8d4dad52
I am getting used to C++ again after a long hiatus from it.  I am writing code that parses a CSV that may have several columns with dates or nulls.  My assumption is that every date column has exactly one kind of valid date format though different columns may have different formats.
For each date column that I have, I find the first value that is successfully parsed as a date given an st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0vector of potential locales with a boost date_input_facet object.  That first date that parses correctly will then return the index in my array of locales that worked.  Once I have the appropriate format for the first parsable date, I want to fix that format forever more so that I no longer have to waste CPU time detecting the format.
Here is my array of locales:
const st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0vector<st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0locale&gt; Dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0date_formats = {
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0locale(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0local75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0classic(), new date_input_facet("%Y-%m-%d")),
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0locale(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0local75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0classic(), new date_input_facet("%Y/%m/%d")),
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0locale(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0local75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0classic(), new date_input_facet("%m-%d-%Y")),
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0locale(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0local75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0classic(), new date_input_facet("%m/%d/%Y")),
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0locale(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0local75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0classic(), new date_input_facet("%d-%b-%Y")),
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0locale(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0local75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0classic(), new date_input_facet("%Y%m%d")),
I use an array of date strings from 20170101 to 20170131 to test this out.  I then print out the original date strings, the date that was parsed, along with the index of the date_formats vector that worked for parsing.
For 20170101 to 201700129, it says that the 0th index worked which is supposed to have the "%Y-%m-%d" format with the dashes?!?!  Moreover, where the dashes go, I have numbers so it is reads 20170101 as 2017-10- then drop the last dash and interprets it as October 2017 which without a date is Oct 1, 2017.  Why would it do that when that is not the format it was supposed to use?
Some results that one could see from my coliru (pY is parsed year, etc):
YYYYMMDD    pY     pM   pD  format_index
20170101    2017    Oct 1   0
20170102    2017    Oct 1   0
20170103    2017    Oct 1   0
20170104    2017    Oct 1   0
20170105    2017    Oct 1   0
For 20170130, 20170131, the correct format index (the 5th) is reported for "%Y%m%d".
Any ideas?  I only want the precise format string I passed to be used.

Here is the accepted answer: I've made a multi-format capable date-time parser myself. I, too, found it hard/impossible to get the parsing strict using the facilities in the standard library and boost.
I ended up using strptime - mostly¹.
adaptive_parser
Intended to be seeded with a list of supported formats, in order of
preference. By default, parser is not adaptive (mode is fixed).
In adaptive modes the format can be required to be
sticky (consistently reuse the first matched format)
ban_failed (remove failed patterns from the list; banning only occurs
on successful parse to avoid banning all patterns on invalid input)
mru (preserves the list but re-orders for performance)
Caution:
If formats are ambiguous (e.g. mm-dd-yyyy vs dd-mm-yyyy) allowing
re-ordering results in unpredictable results.
⇒ Only use mru when there are no ambiguous formats
NOTE:
The function object is stateful. In algorithms, pass it by reference
(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0ref(obj)) to avoid copying the patterns and to ensure correct
adaptive behaviour
I tried the parser on your test data:
#include "adaptive_parser.h"
#include <boost/date_time/gregorian/greg_date.hpp&gt;
#include <boost/date_time/posix_time/posix_time.hpp&gt;
#include <iostream&gt;
#include <sstream&gt;
#include <string&gt;
#include <vector&gt;
class Date{
public:
Date() : y(0), m(0), d(0) {}
Date(int yy, int mm, int dd) : y(yy), m(mm), d(dd) {}
Date(boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0gregorian75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0te dt) : y(dt.year()), m(dt.month()), d(dt.day()) {}
Date(st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string const&amp; delimitedString);
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string to_string() const;
int getYear()  const { return y; }
int getMonth() const { return m; }
int getDay()   const { return d; }
private:
using parser_t = myli75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0datetim75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0adaptive_parser;
parser_t parser { parser_t75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0ull_match,
{
"%Y-%m-%d", "%Y/%m/%d",
"%m-%d-%Y", "%m/%d/%Y",
"%d-%b-%Y",
"%Y%m%d",
} };
int y, m, d;
Dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0Date(const st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string&amp; delimitedString)
using namespace boost75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0posix_time;
auto t = ptime({1970,1,1}) + seconds(parser(delimitedString).count());
*this = Date(t.date());
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string Dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0to_string() const
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0ostringstream os;
os << st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0setfill('0')
<< st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0setw(4) << y
<< st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0setw(2) << m
<< st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0setw(2) << d;
return os.str();
int main() {
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0vector<Date&gt; vec(31);
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0generate(vec.begin(), vec.end(), [i=1]() mutable { return Date(2017,1,i++); });
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0vector<st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string&gt; strvec;
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0transform(vec.begin(), vec.end(), back_inserter(strvec), st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0mem_fn(&amp;Dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0to_string));
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0cout << "YYYYMMDD\tpY\tpM\tpD\tformat_index\n";
for (auto&amp; str : strvec) {
Date parsed(str);
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0cout << str
<< "\t" << parsed.getYear()
<< "\t" << parsed.getMonth()
<< "\t" << parsed.getDay()
<< "\t" << "?"
<< "\n";
Prints:
YYYYMMDD    pY  pM  pD  format_index
20170101    2017    1   1   ?
20170102    2017    1   2   ?
20170103    2017    1   3   ?
20170104    2017    1   4   ?
20170105    2017    1   5   ?
20170106    2017    1   6   ?
20170107    2017    1   7   ?
20170108    2017    1   8   ?
20170109    2017    1   9   ?
20170110    2017    1   10  ?
20170111    2017    1   11  ?
20170112    2017    1   12  ?
20170113    2017    1   13  ?
20170114    2017    1   14  ?
20170115    2017    1   15  ?
20170116    2017    1   16  ?
20170117    2017    1   17  ?
20170118    2017    1   18  ?
20170119    2017    1   19  ?
20170120    2017    1   20  ?
20170121    2017    1   21  ?
20170122    2017    1   22  ?
20170123    2017    1   23  ?
20170124    2017    1   24  ?
20170125    2017    1   25  ?
20170126    2017    1   26  ?
20170127    2017    1   27  ?
20170128    2017    1   28  ?
20170129    2017    1   29  ?
20170130    2017    1   30  ?
20170131    2017    1   31  ?
¹ just the timezone stuff needs tweaks, mostly
Comment for this answer: Re. Boost's input facets: It's hard to know what's a bug and what isn't due to underspecified contracts. But yeah, it's unusable in my book
Comment for this answer: Very nice...so I guess you ran into the same unintuitive stuff that I did - std/boost folks really needs to fix that!  I definitely appreciate the flexibility of your adaptiveness as well - this is exactly what I am trying to do.  Assuming it works (I'm sure it does), this looks great.

Here is another answer: Using Howard Hinnant's free, open-source C++11/14/17 date/time library, this:
#include "date/date.h"
#include <iostream&gt;
#include <sstream&gt;
#include <string&gt;
#include <vector&gt;
localeIndexFromString(const st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string&amp; delimitedString)
using namespace std;
static vector<string&gt; date_formats
"%Y-%m-%d",
"%Y/%m/%d",
"%m-%d-%Y",
"%m/%d/%Y",
"%d-%b-%Y",
"%Y%m%d"
};
istringstream is;
dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0year_month_day dt;
size_t i;
for (i = 0; i < date_formats.size(); ++i)
is.clear();
is.str(delimitedString);
is &gt;&gt; dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0parse(date_formats[i], dt);
if (!is.fail())
{
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0cout << dt.year() << "\t" << dt.month() << "\t" << dt.day();
return i;
}
return -1;
main()
using namespace dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0literals;
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0vector<dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0year_month_day&gt; vec;
for (auto i = 1; i < 32; ++i)
vec.push_back(2017_y/jan/i);
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0vector<st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0string&gt; strvec;
for (auto const&amp; d : vec)
strvec.push_back(dat75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0format("%Y%m%d", d));
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0cout << "YYYYMMDD\tpY\tpM\tpD\tformat_index\n";
for (size_t i=0; i < strvec.size(); ++i)
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0cout << strvec[i] << "\t";
int fmt_index = localeIndexFromString(strvec[i]);
st75b5:f2c1:f5d7:b954:8dc3:cee9:baf:a8c0cout << "\t" << fmt_index << "\n";
Outputs:
YYYYMMDD        pY        pM        pD        format_index
20170101        2017      Jan       01        5
20170102        2017      Jan       02        5
20170103        2017      Jan       03        5
20170104        2017      Jan       04        5
20170105        2017      Jan       05        5
20170106        2017      Jan       06        5
20170107        2017      Jan       07        5
20170108        2017      Jan       08        5
20170109        2017      Jan       09        5
20170110        2017      Jan       10        5
20170111        2017      Jan       11        5
20170112        2017      Jan       12        5
20170113        2017      Jan       13        5
20170114        2017      Jan       14        5
20170115        2017      Jan       15        5
20170116        2017      Jan       16        5
20170117        2017      Jan       17        5
20170118        2017      Jan       18        5
20170119        2017      Jan       19        5
20170120        2017      Jan       20        5
20170121        2017      Jan       21        5
20170122        2017      Jan       22        5
20170123        2017      Jan       23        5
20170124        2017      Jan       24        5
20170125        2017      Jan       25        5
20170126        2017      Jan       26        5
20170127        2017      Jan       27        5
20170128        2017      Jan       28        5
20170129        2017      Jan       29        5
20170130        2017      Jan       30        5
20170131        2017      Jan       31        5
Comment for this answer: no explanations, but I do appreciate the work you did at the link you provided!  I will read it in greater detail later.
Comment for this answer: Nice syntactic sugar that you added as well!

{'source': 'StackExchangeDataset', 'subset': 'stackoverflow.com', 'metadata': {'AnswerCount': 2, 'CommentCount': 0, 'ContentLicense': 'CC BY-SA 3.0', 'CreationDate': '2017:00:09/28/17', 'FavoriteCount': 2.0, 'PostTypeId': 1, 'Tags': '<c++><date><parsing><boost>', 'Title': 'C++ boost date_input_facet seems to parse dates unexpectedly with incorrect formats passed to the facet constructor', 'ViewCount': 371, 'id': 46474237, 'user_AboutMe': None, 'user_AccountId': 2732810.0, 'user_DisplayName': 'FinanceGuyThatCantCode', 'user_Reputation': 371.0, 'user_Views': 91.0, 'user_id': 2356181.0}}